Java tutorial
/* * Copyright 2013 Sigurd Randoll <srandoll@digiway.de>. * * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * 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. */ package de.digiway.rapidbreeze.server.model.download; import de.digiway.rapidbreeze.server.infrastructure.objectstorage.Column; import de.digiway.rapidbreeze.server.infrastructure.objectstorage.Entity; import de.digiway.rapidbreeze.server.infrastructure.objectstorage.Id; import de.digiway.rapidbreeze.shared.rest.download.DownloadStatus; import de.digiway.rapidbreeze.server.model.storage.StorageProvider; import de.digiway.rapidbreeze.server.model.storage.StorageProviderDownloadClient; import de.digiway.rapidbreeze.server.model.storage.UrlStatus; import java.io.IOException; import java.io.Serializable; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.Date; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; /** * An entity class which represents a {@linkplain Download} from a specific * {@linkplain StorageProvider}. A {@linkplain Download} instance can be * started, paused and resumed. This class is not thread safe. * * @author Sigurd Randoll <srandoll@digiway.de> */ @Entity(table = "Download") public class Download implements Serializable { public static final String PROP_CREATED = "created"; private static final long serialVersionUID = 1L; @Id private String identifier; @Column private Path targetFile; @Column private Path tempFile; @Column private URL url; @Column private StorageProvider storageProvider; @Column private Boolean done; @Column private Date created; private transient Long throttleMaxBytesPerSecond = 0L; private transient ReadableByteChannel sourceChannel = null; private transient FileChannel targetChannel = null; private transient ThrottledInputStream throttledInputStream; private transient DownloadStatusHandler statusHandler = new DownloadStatusHandler(this); private transient UrlStatus cachedUrlStatus = null; private static transient final Logger LOG = Logger.getLogger(Download.class.getName()); public static final long BLOCK_SIZE = 4096; private static final int DEFAULT_IDLE = 100; protected Download() { // Required by persisting framework } /** * Creates a new instance to handle a download from the given URL to the * given target file with the provided {@linkplain StorageProvider}. * * @param url * @param targetFile * @param storageProvider */ Download(URL url, Path targetFile, StorageProvider storageProvider) { Validate.notNull(url, "Url is a mandatory parameter."); Validate.notNull(targetFile, "TargetFile is a mandatory parameter."); Validate.notNull(storageProvider, "StorageProvider is a mandatory parameter."); this.url = url; this.targetFile = targetFile; this.storageProvider = storageProvider; this.tempFile = Paths.get(targetFile.toString() + ".tmp"); this.identifier = UUID.randomUUID().toString(); // Required here for hashCode and equals this.done = false; this.created = new Date(); } /** * Returns the unique identifier of this download. * * @return non-null string */ public String getIdentifier() { return identifier; } /** * Returns the {@linkplain Date} when this download was created. * * @return */ public Date getCreated() { return created; } /** * Adds a listener to this {@linkplain Download} instance. The listener will * be informed about changes in the {@linkplain Download}. Pay attention: * The listener might be fired from another thread. * * @param listener */ public void addListener(DownloadListener listener) { statusHandler.addListener(listener); } /** * Removes the given listener * * @param listener */ public void removeListener(DownloadListener listener) { statusHandler.removeListener(listener); } /** * This call fetches the current {@linkplain UrlStatus} of this * {@linkplain Download}. For each running {@linkplain Download} the result * of this call is cached. * * @return */ public UrlStatus getUrlStatus() { if (cachedUrlStatus == null) { cachedUrlStatus = getDownloadClient().getUrlStatus(url); } return cachedUrlStatus; } /** * Executes all pending actions for this {@linkplain Download} instance. * * @return the time in ms when the next call to handle shoule be executed. */ int handle() { try { switch (statusHandler.getCurrentStatus()) { case RUNNING: return handleRunning(); } } catch (IOException | RuntimeException ex) { LOG.log(Level.SEVERE, "An exception occured during handling of the " + Download.class.getSimpleName() + ": " + this, ex); closeChannels(); cachedUrlStatus = null; statusHandler.newException(ex); } return DEFAULT_IDLE; } private int handleRunning() throws IOException { long position = targetChannel.position(); long transferred = targetChannel.transferFrom(sourceChannel, position, BLOCK_SIZE); targetChannel.position(position + transferred); if (targetChannel.size() == getUrlStatus().getFileSize()) { closeChannels(); Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); done = true; statusHandler.newStatus(DownloadStatus.FINISHED_SUCCESSFUL); } return throttledInputStream.nextTransfer(BLOCK_SIZE); } /** * Starts this {@linkplain Download}. * */ void start() { switch (statusHandler.getCurrentStatus()) { case RUNNING: return; case PAUSE: statusHandler.newStatus(DownloadStatus.RUNNING); return; } try { long startAt = 0; if (Files.exists(tempFile)) { try { startAt = Files.size(tempFile); } catch (IOException ex) { // File might be removed in the meantime startAt = 0; } } StorageProviderDownloadClient storageDownload = getDownloadClient(); throttledInputStream = new ThrottledInputStream(storageDownload.start(url, startAt)); throttledInputStream.setThrottle(throttleMaxBytesPerSecond); sourceChannel = Channels.newChannel(throttledInputStream); targetChannel = FileChannel.open(tempFile, StandardOpenOption.WRITE, StandardOpenOption.APPEND, StandardOpenOption.CREATE); targetChannel.position(startAt); } catch (IOException | RuntimeException ex) { LOG.log(Level.SEVERE, "An exception occured during data transfer setup for " + Download.class.getSimpleName() + ":" + this, ex); closeChannels(); cachedUrlStatus = null; statusHandler.newException(ex); return; } done = false; statusHandler.newStatus(DownloadStatus.RUNNING); } private void closeChannels() { try { IOUtils.closeQuietly(sourceChannel); IOUtils.closeQuietly(targetChannel); } catch (RuntimeException re) { // Might occur if closeQuietly throws NP } } /** * Pauses the {@linkplain Download}. */ void pause() { statusHandler.newStatus(DownloadStatus.PAUSE); } void waitState() { closeChannels(); done = false; statusHandler.newStatus(DownloadStatus.WAITING); } private StorageProviderDownloadClient getDownloadClient() { StorageProviderDownloadClient storageDownload = storageProvider.createDownloadClient(); return storageDownload; } /** * Removes the temporary file where the download is streamed to. The * download must be waiting or error. */ public void removeTempFile() { if (!getDownloadStatus().equals(DownloadStatus.WAITING) && !getDownloadStatus().equals(DownloadStatus.ERROR)) { throw new IllegalStateException("Cannot remove temporary file. " + Download.class.getSimpleName() + " must be in state " + DownloadStatus.WAITING + " or " + DownloadStatus.ERROR); } try { Files.deleteIfExists(tempFile); } catch (IOException ex) { LOG.log(Level.WARNING, "Error removing temporary download file.", ex); } } /** * Returns the estimated time in seconds for this download. * * @return */ public long getEta() { double bytesPerSecond = getBytesPerSecond(); Long fileSize = getUrlStatus().getFileSize(); long currentSize = getCurrentSize(); if (bytesPerSecond > 0 && fileSize != null) { return (long) ((fileSize - currentSize) / bytesPerSecond); } return 0; } /** * Retrieves the current speed of the downloads. * * @return */ public double getBytesPerSecond() { if (throttledInputStream != null) { return throttledInputStream.getCurrentBytesPerSecond(); } return 0; } /** * Returns the current {@linkplain DownloadStatus} of the download. * * @return {@linkplain DownloadStatus} instance */ public DownloadStatus getDownloadStatus() { if (done) { return DownloadStatus.FINISHED_SUCCESSFUL; } return statusHandler.getCurrentStatus(); } /** * Returns the last exception which occured for this {@linkplain Download}. * This might (or should) be null. * * @return last exception or null */ public Exception getError() { return statusHandler.getErrorException(); } /** * Returns the target {@linkplain Path} where the file will be stored on the * local machine. * * @return path object */ public Path getFile() { return targetFile; } /** * Returns the current progress of the download. This is a percentage number * between 0 and 1. * * @return */ public Double getProgress() { if (done) { return 1D; } Long totalFilesize = getUrlStatus().getFileSize(); if (totalFilesize == null || totalFilesize == 0D) { return 0D; } long currentSize = getCurrentSize(); return ((double) currentSize / (double) totalFilesize); } /** * Throttles the download to the given maximum bytes per second. * * @param maxBytesPerSecond */ public void setThrottle(long maxBytesPerSecond) { this.throttleMaxBytesPerSecond = maxBytesPerSecond; if (throttledInputStream != null) { throttledInputStream.setThrottle(maxBytesPerSecond); } } /** * Returns the currently downloaded size of the file in bytes. * * @return */ public long getCurrentSize() { try { if (!Files.exists(tempFile)) { if (Files.exists(targetFile)) { return Files.size(targetFile); } return 0; } return Files.size(tempFile); } catch (IOException ex) { throw new IllegalStateException("Cannot retrieve size of temporary file:" + tempFile, ex); } } /** * Returns the filename of the file to download. This will not include any * paths. * * @return */ public String getFilename() { return targetFile.getFileName().toString(); } /** * Returns the provider which is used to download the file. * * @return */ public String getProviderName() { return storageProvider.getName(); } /** * Returns the {@linkplain URL} where the file is downloaded from. * * @return */ public URL getUrl() { return url; } @Override public int hashCode() { int hash = 7; hash = 23 * hash + (this.identifier != null ? this.identifier.hashCode() : 0); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Download other = (Download) obj; if ((this.identifier == null) ? (other.identifier != null) : !this.identifier.equals(other.identifier)) { return false; } return true; } @Override public String toString() { return "Download{identifier=" + identifier + ", url=" + url + ", state=" + getDownloadStatus() + ", created=" + created + '}'; } }