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.model.download.action.DownloadManagerAction; import de.digiway.rapidbreeze.server.model.download.action.DownloadManagerActionExecutor; import de.digiway.rapidbreeze.server.config.ServerConfiguration; import de.digiway.rapidbreeze.server.model.storage.StorageProvider; import de.digiway.rapidbreeze.server.model.storage.StorageProviderRepository; import de.digiway.rapidbreeze.server.model.storage.FileStatus; import de.digiway.rapidbreeze.server.model.storage.StorageProviderDownloadClient; import de.digiway.rapidbreeze.server.model.storage.UrlStatus; import de.digiway.rapidbreeze.shared.rest.download.DownloadStatus; import static de.digiway.rapidbreeze.shared.rest.download.DownloadStatus.PAUSE; import static de.digiway.rapidbreeze.shared.rest.download.DownloadStatus.RUNNING; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang3.Validate; /** * An instance of the the {@linkplain DownloadManager} handles * {@linkplain Download} instances. To avoid threading issues and to accomplish * a synchronous execution of actions related to the * {@linkplain DownloadManager}, all operations are queued up and executed in * the order of arrival. * * @author Sigurd Randoll <srandoll@digiway.de> */ public class DownloadManager { private Map<String, Download> downloadCacheIdentifiers; private List<Download> downloadCache; private DownloadRepository downloadRepository; private StorageProviderRepository storageProviderRepository; private Path downloadTargetFolder; private ServerConfiguration applicationConfiguration; private DownloadListenerHandler listenerHandler; private List<DownloadManagerListener> listeners; private ThrottleSupport throttleSupport; private DownloadManagerActionExecutor actionExecutor; private Executor threadExecutor = Executors.newSingleThreadExecutor(); private static final Logger LOG = Logger.getLogger(DownloadManager.class.getName()); public DownloadManager(DownloadRepository downloadRepository, StorageProviderRepository storageProviderRepository, ServerConfiguration applicationConfiguration) { this.downloadRepository = downloadRepository; this.storageProviderRepository = storageProviderRepository; this.applicationConfiguration = applicationConfiguration; this.downloadTargetFolder = applicationConfiguration.getDownloadTargetFolder(); this.listeners = new CopyOnWriteArrayList<>(); this.downloadCacheIdentifiers = new HashMap<>(); this.downloadCache = new ArrayList<>(); this.listenerHandler = new DownloadListenerHandler(); this.actionExecutor = new DownloadManagerActionExecutor(); this.actionExecutor.start(); this.throttleSupport = new ThrottleSupport(this); for (Download download : downloadRepository.getDownloads()) { downloadCacheIdentifiers.put(download.getIdentifier(), download); downloadCache.add(download); download.addListener(listenerHandler); } checkTargetFolder(); threadExecutor.execute(new DownloadHandlingThread()); } /** * Throttles the overall download speed to the given bytes per second. * * @param bytesPerSecond */ public void setThrottle(long bytesPerSecond) { executeAtomic(new SetThrottleAction(bytesPerSecond)); } /** * Checks if this {@linkplain DownloadManager} can handle the given * {@linkplain URL}. * * @param url * @return true if yes */ public boolean canHandleUrl(URL url) { return storageProviderRepository.getStorageProvider(url) != null; } /** * Returns an unmodifiable list of all available {@linkplain Download} * instances. * * @return */ public List<Download> getDownloads() { return executeAtomic(new GetDownloadsAction()); } /** * Checks if the {@linkplain DownloadManager} instance contains an * {@linkplain Download} with the given identifier. * * @param identifier * @return */ public boolean hasDownload(String identifier) { Validate.notEmpty(identifier); return executeAtomic(new HasDownloadAction(identifier)); } /** * Returns the {@linkplain Download} of the given identifier. * * @param identifier * @return * @throws IllegalArgumentException if the {@linkplain DownloadManager} does * not contain a {@linkplain Download} with the given identifier. */ public Download getDownload(String identifier) { Validate.notEmpty(identifier); return executeAtomic(new GetDownloadAction(identifier)); } /** * Creates a new {@linkplain Download} which will handle the given * {@linkplain URL}. * * @param url * @return */ public Download addUrl(URL url) { Validate.notNull(url); return executeAtomic(new AddUrlAction(url)); } /** * Removes the given {@linkplain Download} instance. If the download is * currently active, it will be stopped (wait state) and afterwards removed. * * @param download */ public void removeDownload(Download download) { Validate.notNull(download); executeAtomic(new RemoveDownloadAction(download)); } /** * Starts the download. This will trigger the start of a download * asynchronous. The call will returned immediatly. A download can be * started and resumed after pausing. * * @throws IllegalStateException if a download is already active. */ public void startDownload(Download download) { Validate.notNull(download); executeAtomic(new StartDownloadAction(download)); } /** * Pauses the given {@linkplain Download}. * * @param download */ public void pauseDownload(Download download) { Validate.notNull(download); executeAtomic(new PauseDownloadAction(download)); } /** * Tries to set the given {@linkplain Download} into wait state. Wait state * is different do pause in so far that any download in wait state will * continue soon (maybe automatically). * * @param download */ public void waitDownload(Download download) { Validate.notNull(download); executeAtomic(new WaitDownloadAction(download)); } /** * Adds a new {@linkplain DownloadManagerListener} to this * {@linkplain DownloadManager}. It will be informed about new * {@linkplain Download} instances added to the manager and about status * changes of all {@linkplain Download} instances. * * @param listener */ public void addListener(DownloadManagerListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } /** * Removes the listener again. * * @param listener */ public void removeListener(DownloadManagerListener listener) { listeners.remove(listener); } /** * Executes the given {@linkplain DownloadManagerAction} synchronously as an * atomic operation. * * @param <V> * @param action * @return result of the action */ public <V> V executeAtomic(DownloadManagerAction<V> action) { return handleFutureException(actionExecutor.executeAction(action)); } private void checkTargetFolder() { if (!Files.exists(downloadTargetFolder)) { try { Files.createDirectory(downloadTargetFolder); } catch (IOException ex) { throw new IllegalStateException("Cannot create download directory: " + downloadTargetFolder, ex); } } } private <T> T handleFutureException(Future<T> future) { try { return future.get(); } catch (InterruptedException ex) { throw new IllegalStateException(DownloadManager.class.getSimpleName() + " action was interrupted.", ex); } catch (ExecutionException ex) { throw new IllegalStateException( "Exception during execution of " + DownloadManager.class.getSimpleName() + " action.", ex.getCause()); } } private void fireAddEvent(Download download) { for (DownloadManagerListener listener : listeners) { listener.onDownloadAdded(download); } } private void fireRemoveEvent(Download download) { for (DownloadManagerListener listener : listeners) { listener.onDownloadRemoved(download); } } private class DownloadListenerHandler implements DownloadListener { @Override public void onDownloadStatusChange(DownloadEvent event) { handleFutureException(actionExecutor.executeAction(new OnDownloadStatusChange(event))); } } private class OnDownloadStatusChange implements DownloadManagerAction { private DownloadEvent event; public OnDownloadStatusChange(DownloadEvent event) { this.event = event; } @Override public Void call() throws Exception { try { if (DownloadStatus.FINISHED_SUCCESSFUL.equals(event.getNewStatus())) { Download downloadSource = (Download) event.getSource(); downloadRepository.update(downloadSource); } } finally { // Always refire event: for (DownloadManagerListener listener : listeners) { listener.onDownloadStatusChange(event); } } return null; } } private class SetThrottleAction implements DownloadManagerAction { private long speed; public SetThrottleAction(long speed) { this.speed = speed; } @Override public Void call() throws Exception { throttleSupport.setThrottleBytesPerSecond(speed); return null; } } private class GetDownloadsAction implements DownloadManagerAction<List<Download>> { @Override public List<Download> call() throws Exception { return Collections.unmodifiableList(downloadCache); } } private class HasDownloadAction implements DownloadManagerAction<Boolean> { private String identifier; public HasDownloadAction(String identifier) { this.identifier = identifier; } @Override public Boolean call() throws Exception { return downloadCacheIdentifiers.containsKey(identifier); } } private class GetDownloadAction implements DownloadManagerAction<Download> { private String identifier; public GetDownloadAction(String identifier) { this.identifier = identifier; } @Override public Download call() throws Exception { if (!hasDownload(identifier)) { throw new IllegalArgumentException( "Cannot find " + Download.class.getSimpleName() + " with identifier:" + identifier); } return downloadCacheIdentifiers.get(identifier); } } private class AddUrlAction implements DownloadManagerAction<Download> { private URL url; public AddUrlAction(URL url) { this.url = url; } @Override public Download call() throws Exception { StorageProvider storageProvider = storageProviderRepository.getStorageProvider(url); if (storageProvider == null) { throw new IllegalArgumentException("The given URL " + url + " cannot be handled. There is no " + StorageProvider.class.getSimpleName() + " registered."); } StorageProviderDownloadClient downloadClient = storageProvider.createDownloadClient(); UrlStatus urlStatus = downloadClient.getUrlStatus(url); String filename = url.getPath(); if (urlStatus.getFileStatus().equals(FileStatus.OK)) { filename = urlStatus.getFilename(); } Path target = Paths.get(downloadTargetFolder.toString(), filename); Download download = new Download(url, target, storageProvider); downloadRepository.add(download); downloadCacheIdentifiers.put(download.getIdentifier(), download); downloadCache.add(download); download.addListener(listenerHandler); fireAddEvent(download); return download; } } private class RemoveDownloadAction implements DownloadManagerAction<Void> { private Download download; public RemoveDownloadAction(Download download) { this.download = download; } @Override public Void call() throws Exception { downloadRepository.remove(download); downloadCacheIdentifiers.remove(download.getIdentifier()); downloadCache.remove(download); download.removeListener(listenerHandler); fireRemoveEvent(download); // Get status again. Event listeners might have modified status: switch (download.getDownloadStatus()) { case PAUSE: case RUNNING: download.waitState(); download.removeTempFile(); } return null; } } private static class StartDownloadAction implements DownloadManagerAction<Void> { private Download download; public StartDownloadAction(Download download) { this.download = download; } @Override public Void call() throws Exception { download.start(); return null; } } private static class PauseDownloadAction implements DownloadManagerAction<Void> { private Download download; public PauseDownloadAction(Download download) { this.download = download; } @Override public Void call() throws Exception { download.pause(); return null; } } private static class WaitDownloadAction implements DownloadManagerAction<Void> { private Download download; public WaitDownloadAction(Download download) { this.download = download; } @Override public Void call() throws Exception { download.waitState(); return null; } } private class DownloadHandlingThread implements Runnable { private boolean running = true; @Override public void run() { while (running) { try { Integer idle = executeAtomic(new DownloadHandlingAction()); Thread.sleep(idle < 0 ? 0 : idle); } catch (RuntimeException ex) { LOG.log(Level.SEVERE, "Exception in " + Download.class.getSimpleName() + " handling class.", ex); } catch (InterruptedException ex) { LOG.log(Level.SEVERE, Download.class.getSimpleName() + " interrupted.", ex); running = false; } } } } private class DownloadHandlingAction implements DownloadManagerAction<Integer> { @Override public Integer call() throws Exception { int idle = 500; for (Download download : getDownloads()) { Integer nextIdle = download.handle(); if (nextIdle < idle) { idle = nextIdle; } } return idle; } } }