/*******************************************************************************
* Copyright (c) 2003, 2010 Albert P�rez and RoboRumble contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://robocode.sourceforge.net/license/epl-v10.html
*
* Contributors:
* Albert P�rez
* - Initial API and implementation
* Flemming N. Larsen
* - Completely rewritten to be fully multi-threaded so that download is not
* blocked if a connection is hanging. In addition, this version of
* FileTransfer support sessions
*******************************************************************************/
package net.sf.robocode.roborumble.netengine;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Utility class for downloading files from the net and copying files.
*
* @author Flemming N. Larsen (original)
*/
public class FileTransfer {
private final static int CONNECTION_TIMEOUT = 5000; // 5 seconds
/**
* Represents the download status returned when downloading files.
*
* @author Flemming N. Larsen
*/
public enum DownloadStatus {
OK, // The download was succesful
COULD_NOT_CONNECT, // Connection problem
FILE_NOT_FOUND, // The file to download was not found
}
/**
* Daemon worker thread containing a 'finish' flag for waiting and notifying
* when the thread has finished it's job.
*
* @author Flemming N. Larsen
*/
private static class WorkerThread extends Thread {
final Object monitor = new Object();
volatile boolean isFinished;
public WorkerThread(String name) {
super(name);
setDaemon(true);
}
void notifyFinish() {
// Notify that this thread is finish
synchronized (monitor) {
isFinished = true;
monitor.notifyAll();
}
}
}
/*
* Returns a session id for keeping a session on a HTTP site.
*
* @param url the url of the HTTP site
* @return a session id for keeping a session on a HTTP site or null if no
* session is available
*/
public static String getSessionId(String url) {
HttpURLConnection con = null;
try {
// Open connection
con = (HttpURLConnection) new URL(url).openConnection();
if (con == null) {
throw new IOException("Could not open connection to '" + url + "'");
}
// Get a session id if available
final GetSessionIdThread sessionIdThread = new GetSessionIdThread(con);
sessionIdThread.start();
// Wait for the session id
synchronized (sessionIdThread.monitor) {
while (!sessionIdThread.isFinished) {
try {
sessionIdThread.monitor.wait(CONNECTION_TIMEOUT);
sessionIdThread.interrupt();
} catch (InterruptedException e) {
// Immediately reasserts the exception by interrupting the caller thread itself
Thread.currentThread().interrupt();
return null;
}
}
}
// Return the session id
return sessionIdThread.sessionId;
} catch (final IOException e) {
return null;
} finally {
// Make sure the connection is disconnected.
// This will cause threads using the connection to throw an exception
// and thereby terminate if they were hanging.
if (con != null) {
con.disconnect();
}
}
}
/**
* Worker thread used for getting the session id of an already open HTTP
* connection.
*
* @author Flemming N. Larsen
*/
private final static class GetSessionIdThread extends WorkerThread {
// The resulting session id to read out
String sessionId;
final HttpURLConnection con;
GetSessionIdThread(HttpURLConnection con) {
super("FileTransfer: Get session ID");
this.con = con;
}
@Override
public void run() {
try {
// Get the cookie value
final String cookieVal = con.getHeaderField("Set-Cookie");
// Extract the session id from the cookie value
if (cookieVal != null) {
sessionId = cookieVal.substring(0, cookieVal.indexOf(";"));
}
} catch (final Exception e) {
sessionId = null;
}
// Notify that this thread is finish
notifyFinish();
}
}
/**
* Downloads a file from a HTTP site.
*
* @param url the url of the HTTP site to download the file from
* @param filename the filename of the destination file
* @param sessionId an optional session id if the download is session based
* @return the download status, which is DownloadStatus.OK if the download
* completed successfully; otherwise an error occurred
*/
public static DownloadStatus download(String url, String filename, String sessionId) {
HttpURLConnection con = null;
try {
// Open connection
con = (HttpURLConnection) new URL(url).openConnection();
if (con == null) {
throw new IOException("Could not open connection to '" + url + "'");
}
// Set the session id if it is specified
if (sessionId != null) {
con.setRequestProperty("Cookie", sessionId);
}
// Prepare the connection
con.setDoInput(true);
con.setDoOutput(false);
con.setUseCaches(false);
// Make the connection
con.setConnectTimeout(CONNECTION_TIMEOUT);
con.connect();
// Begin the download
final DownloadThread downloadThread = new DownloadThread(con, filename);
downloadThread.start();
// Wait for the download to complete
synchronized (downloadThread.monitor) {
while (!downloadThread.isFinished) {
try {
downloadThread.monitor.wait();
} catch (InterruptedException e) {
return DownloadStatus.COULD_NOT_CONNECT;
}
}
}
// Return the download status
return downloadThread.status;
} catch (final IOException e) {
return DownloadStatus.COULD_NOT_CONNECT;
} finally {
// Make sure the connection is disconnected.
// This will cause threads using the connection to throw an exception
// and thereby terminate if they were hanging.
try {
if (con != null) {
con.disconnect();
}
} catch (Throwable ignore) {// we expect this, right ?
}
}
}
/**
* Worker thread used for downloading a file from an already open HTTP
* connection.
*
* @author Flemming N. Larsen
*/
private final static class DownloadThread extends WorkerThread {
// The download status to be read out
DownloadStatus status = DownloadStatus.COULD_NOT_CONNECT; // Default error
final HttpURLConnection con;
final String filename;
InputStream in;
OutputStream out;
DownloadThread(HttpURLConnection con, String filename) {
super("FileTransfer: Download");
this.con = con;
this.filename = filename;
}
@Override
public void run() {
try {
// Start getting the response code
final GetResponseCodeThread responseThread = new GetResponseCodeThread(con);
responseThread.start();
// Wait for the response to finish
synchronized (responseThread.monitor) {
while (!responseThread.isFinished) {
try {
responseThread.monitor.wait(CONNECTION_TIMEOUT);
responseThread.interrupt();
} catch (InterruptedException e) {
notifyFinish();
return;
}
}
}
final int responseCode = responseThread.responseCode;
if (responseCode == -1) {
// Terminate if we did not get the response code
notifyFinish();
return;
} else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
// Terminate if the HTTP page containing the file was not found
status = DownloadStatus.FILE_NOT_FOUND;
notifyFinish();
return;
} else if (responseCode != HttpURLConnection.HTTP_OK) {
// Generally, terminate if did not receive a OK response
notifyFinish();
return;
}
// Start getting the size of the file to download
final GetContentLengthThread contentLengthThread = new GetContentLengthThread(con);
contentLengthThread.start();
// Wait for the file size
synchronized (contentLengthThread.monitor) {
while (!contentLengthThread.isFinished) {
try {
contentLengthThread.monitor.wait(CONNECTION_TIMEOUT);
contentLengthThread.interrupt();
} catch (InterruptedException e) {
notifyFinish();
return;
}
}
}
final int size = contentLengthThread.contentLength;
if (size == -1) {
// Terminate if we did not get the content length
notifyFinish();
return;
}
// Get an input stream from the connection
in = con.getInputStream();
// Prepare the output stream for the file output
out = new FileOutputStream(filename);
// Download the file
final byte[] buf = new byte[4096];
int totalRead = 0;
int bytesRead;
// Start thread for reading bytes into the buffer
while (totalRead < size) {
// Start reading bytes into the buffer
final ReadInputStreamToBufferThread readThread = new ReadInputStreamToBufferThread(in, buf);
readThread.start();
// Wait for the reading to finish
synchronized (readThread.monitor) {
while (!readThread.isFinished) {
try {
readThread.monitor.wait(CONNECTION_TIMEOUT);
readThread.interrupt();
} catch (InterruptedException e) {
notifyFinish();
return;
}
}
}
bytesRead = readThread.bytesRead;
if (bytesRead == -1) {
// Read completed has completed
notifyFinish();
break;
}
// Write the byte buffer to the output
out.write(buf, 0, bytesRead);
totalRead += bytesRead;
}
// If we reached this point, the download was succesful
status = DownloadStatus.OK;
notifyFinish();
} catch (final IOException e) {
status = DownloadStatus.COULD_NOT_CONNECT;
} finally {
// Make sure the input stream is closed
if (in != null) {
try {
in.close();
} catch (final IOException e) {
e.printStackTrace();
}
}
// Make sure the output stream is closed
if (out != null) {
try {
out.close();
} catch (final IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* Worker thread used for getting the response code of an already open HTTP
* connection.
*
* @author Flemming N. Larsen
*/
final static class GetResponseCodeThread extends WorkerThread {
// The response code to read out
int responseCode;
final HttpURLConnection con;
GetResponseCodeThread(HttpURLConnection con) {
super("FileTransfer: Get response code");
this.con = con;
}
@Override
public void run() {
try {
// Get the response code
responseCode = con.getResponseCode();
} catch (final Exception e) {
responseCode = -1;
}
// Notify that this thread is finish
notifyFinish();
}
}
/**
* Worker thread used for getting the content length of an already open HTTP
* connection.
*
* @author Flemming N. Larsen
*/
final static class GetContentLengthThread extends WorkerThread {
// The content length to read out
int contentLength;
final HttpURLConnection con;
GetContentLengthThread(HttpURLConnection con) {
super("FileTransfer: Get content length");
this.con = con;
}
@Override
public void run() {
try {
// Get the content length
contentLength = con.getContentLength();
} catch (final Exception e) {
contentLength = -1;
}
// Notify that this thread is finish
notifyFinish();
}
}
/**
* Worker thread used for reading bytes from an already open input stream
* into a byte buffer.
*
* @author Flemming N. Larsen
*/
final static class ReadInputStreamToBufferThread extends WorkerThread {
int bytesRead;
final InputStream in;
final byte[] buf;
ReadInputStreamToBufferThread(InputStream in, byte[] buf) {
super("FileTransfer: Read input stream to buffer");
this.in = in;
this.buf = buf;
}
@Override
public void run() {
try {
// Read bytes into the buffer
bytesRead = in.read(buf);
} catch (final Exception e) {
bytesRead = -1;
}
// Notify that this thread is finish
notifyFinish();
}
}
/**
* Copies a file into another file.
*
* @param src_file the filename of the source file to copy
* @param dest_file the filename of the destination file to copy the file
* into
* @return true if the file was copied; false otherwise
*/
public static boolean copy(String src_file, String dest_file) {
FileInputStream in = null;
FileOutputStream out = null;
try {
if (src_file.equals(dest_file)) {
throw new IOException("You cannot copy a file onto itself");
}
final byte[] buf = new byte[4096];
in = new FileInputStream(src_file);
out = new FileOutputStream(dest_file);
while (in.available() > 0) {
out.write(buf, 0, in.read(buf, 0, buf.length));
}
} catch (final IOException e) {
return false;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
}