Index: java/HTTPFileDownloader/.classpath
===================================================================
--- java/HTTPFileDownloader/.classpath	(revision 741)
+++ java/HTTPFileDownloader/.classpath	(revision 741)
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
Index: java/HTTPFileDownloader/.project
===================================================================
--- java/HTTPFileDownloader/.project	(revision 741)
+++ java/HTTPFileDownloader/.project	(revision 741)
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>HTTPFileDownloader</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
Index: java/HTTPFileDownloader/.settings/org.eclipse.jdt.core.prefs
===================================================================
--- java/HTTPFileDownloader/.settings/org.eclipse.jdt.core.prefs	(revision 741)
+++ java/HTTPFileDownloader/.settings/org.eclipse.jdt.core.prefs	(revision 741)
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
Index: java/HTTPFileDownloader/src/net/oni2/httpfiledownloader/FileDownloadListener.java
===================================================================
--- java/HTTPFileDownloader/src/net/oni2/httpfiledownloader/FileDownloadListener.java	(revision 741)
+++ java/HTTPFileDownloader/src/net/oni2/httpfiledownloader/FileDownloadListener.java	(revision 741)
@@ -0,0 +1,24 @@
+package net.oni2.httpfiledownloader;
+
+/**
+ * Interface for listeners to status updates during file download
+ * 
+ * @author Christian Illy
+ */
+public interface FileDownloadListener {
+
+	/**
+	 * Called after checking out / updating a single file
+	 * 
+	 * @param source
+	 *            Source of event
+	 * @param state
+	 *            Current state of downloader
+	 * @param done
+	 *            Bytes done
+	 * @param total
+	 *            Total bytes for the download
+	 */
+	public void statusUpdate(FileDownloader source,
+			FileDownloader.EState state, int done, int total);
+}
Index: java/HTTPFileDownloader/src/net/oni2/httpfiledownloader/FileDownloader.java
===================================================================
--- java/HTTPFileDownloader/src/net/oni2/httpfiledownloader/FileDownloader.java	(revision 741)
+++ java/HTTPFileDownloader/src/net/oni2/httpfiledownloader/FileDownloader.java	(revision 741)
@@ -0,0 +1,260 @@
+package net.oni2.httpfiledownloader;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashSet;
+
+/**
+ * @author Christian Illy
+ */
+public class FileDownloader implements Runnable {
+	/**
+	 * @author Christian Illy
+	 */
+	public enum EState {
+		/**
+		 * Downloader initialized but not started
+		 */
+		INIT,
+		/**
+		 * Download running
+		 */
+		RUNNING,
+		/**
+		 * Download suspended
+		 */
+		PAUSED,
+		/**
+		 * Download interrupted
+		 */
+		INTERRUPTED,
+		/**
+		 * Download finished successfully
+		 */
+		FINISHED,
+		/**
+		 * Aborted because of an error
+		 */
+		ERROR
+	};
+
+	private HashSet<FileDownloadListener> listeners = new HashSet<FileDownloadListener>();
+	private Thread t = null;
+	private URL url = null;
+	private File target = null;
+	private int fileLength = Integer.MAX_VALUE;
+	private int downloaded = 0;
+
+	private EState state = EState.INIT;
+
+	/**
+	 * @param url
+	 *            URL of file to download
+	 * @param target
+	 *            Path of target file to save to
+	 * @throws IOException
+	 *             If url could not be opened
+	 */
+	public FileDownloader(String url, File target) throws IOException {
+		this.url = new URL(url);
+		this.target = target;
+	}
+
+	/**
+	 * @param listener
+	 *            Listener to add
+	 */
+	public void addListener(FileDownloadListener listener) {
+		listeners.add(listener);
+	}
+
+	/**
+	 * @param listener
+	 *            Listener to remove
+	 */
+	public void removeListener(FileDownloadListener listener) {
+		listeners.remove(listener);
+	}
+
+	/**
+	 * Start the download process
+	 */
+	public synchronized void start() {
+		if (t == null) {
+			t = new Thread(this);
+			t.start();
+			state = EState.RUNNING;
+		}
+	}
+
+	/**
+	 * @param suspend
+	 *            Suspend or resume
+	 */
+	public synchronized void suspend(boolean suspend) {
+		if ((state == EState.RUNNING) || (state == EState.PAUSED)) {
+			if (suspend)
+				state = EState.PAUSED;
+			else
+				state = EState.RUNNING;
+			updateStatus(downloaded, fileLength);
+		}
+	}
+
+	/**
+	 * Stop (abort) download
+	 */
+	public synchronized void stop() {
+		if (state != EState.FINISHED) {
+			state = EState.INTERRUPTED;
+			if (t != null) {
+				try {
+					t.join();
+				} catch (InterruptedException e) {
+					e.printStackTrace();
+				}
+				t = null;
+			}
+			updateStatus(0, 1);
+			if (target.exists())
+				target.delete();
+		}
+	}
+
+	/**
+	 * @return current state
+	 */
+	public EState getState() {
+		return state;
+	}
+
+	private synchronized void updateStatus(int current, int total) {
+		downloaded = current;
+		for (FileDownloadListener l : listeners) {
+			l.statusUpdate(this, state, current, total);
+		}
+	}
+
+	@Override
+	public void run() {
+		int downloaded = 0;
+		String strLastModified = null;
+		String strEtag = null;
+		RandomAccessFile outFile = null;
+		try {
+			outFile = new RandomAccessFile(target, "rw");
+		} catch (FileNotFoundException e1) {
+			e1.printStackTrace();
+			state = EState.ERROR;
+			updateStatus(downloaded, fileLength);
+			return;
+		}
+
+		try {
+			while (downloaded < fileLength) {
+				switch (state) {
+					case ERROR:
+						updateStatus(downloaded, fileLength);
+						return;
+					case PAUSED:
+						try {
+							Thread.sleep(100);
+						} catch (InterruptedException e) {
+							e.printStackTrace();
+						}
+						break;
+					case INTERRUPTED:
+						return;
+					case RUNNING:
+						BufferedInputStream input = null;
+						try {
+							URLConnection connection = url.openConnection();
+							connection.setRequestProperty("Cache-Control", "no-cache");
+							if (downloaded == 0) {
+								connection.connect();
+								strLastModified = connection
+										.getHeaderField("Last-Modified");
+								strEtag = connection.getHeaderField("ETag");
+								fileLength = connection.getContentLength();
+							} else {
+								connection.setRequestProperty("Range", "bytes="
+										+ downloaded + "-");
+								if (strEtag != null)
+									connection.setRequestProperty("If-Range",
+											strEtag);
+								else
+									connection.setRequestProperty("If-Range",
+											strLastModified);
+								connection.connect();
+							}
+
+							// Setup streams and buffers.
+							input = new BufferedInputStream(
+									connection.getInputStream(), 8192);
+							if (downloaded > 0)
+								outFile.seek(downloaded);
+							byte data[] = new byte[1024];
+
+							// Download file.
+							int dataRead = 0;
+							int i = 0;
+							while (((dataRead = input.read(data, 0, 1024)) != -1)
+									&& (state == EState.RUNNING)) {
+								outFile.write(data, 0, dataRead);
+								downloaded += dataRead;
+								if (downloaded >= fileLength)
+									break;
+
+								i++;
+								if ((i % 50) == 0)
+									updateStatus(downloaded, fileLength);
+							}
+							input.close();
+						} catch (IOException e) {
+							// TODO Auto-generated catch block
+							e.printStackTrace();
+							try {
+								if (input != null)
+									input.close();
+							} catch (IOException e2) {
+								e2.printStackTrace();
+							}
+						}
+						break;
+					default:
+						break;
+				}
+			}
+		} finally {
+			try {
+				// Close streams.
+				outFile.close();
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+
+		state = EState.FINISHED;
+		updateStatus(downloaded, fileLength);
+	}
+
+	/**
+	 * @return the target
+	 */
+	public File getTarget() {
+		return target;
+	}
+
+	/**
+	 * @return the downloaded size
+	 */
+	public int getDownloaded() {
+		return downloaded;
+	}
+
+}
