summaryrefslogtreecommitdiff
path: root/src/main/java/net
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/net')
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/PodcastUploader.java64
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/config/Config.java137
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/control/ConfigureTask.java85
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/control/ObservableTask.java40
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/control/PublishPodcastTask.java25
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/control/S3UploadTask.java86
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/control/UploadTask.java212
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/ui/Action.java17
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/ui/ConfigurationWindow.java157
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/ui/ProgressInterface.java78
-rw-r--r--src/main/java/net/jesterpm/podcastuploader/ui/ProgressWindow.java16
11 files changed, 917 insertions, 0 deletions
diff --git a/src/main/java/net/jesterpm/podcastuploader/PodcastUploader.java b/src/main/java/net/jesterpm/podcastuploader/PodcastUploader.java
new file mode 100644
index 0000000..f386f0d
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/PodcastUploader.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader;
+
+import net.jesterpm.podcastuploader.config.Config;
+import net.jesterpm.podcastuploader.control.ConfigureTask;
+import net.jesterpm.podcastuploader.control.UploadTask;
+import net.jesterpm.podcastuploader.ui.ConfigurationWindow;
+import net.jesterpm.podcastuploader.ui.ProgressWindow;
+
+/**
+ * Application entry-point.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class PodcastUploader {
+ private static final String DEFAULT_CONFIG = System.getProperty("user.home")
+ + System.getProperty("file.separator") + ".podcastuploader";
+
+ public static void main(String... args) {
+ final Config appconfig = new Config(DEFAULT_CONFIG);
+
+ if (args.length == 0) {
+ startConfigure(appconfig);
+
+ } else {
+ if (args[0].equals("--help")) {
+ printHelp();
+ }
+
+ startUpload(appconfig, args[0]);
+ }
+ }
+
+ private static void printHelp() {
+ System.out.println("PodcastUploader - Podcast upload utility.");
+ System.out.println("Created by Jesse Morgan <jesse@jesterpm.net>");
+ System.out.println("http://jesterpm.net/projects/podcastuploader");
+ System.out.println();
+ System.out.println("Usage: PodcastUploader [directory]");
+ System.out.println(
+ "When started with no arguments, the configuration dialog is opened.");
+ System.out.println(
+ "When started with one argument, it is assumed to be a directory\n"
+ + "with a metadata.txt file with upload instructions.");
+ System.out.println();
+ }
+
+ private static void startConfigure(final Config appconfig) {
+ ConfigurationWindow win = new ConfigurationWindow();
+ ConfigureTask task = new ConfigureTask(appconfig, win);
+
+ task.run();
+ }
+
+ public static void startUpload(final Config appconfig, final String dir) {
+ ProgressWindow win = new ProgressWindow();
+ UploadTask task = new UploadTask(appconfig, win, dir);
+
+ task.run();
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/config/Config.java b/src/main/java/net/jesterpm/podcastuploader/config/Config.java
new file mode 100644
index 0000000..b9260e0
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/config/Config.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.config;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.File;
+
+import java.text.ParseException;
+
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Configuration and metadata parser.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Config {
+ private final String mFilename;
+ private Map<String, String> mConfig;
+
+ /**
+ * States the parser can be in.
+ */
+ private enum TokenizerState {
+ OUT_OF_ENTRY, IN_ENTRY;
+ }
+
+ /**
+ * Create a new Config object based on the given file.
+ */
+ public Config(final String filename) {
+ mFilename = filename;
+ mConfig = new HashMap<String, String>();
+ parse();
+ }
+
+ private void parse() {
+ try {
+ final File configfile = new File(mFilename);
+ if (configfile.isFile()) {
+ BufferedReader in = new BufferedReader(new FileReader(mFilename));
+
+ int lineno = 0;
+ String line;
+ String key = "";
+ String value = "";
+ TokenizerState state = TokenizerState.OUT_OF_ENTRY;
+ while ((line = in.readLine()) != null) {
+ lineno++;
+ if (line.length() == 0) {
+ continue;
+ }
+
+ switch (state) {
+ case IN_ENTRY:
+ if (Character.isWhitespace(line.charAt(0))) {
+ value += "\n" + line.trim();
+ break;
+
+ } else {
+ // Beginning new entry. Save old and pass through.
+ mConfig.put(key, value);
+ key = value = "";
+ state = TokenizerState.OUT_OF_ENTRY;
+ // NB Intentionally falling through...
+ }
+
+ case OUT_OF_ENTRY:
+ if (line.charAt(0) == '#') {
+ continue;
+ }
+
+ final int pos = line.indexOf(':');
+ if (pos == -1) {
+ throw new ParseException("Missing : at line " + lineno, lineno);
+ }
+ key = line.substring(0, pos).trim().toLowerCase();
+ if (key.length() == 0) {
+ throw new ParseException("Zero-length key on line " + lineno,
+ lineno);
+ }
+ value = line.substring(pos + 1).trim();
+ state = TokenizerState.IN_ENTRY;
+ break;
+ }
+ }
+
+ // Catch last key/value
+ if (state == TokenizerState.IN_ENTRY) {
+ mConfig.put(key, value);
+ }
+
+ in.close();
+ }
+
+ } catch (final Exception e) {
+ System.err.println("[ERROR] Failed to load config from " + mFilename + ". "
+ + e.getMessage());
+ }
+ }
+
+ public void save() {
+ try {
+ BufferedWriter out = new BufferedWriter(new FileWriter(mFilename));
+
+ for (Map.Entry<String, String> entry : mConfig.entrySet()) {
+ out.write(entry.getKey());
+ out.write(": ");
+ out.write(entry.getValue());
+ out.newLine();
+ }
+
+ out.close();
+
+ } catch (final Exception e) {
+ System.err.println("[ERROR] Failed to save configuration: " + e.getMessage());
+ }
+ }
+
+ public String get(final String key) {
+ return mConfig.get(key);
+ }
+
+ public void put(final String key, final String obj) {
+ mConfig.put(key, obj);
+ }
+
+ public Map<String, String> getMap() {
+ return mConfig;
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/control/ConfigureTask.java b/src/main/java/net/jesterpm/podcastuploader/control/ConfigureTask.java
new file mode 100644
index 0000000..0158155
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/control/ConfigureTask.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.control;
+
+import net.jesterpm.podcastuploader.config.Config;
+
+import net.jesterpm.podcastuploader.ui.Action;
+import net.jesterpm.podcastuploader.ui.ConfigurationWindow;
+
+/**
+ * Controller for the ConfigurationWindow.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ConfigureTask {
+ private final Config mAppConfig;
+ private final ConfigurationWindow mWin;
+
+ public ConfigureTask(final Config appconfig, final ConfigurationWindow win) {
+ mAppConfig = appconfig;
+ mWin = win;
+
+ mWin.addSaveAction(new Action() {
+ public void onAction() {
+ populateConfig();
+ mAppConfig.save();
+ mWin.setVisible(false);
+ System.exit(0);
+ }
+ });
+
+ mWin.addCancelAction(new Action() {
+ public void onAction() {
+ mWin.setVisible(false);
+ System.exit(0);
+ }
+ });
+
+ mWin.addAuthorizeAction(new Action() {
+ public void onAction() {
+ populateConfig();
+ getAuthorization();
+ }
+ });
+
+ populateWindow();
+ }
+
+ /**
+ * Set the fields in the configuration window.
+ */
+ private void populateWindow() {
+ mWin.setAWSKey(mAppConfig.get("AWSAccessKeyId"));
+ mWin.setAWSSecret(mAppConfig.get("AWSSecretKey"));
+ mWin.setS3Bucket(mAppConfig.get("S3Bucket"));
+ mWin.setMetadataServer(mAppConfig.get("MetadataURL"));
+ mWin.setHasAuthKey(mAppConfig.get("MetadataAuthKey") != null);
+ }
+
+ /**
+ * Populate the config from the window.
+ */
+ private void populateConfig() {
+ mAppConfig.put("AWSAccessKeyId", mWin.getAWSKey());
+ mAppConfig.put("AWSSecretKey", mWin.getAWSSecret());
+ mAppConfig.put("S3Bucket", mWin.getS3Bucket());
+ mAppConfig.put("MetadataURL", mWin.getMetadataServer());
+ }
+
+ /**
+ * Display the window.
+ */
+ public void run() {
+ mWin.setVisible(true);
+ }
+
+ /**
+ * Get an authorization token from the metadata service.
+ */
+ private void getAuthorization() {
+
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/control/ObservableTask.java b/src/main/java/net/jesterpm/podcastuploader/control/ObservableTask.java
new file mode 100644
index 0000000..381145e
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/control/ObservableTask.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.control;
+
+import java.util.Observable;
+
+/**
+ * ProgressInterface is an abstract based class for any interface that
+ * provides progress notification. The class tracks the progress of multiple
+ * simultaneous tasks and aggregates their progress.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class ObservableTask extends Observable {
+ private float mProgress;
+
+ public ObservableTask() {
+ mProgress = 0;
+ }
+
+ /**
+ * @return The percentage complete as a float between 0 and 1.
+ */
+ public float getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * Set the current progress.
+ *
+ * @param percentComplete The percentage complete as a float between 0 and 1.
+ */
+ protected void setProgress(float percentComplete) {
+ mProgress = percentComplete;
+ setChanged();
+ notifyObservers();
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/control/PublishPodcastTask.java b/src/main/java/net/jesterpm/podcastuploader/control/PublishPodcastTask.java
new file mode 100644
index 0000000..fdee900
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/control/PublishPodcastTask.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.control;
+
+import java.util.Map;
+
+import net.jesterpm.podcastuploader.config.Config;
+
+/**
+ * Task to publish podcast meta-data to a server.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class PublishPodcastTask implements Runnable {
+ public PublishPodcastTask(final Config appConfig, final Map<String, String> metadata) {
+
+ }
+
+ @Override
+ public void run() {
+
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/control/S3UploadTask.java b/src/main/java/net/jesterpm/podcastuploader/control/S3UploadTask.java
new file mode 100644
index 0000000..8e9f9ab
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/control/S3UploadTask.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.control;
+
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.s3.AmazonS3Client;
+
+import java.util.concurrent.ExecutorService;
+
+import net.jesterpm.podcastuploader.config.Config;
+
+/**
+ * Task for uploading a single file to S3.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class S3UploadTask extends ObservableTask implements Runnable {
+ private final ExecutorService mExecutor;
+
+ private final AmazonS3Client mClient;
+
+ private final String mBucket;
+ private final String mLocalFile;
+ private final String mS3Key;
+
+ private int mCurrentChunk;
+ private int mTotalChunks;
+
+ /**
+ * Prepare a new S3UploadTask.
+ *
+ * @param appConfig The current running appConfig.
+ * @param localFile The name of the local file to upload.
+ * @param remoteFile The key to use for the file in S3.
+ */
+ public S3UploadTask(final Config appConfig, final String localFile,
+ final String remoteFile, final ExecutorService executor) {
+
+ mExecutor = executor;
+
+ mClient = new AmazonS3Client(new BasicAWSCredentials(
+ appConfig.get("AWSAccessKeyId"), appConfig.get("AWSSecretKey")));
+
+ mBucket = appConfig.get("S3Bucket");
+ mLocalFile = localFile;
+ mS3Key = remoteFile;
+
+ mCurrentChunk = 0;
+ mTotalChunks = 1; // Avoid div-by-0 in getProgress()
+ }
+
+ /**
+ * Start the upload.
+ */
+ @Override
+ public void run() {
+ // Create bucket if needed
+ createBucket();
+
+ // Start Upload
+ }
+
+ /**
+ * Create the S3 bucket if it doesn't already exist.
+ */
+ private void createBucket() {
+
+ }
+
+ /**
+ * @return The number of file parts uploaded over the total number of file parts.
+ */
+ @Override
+ public float getProgress() {
+ return (float) mCurrentChunk / mTotalChunks;
+ }
+
+ /**
+ * @return The S3 key this object uploads to.
+ */
+ public String getS3Key() {
+ return mS3Key;
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/control/UploadTask.java b/src/main/java/net/jesterpm/podcastuploader/control/UploadTask.java
new file mode 100644
index 0000000..a5fdf05
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/control/UploadTask.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.control;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import net.jesterpm.podcastuploader.config.Config;
+import net.jesterpm.podcastuploader.ui.ProgressInterface;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class UploadTask {
+ /**
+ * This is the filename of the metadata file with a path separator prefixed.
+ */
+ private static final String METADATA_FILE =
+ System.getProperty("file.separator") + "/metadata.txt";
+
+ /**
+ * Progress window.
+ */
+ private final ProgressInterface mProgressInterface;
+
+ /**
+ * Application config.
+ */
+ private final Config mAppConfig;
+
+ /**
+ * Podcast metadata file.
+ */
+ private final Config mMetadata;
+
+ /**
+ * Thread Pool used for and by the UploadTasks.
+ */
+ private final ExecutorService mExecutor;
+
+ /**
+ * UploadTask Constructor.
+ * @param appconfig The application config
+ * @param win The progress window user interface.
+ * @param dir The directory to upload.
+ */
+ public UploadTask(final Config appconfig, final ProgressInterface progressInterface,
+ final String dir) {
+
+ mProgressInterface = progressInterface;
+ mAppConfig = appconfig;
+
+ mMetadata = new Config(dir + METADATA_FILE);
+
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ /**
+ * Uploads the podcast described in the metadata file.
+ *
+ * Expected keys:
+ * date The podcast date.
+ * title The podcast title.
+ * author The podcast author.
+ *
+ * Optional keys:
+ * series The series the video is part of.
+ * video Name of the video file.
+ * audio Name of the audio file.
+ * video_lowres Name of the low-res video file
+ * image Image associated with the podcast
+ * mobileimage Image for mobile phones
+ * description Podcast description
+ *
+ * Video and/or audio is required, as without one of them there is nothing
+ * to upload.
+ */
+ public void run() {
+ final String baseFilename;
+
+ try {
+ DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT);
+ final Date date = fmt.parse(mMetadata.get("date"));
+
+ fmt = new SimpleDateFormat("yyyyMMdd");
+ baseFilename = fmt.format(date) + "-" + safeString(mMetadata.get("title"));
+
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Build a list of files to upload.
+ Map<String, S3UploadTask> files = getFilesToUpload(baseFilename);
+ for (S3UploadTask task : files.values()) {
+ mExecutor.submit(task);
+ mProgressInterface.monitorTask(task);
+ }
+
+ // Wait until all uploads complete.
+
+ // Publish the podcast metadata.
+ Map<String, String> metadata = new HashMap<String, String>(mMetadata.getMap());
+
+ for (Map.Entry<String, S3UploadTask> entry : files.entrySet()) {
+ metadata.put(entry.getKey(), entry.getValue().getS3Key());
+ }
+
+ PublishPodcastTask task = new PublishPodcastTask(mAppConfig, metadata);
+ task.run();
+ }
+
+ /**
+ * Build the list of S3UploadTasks to execute.
+ */
+ private Map<String, S3UploadTask> getFilesToUpload(final String basename) {
+ Map<String, S3UploadTask> files = new HashMap<String, S3UploadTask>();
+
+ String localFile;
+ String remoteFile;
+
+ localFile = mMetadata.get("video");
+ if (localFile != null) {
+ remoteFile = basename + "-video" + fileExtension(localFile);
+ files.put("video", new S3UploadTask(mAppConfig, localFile,
+ remoteFile, mExecutor));
+ }
+
+ localFile = mMetadata.get("video_lowres");
+ if (localFile != null) {
+ remoteFile = basename + "-videolow" + fileExtension(localFile);
+ files.put("video_lowres", new S3UploadTask(mAppConfig, localFile,
+ remoteFile, mExecutor));
+ }
+
+ localFile = mMetadata.get("audio");
+ if (localFile != null) {
+ remoteFile = basename + "-audio" + fileExtension(localFile);
+ files.put("audio", new S3UploadTask(mAppConfig, localFile,
+ remoteFile, mExecutor));
+ }
+
+ localFile = mMetadata.get("image");
+ if (localFile != null) {
+ remoteFile = basename + "-image" + fileExtension(localFile);
+ files.put("image", new S3UploadTask(mAppConfig, localFile,
+ remoteFile, mExecutor));
+ }
+
+ localFile = mMetadata.get("mobileimage");
+ if (localFile != null) {
+ remoteFile = basename + "-mobileimage" + fileExtension(localFile);
+ files.put("mobileimage", new S3UploadTask(mAppConfig, localFile,
+ remoteFile, mExecutor));
+ }
+
+ return files;
+ }
+
+ /**
+ * @return the extension from the given filename.
+ */
+ private String fileExtension(final String file) {
+ int pos = file.lastIndexOf('.');
+
+ if (pos == -1) {
+ return "";
+
+ } else {
+ return file.substring(pos);
+ }
+ }
+
+ /**
+ * Transform str into a URL safe string by substituting spaces with dashes
+ * and dropping all other non-alphanumeric characters.
+ *
+ * @param str The String to transform.
+ * @return The transformed string.
+ */
+ private String safeString(String str) {
+ char[] newStr = str.trim().toLowerCase().toCharArray();
+ boolean firstSpace = true;
+ int p = 0;
+ for (int i = 0; i < newStr.length; i++) {
+ if (Character.isWhitespace(newStr[i])) {
+ if (firstSpace) {
+ newStr[p++] = '-';
+ firstSpace = false;
+ }
+
+ } else if (Character.isLetterOrDigit(newStr[i])) {
+ newStr[p++] = newStr[i];
+ firstSpace = true;
+
+ } else {
+ firstSpace = true;
+ }
+ }
+
+ return new String(newStr, 0, p);
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/ui/Action.java b/src/main/java/net/jesterpm/podcastuploader/ui/Action.java
new file mode 100644
index 0000000..5ec4bde
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/ui/Action.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.ui;
+
+/**
+ * Action handler for the UI.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface Action {
+ /**
+ * This method is called when the action is performed.
+ */
+ public void onAction();
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/ui/ConfigurationWindow.java b/src/main/java/net/jesterpm/podcastuploader/ui/ConfigurationWindow.java
new file mode 100644
index 0000000..d0dc96d
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/ui/ConfigurationWindow.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.ui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+
+import javax.swing.border.EmptyBorder;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+/**
+ * UI for the configuration window.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ConfigurationWindow extends JFrame {
+ private final JTextField mAWSKey;
+ private final JTextField mAWSSecret;
+ private final JTextField mS3Bucket;
+ private final JTextField mMetadataServer;
+ private final JButton mAuthorize;
+ private final JButton mSave;
+
+ public ConfigurationWindow() {
+ super("Podcast Uploader Configuration");
+
+ JPanel panel = new JPanel();
+ panel.setBorder(new EmptyBorder(10, 10, 10, 10));
+ panel.setLayout(new GridBagLayout());
+ add(panel);
+
+ mAWSKey = new JTextField();
+ mAWSSecret = new JTextField();
+ mS3Bucket = new JTextField();
+ mMetadataServer = new JTextField();
+ mAuthorize = new JButton("Authorize App");
+ mSave = new JButton("Save");
+ mSave.setDefaultCapable(true);
+
+ GridBagConstraints labelConstraint = new GridBagConstraints();
+ GridBagConstraints fieldConstraint = new GridBagConstraints();
+
+ labelConstraint.gridx = 0;
+ labelConstraint.gridy = GridBagConstraints.RELATIVE;
+
+ fieldConstraint.gridx = 1;
+ fieldConstraint.gridy = GridBagConstraints.RELATIVE;
+ fieldConstraint.fill = GridBagConstraints.HORIZONTAL;
+ fieldConstraint.weightx = 1;
+
+ panel.add(new JLabel("Podcast Server:", JLabel.RIGHT), labelConstraint);
+ panel.add(mMetadataServer, fieldConstraint);
+
+ panel.add(new JLabel("AWS Access Key:", JLabel.RIGHT), labelConstraint);
+ panel.add(mAWSKey, fieldConstraint);
+
+ panel.add(new JLabel("AWS Secret Key:", JLabel.RIGHT), labelConstraint);
+ panel.add(mAWSSecret, fieldConstraint);
+
+ panel.add(new JLabel("S3 Bucket:", JLabel.RIGHT), labelConstraint);
+ panel.add(mS3Bucket, fieldConstraint);
+
+ GridBagConstraints buttonConstraint = new GridBagConstraints();
+ buttonConstraint.gridy = 5;
+ buttonConstraint.gridwidth = 2;
+ buttonConstraint.weighty = 1;
+ panel.add(mAuthorize, buttonConstraint);
+
+ buttonConstraint.gridx = 1;
+ buttonConstraint.gridy = 7;
+ buttonConstraint.gridwidth = 1;
+ buttonConstraint.anchor = GridBagConstraints.LAST_LINE_END;
+ panel.add(mSave, buttonConstraint);
+
+ pack();
+ Dimension d = getPreferredSize();
+ d.height += 20;
+ d.width += 50;
+ setMinimumSize(d);
+ }
+
+ public void setAWSKey(final String value) {
+ mAWSKey.setText(value);
+ }
+
+ public String getAWSKey() {
+ return mAWSKey.getText();
+ }
+
+ public void setAWSSecret(final String value) {
+ mAWSSecret.setText(value);
+ }
+
+ public String getAWSSecret() {
+ return mAWSSecret.getText();
+ }
+
+ public void setS3Bucket(final String value) {
+ mS3Bucket.setText(value);
+ }
+
+ public String getS3Bucket() {
+ return mS3Bucket.getText();
+ }
+
+ public void setMetadataServer(final String value) {
+ mMetadataServer.setText(value);
+ }
+
+ public String getMetadataServer() {
+ return mMetadataServer.getText();
+ }
+
+ public void setHasAuthKey(final boolean value) {
+ if (value) {
+ mAuthorize.setText("Reauthorize App");
+
+ } else {
+ mAuthorize.setText("Authorize App");
+ }
+ }
+
+ public void addAuthorizeAction(final Action a) {
+ mAuthorize.addActionListener(new ActionListener() {
+ public void actionPerformed(final ActionEvent e) {
+ a.onAction();
+ }
+ });
+ }
+
+ public void addSaveAction(final Action a) {
+ mSave.addActionListener(new ActionListener() {
+ public void actionPerformed(final ActionEvent e) {
+ a.onAction();
+ }
+ });
+ }
+
+ public void addCancelAction(final Action a) {
+ addWindowListener(new WindowAdapter() {
+ public void windowClosing(final WindowEvent e) {
+ a.onAction();
+ }
+ });
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/ui/ProgressInterface.java b/src/main/java/net/jesterpm/podcastuploader/ui/ProgressInterface.java
new file mode 100644
index 0000000..3e85667
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/ui/ProgressInterface.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.ui;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Observer;
+import java.util.Observable;
+
+import net.jesterpm.podcastuploader.control.ObservableTask;
+
+/**
+ * ProgressInterface is an abstract based class for any interface that
+ * provides progress notification. The class tracks the progress of multiple
+ * simultaneous tasks and aggregates their progress.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class ProgressInterface implements Observer {
+ private Map<ObservableTask, Float> mProgressMap;
+
+ /**
+ * Creates a new ProgressInterface monitoring nothing.
+ */
+ public ProgressInterface() {
+ mProgressMap = new HashMap<ObservableTask, Float>();
+ }
+
+ /**
+ * Begin monitoring the given task.
+ *
+ * @param task The ObservableTask to monitor.
+ */
+ public void monitorTask(final ObservableTask task) {
+ task.addObserver(this);
+ mProgressMap.put(task, 0f);
+ setProgress(getProgress());
+ }
+
+ /**
+ * Called when the progress changes.
+ *
+ * @param progress The current percentage complete,
+ * indicated by a float ranging from 0 to 1.
+ */
+ protected abstract void setProgress(float percentComplete);
+
+ /**
+ * Called when the progress for a task changes.
+ * @param task The task that changed.
+ * @param arg Unused.
+ */
+ @Override
+ public void update(Observable task, Object arg) {
+ ObservableTask observableTask = (ObservableTask) task;
+ mProgressMap.put(observableTask, observableTask.getProgress());
+ setProgress(getProgress());
+ }
+
+ /**
+ * @return A float ranging from 0 to 1 representing the aggregate progress.
+ * If the ProgressInterface is not monitoring any tasks, this will return 0.
+ */
+ public float getProgress() {
+ if (mProgressMap.size() == 0) {
+ return 0;
+ }
+
+ float totalProgress = 0;
+ for (float taskProgress : mProgressMap.values()) {
+ totalProgress += taskProgress;
+ }
+
+ return totalProgress / mProgressMap.size();
+ }
+}
diff --git a/src/main/java/net/jesterpm/podcastuploader/ui/ProgressWindow.java b/src/main/java/net/jesterpm/podcastuploader/ui/ProgressWindow.java
new file mode 100644
index 0000000..01daa83
--- /dev/null
+++ b/src/main/java/net/jesterpm/podcastuploader/ui/ProgressWindow.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package net.jesterpm.podcastuploader.ui;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ProgressWindow extends ProgressInterface {
+ @Override
+ public void setProgress(float percentComplete) {
+
+ }
+}