/**
* Copyright 2012-2013 Maciej Jaworski, Mariusz Kapcia, Paweł Kędzia, Mateusz Kubuszok
*
* <p>Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at</p>
*
* <p>http://www.apache.org/licenses/LICENSE-2.0</p>
*
* <p>Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.</p>
*/
package com.autoupdater.client.download.runnables;
import static com.autoupdater.client.download.ConnectionConfiguration.*;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.interrupted;
import static net.jsdpu.logger.Logger.getLogger;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import net.jsdpu.logger.Logger;
import com.autoupdater.client.AutoUpdaterClientException;
import com.autoupdater.client.download.DownloadResultException;
import com.autoupdater.client.download.DownloadServiceMessage;
import com.autoupdater.client.download.DownloadServiceProgressMessage;
import com.autoupdater.client.download.EDownloadStatus;
import com.autoupdater.client.download.runnables.post.download.strategies.IPostDownloadStrategy;
import com.autoupdater.client.utils.executions.RunnableWithErrors;
import com.autoupdater.client.utils.services.ObservableService;
/**
* Superclass of all DownloadRunnables.
*
* <p>
* It connects to server, download data and parse it with DownloadStrategy.
* </p>
*
* <p>
* Because it's extends ObservableService, it can have Observer notified about
* changes of current download state. One of those Observers is always
* DownloadService.
* </p>
*
* @see com.autoupdater.client.download.runnables.PackagesInfoDownloadRunnable
* @see com.autoupdater.client.download.runnables.UpdateInfoDownloadRunnable
* @see com.autoupdater.client.download.runnables.ChangelogInfoDownloadRunnable
* @see com.autoupdater.client.download.runnables.FileDownloadRunnable
* @see com.autoupdater.client.download.services.AbstractDownloadService
* @see com.autoupdater.client.download.runnables.post.download.strategies.IPostDownloadStrategy
*
* @param <Result>
* type of returned result
*/
public abstract class AbstractDownloadRunnable<Result> extends
ObservableService<DownloadServiceMessage> implements RunnableWithErrors {
private static final Logger logger = getLogger(AbstractDownloadRunnable.class);
private final HttpURLConnection httpURLConnection;
private final String fileDestinationPath;
private EDownloadStatus state;
private IPostDownloadStrategy<Result> downloadStrategy;
private long contentLength = -1;
protected Result result;
private Throwable thrownException = null;
/**
* Creates AbstractDownloadRunnable instance.
*
* @param connection
* connection used for obtaining data
*/
AbstractDownloadRunnable(HttpURLConnection connection) {
this(connection, null);
}
/**
* Creates AbstractDownloadRunnable instance.
*
* @param connection
* connection used for obtaining data
* @param fileDestinationPath
* path to file where result should be stored
*/
AbstractDownloadRunnable(HttpURLConnection connection, String fileDestinationPath) {
this.httpURLConnection = connection;
this.fileDestinationPath = fileDestinationPath;
result = null;
state = EDownloadStatus.HASNT_STARTED;
}
/**
* Starts download process.
*/
@Override
public void run() {
try {
connectToServer();
downloadContent();
processDownload();
} catch (IOException | AutoUpdaterClientException e) {
setThrownException(e);
reportError(e.getMessage());
} catch (InterruptedException e) {
setThrownException(e);
reportCancelled();
}
}
/**
* Returns content length.
*
* @return content length if available, -1 otherwise
*/
public long getContentLength() {
return contentLength;
}
/**
* Returns file destination path.
*
* @return file destination path if available, null otherwise
*/
public String getFileDestinationPath() {
return fileDestinationPath;
}
/**
* Returns result of download.
*
* @return result of download
* @throws DownloadResultException
* thrown if download failed, was cancelled or isn't finished
* yet
*/
public Result getResult() throws DownloadResultException {
switch (state) {
case HASNT_STARTED:
throw new DownloadResultException("Cannot obtain results - download hasn't started");
case CONNECTED:
throw new DownloadResultException(
"Cannot obtain results - connection to the server was just made");
case IN_PROCESS:
throw new DownloadResultException("Cannot obtain results - download is in process");
case COMPLETED:
throw new DownloadResultException(
"Cannot obtain results - download is completed but hasn't been processed yet");
case FAILED:
throw new DownloadResultException("Cannot obtain results - download resulted in error");
case CANCELLED:
throw new DownloadResultException("Cannot obtain results - download cancelled");
case PROCESSED:
return result;
default:
throw new DownloadResultException("Cannot obtain results - uknown download state");
}
}
/**
* Return current state of download.
*
* @return current download state
*/
public EDownloadStatus getStatus() {
return state;
}
@Override
public Throwable getThrownException() {
return thrownException;
}
@Override
public void setThrownException(Throwable throwable) {
thrownException = throwable;
}
@Override
public void throwExceptionIfErrorOccured() throws Throwable {
if (thrownException != null)
throw thrownException;
}
/**
* Returns DownloadStrategy used for processing result into expected format.
*
* @see com.autoupdater.client.download.runnables.post.download.strategies
*
* @return DownloadStorageStrategyInterface instance
* @throws IOException
* thrown if IO error occurs (used in some implementation)
*/
abstract protected IPostDownloadStrategy<Result> getPostDownloadStrategy() throws IOException;
/**
* Procedure connection to server.
*
* @throws IOException
* thrown if connection error occurs
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
void connectToServer() throws IOException, InterruptedException {
reportChange("Initialized process of obtaining packages' data from server: "
+ httpURLConnection.getURL().getHost() + httpURLConnection.getURL().getPath());
httpURLConnection.setConnectTimeout(CONNECTION_TIME_OUT);
httpURLConnection.setReadTimeout(CONNECTION_TIME_OUT);
httpURLConnection.connect();
if (httpURLConnection.getResponseCode() / 100 != 2)
throw new IOException("Server returned response code "
+ httpURLConnection.getResponseCode());
contentLength = httpURLConnection.getContentLengthLong();
reportChange("Connected to the server", EDownloadStatus.CONNECTED);
}
/**
* Procedure downloading content from server.
*
* <p>
* Has to be called after connectToServer().
* </p>
*
* @throws IOException
* thrown if connection error occurs
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
void downloadContent() throws IOException, InterruptedException {
if (downloadStrategy == null)
downloadStrategy = getPostDownloadStrategy();
InputStream in = httpURLConnection.getInputStream();
byte[] buffer = new byte[MAX_BUFFER_SIZE];
long downloadStartTime = currentTimeMillis();
reportChange(EDownloadStatus.IN_PROCESS.getMessage(), EDownloadStatus.IN_PROCESS);
int bytesRead = 0;
long bytesWritten = 0;
while ((bytesRead = in.read(buffer)) != -1) {
checkInterruption();
downloadStrategy.write(buffer, bytesRead);
bytesWritten += bytesRead;
long elapsedTime = currentTimeMillis() - downloadStartTime;
if (elapsedTime > 500)
reportProgress(elapsedTime, bytesWritten);
}
in.close();
reportChange(EDownloadStatus.COMPLETED.getMessage(), EDownloadStatus.COMPLETED);
}
/**
* Procedure processing download using DownloadStrategy.
*
* <p>
* Has to be called after downloadContent().
* </p>
*
* @throws AutoUpdaterClientException
* thrown if ParserException or DownloadResultException occurred
* @throws IOException
* thrown if IO error occurs (used in come implementations of
* DownloadStrategy)
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
void processDownload() throws AutoUpdaterClientException, IOException, InterruptedException {
if (downloadStrategy == null)
downloadStrategy = getPostDownloadStrategy();
result = downloadStrategy.processDownload();
reportChange(EDownloadStatus.PROCESSED.getMessage(), EDownloadStatus.PROCESSED);
}
/**
* Sent message that download was cancelled.
*/
protected void reportCancelled() {
state = EDownloadStatus.CANCELLED;
hasChanged();
notifyObservers(new DownloadServiceMessage(this, EDownloadStatus.CANCELLED.getMessage()));
logger.info("Cancelled download from " + httpURLConnection.getURL());
}
/**
* Send message about download current state.
*
* @param message
* message about change
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
private void reportChange(String message) throws InterruptedException {
checkInterruption();
hasChanged();
notifyObservers(new DownloadServiceMessage(this, message));
}
/**
* Send message about download current state.
*
* @param message
* message about change
* @param state
* new download state
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
protected void reportChange(String message, EDownloadStatus state) throws InterruptedException {
checkInterruption();
this.state = state;
hasChanged();
notifyObservers(new DownloadServiceMessage(this, message));
}
/**
* Send message that download failed.
*
* @param message
* error message
*/
private void reportError(String message) {
state = EDownloadStatus.FAILED;
hasChanged();
notifyObservers(new DownloadServiceMessage(this, message, true));
logger.error("Failed download from " + httpURLConnection.getURL() + ": " + message);
}
/**
* Send message about download progress.
*
* @param elapsedTime
* elapsed time
* @param bytesRead
* bytes already read
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
private void reportProgress(long elapsedTime, long bytesRead) throws InterruptedException {
checkInterruption();
hasChanged();
notifyObservers(new DownloadServiceProgressMessage(this, bytesRead, contentLength,
elapsedTime, bytesRead));
}
/**
* Check whether thread was interrupted (cancelled). If so throws
* InterruptedException.
*
* @throws InterruptedException
* thrown if thread was interrupted (cancelled)
*/
private synchronized void checkInterruption() throws InterruptedException {
if (interrupted())
throw new InterruptedException("Download cancelled");
}
}