/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.ui.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.osgi.util.NLS;
import org.xmind.ui.internal.ToolkitPlugin;
import org.xmind.ui.io.IDownloadTarget.IDownloadTarget2;
/**
* A <code>DownloadJob</code> performs data downloading from a URL to a target
* location.
*
* @author Frank Shaka
*
*/
public class DownloadJob extends Job {
private String pluginId;
private String sourceURL;
private IDownloadTarget target;
private URLConnection connection = null;
public DownloadJob(String jobName, String sourceURL, String targetPath) {
this(jobName, sourceURL, new FileDownloadTarget(targetPath, true),
ToolkitPlugin.PLUGIN_ID);
}
public DownloadJob(String jobName, String sourceURL, String targetPath,
String pluginId) {
this(jobName, sourceURL, new FileDownloadTarget(targetPath, true),
pluginId);
}
public DownloadJob(String jobName, String sourceURL,
IDownloadTarget target) {
this(jobName, sourceURL, target, ToolkitPlugin.PLUGIN_ID);
}
public DownloadJob(String jobName, String sourceURL, IDownloadTarget target,
String pluginId) {
super(jobName);
Assert.isNotNull(sourceURL);
Assert.isNotNull(target);
this.sourceURL = sourceURL;
this.target = target;
this.pluginId = pluginId;
}
public String getPluginId() {
return pluginId;
}
public String getSourceURL() {
return sourceURL;
}
public String getTargetPath() {
return target.getPath();
}
/**
* Sets up the URL connection. This method is called after the connection is
* created, and before it is actually connected.
* <p>
* Clients may extend this method to add options (e.g. timeout) or HTTP
* headers to this connection. The default implementation does nothing so
* that all default values are used.
*
* @param connection
* the connection created from the source URL
*/
protected void setupConnection(URLConnection connection) {
// To be extended.
}
/**
* Validates the connected URL connection object. This method is called
* after the connection is connected, and before the data transfer starts.
* <p>
* Clients may extend this method to check for response code or other
* validating factors of this connection. The default implementation does
* nothing and simply returns <code>null</code> to accept all connections.
*
* @param connection
* the connection object connected to the source URL
* @return a non-<code>null</code> status to reject this connection from
* being downloaded, or <code>null</code> to accept it
*/
protected IStatus validateConnection(URLConnection connection) {
return null;
}
/**
* Executes this download job.
*
* <p>
* This implementation delegates the actual download job to
* {@link #runSafely(IProgressMonitor)} and interprets its exceptions to
* error status.
*
* @param monitor
* the monitor to be used for reporting progress and responding
* to cancelation. The monitor is never <code>null</code>
* @return resulting status of the run. The result must not be
* <code>null</code>
*/
protected IStatus run(IProgressMonitor monitor) {
IStatus status;
try {
status = runSafely(monitor);
} catch (Throwable e) {
if (e instanceof InterruptedIOException) {
status = cancelStatus();
} else {
status = errorStatus(e);
}
}
if (target instanceof IDownloadTarget2) {
try {
((IDownloadTarget2) target).afterDownload(status);
} catch (Throwable ignore) {
}
}
return status;
}
/**
* Executes the download job in a safe context. It's safe to throw
* exceptions in this method to interrupt the download process.
*
* @param monitor
* the progress monitor
* @return resulting status. Must not be <code>null</code>
* @throws Exception
* any type of exception
*/
private IStatus runSafely(IProgressMonitor monitor) throws Exception {
monitor.beginTask(null, 100);
monitor.subTask(NLS.bind(Messages.ConnectingSource, getSourceURL()));
URL url = new URL(sourceURL);
URLConnection connection = url.openConnection();
setURLConnection(connection);
if (monitor.isCanceled())
return cancelStatus();
setupConnection(connection);
if (monitor.isCanceled())
return cancelStatus();
connection.connect();
if (monitor.isCanceled())
return cancelStatus();
IStatus consumed = validateConnection(connection);
if (consumed != null)
return consumed;
if (monitor.isCanceled())
return cancelStatus();
int length = connection.getContentLength();
if (monitor.isCanceled())
return cancelStatus();
InputStream sourceStream = connection.getInputStream();
try {
if (monitor.isCanceled())
return cancelStatus();
monitor.subTask(
NLS.bind(Messages.InitializingTarget, getTargetPath()));
OutputStream targetStream = target.openOutputStream();
if (monitor.isCanceled())
return cancelStatus();
try {
sourceStream = new MonitoredInputStream(sourceStream, monitor);
targetStream = new MonitoredOutputStream(targetStream, monitor);
monitor.subTask(Messages.TransferingData);
transfer(sourceStream, targetStream,
new SubProgressMonitor(monitor, 100), length);
setURLConnection(null);
monitor.done();
return new Status(IStatus.OK, pluginId,
NLS.bind(Messages.DownloadFinished, getSourceURL(),
getTargetPath()));
} finally {
try {
targetStream.close();
} catch (IOException ignore) {
}
}
} finally {
try {
sourceStream.close();
} catch (IOException ignore) {
}
}
}
/**
* Transfers all content from the source input stream to the target output
* stream.
*
* @param sourceStream
* the source input stream
* @param targetStream
* the target output stream
* @param monitor
* the progress monitor
* @param length
* the total length of data to read, or an integer value less
* than <code>0</code> indicating that the length is unknown
* @throws IOException
*/
private void transfer(InputStream sourceStream, OutputStream targetStream,
IProgressMonitor monitor, int length) throws IOException {
monitor.beginTask(null, length < 0 ? 100 : Math.max(1, length / 1024));
String total = length < 0 ? null : String.format("%.1fK", //$NON-NLS-1$
length / 1024.0d);
byte[] buffer = new byte[1024];
int downloaded = 0;
int num;
int worked = 0, newWorked = 0;
while ((num = sourceStream.read(buffer)) > 0) {
targetStream.write(buffer, 0, num);
downloaded += num;
String taskName = (total == null
? String.format("(%.1fK)", //$NON-NLS-1$
(downloaded / 1024.0))
: String.format("(%.1fK/%s)", //$NON-NLS-1$
(downloaded / 1024.0), total));
monitor.subTask(Messages.TransferingData + " " + taskName); //$NON-NLS-1$
if (length < 0) {
newWorked = Math.min(worked + 1, 99);
} else {
newWorked = downloaded / 1024;
}
if (newWorked > worked) {
monitor.worked(newWorked - worked);
worked = newWorked;
}
}
}
/**
* Creates a status indicating the job is canceled.
*
* @return a status with <code>CANCEL</code> severity
*/
protected Status cancelStatus() {
return new Status(IStatus.CANCEL, pluginId, NLS.bind(
Messages.DownloadCanceled, getSourceURL(), getTargetPath()));
}
/**
* Creates an error status that wraps the given exception.
*
* @param e
* the exception to wrap
* @return a status with <code>ERROR</code> severity that wraps the given
* exception
*/
protected IStatus errorStatus(Throwable e) {
return new Status(IStatus.ERROR, pluginId, NLS.bind(
Messages.DownloadFailed, getSourceURL(), getTargetPath()), e);
}
private void setURLConnection(URLConnection connection) {
this.connection = connection;
}
protected void canceling() {
super.canceling();
URLConnection currentConnection = this.connection;
if (currentConnection != null) {
// TODO destroy current URL connection
}
}
}