/* This file is part of RouteConverter. RouteConverter is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. RouteConverter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RouteConverter; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Copyright (C) 2007 Christian Pesch. All Rights Reserved. */ package slash.navigation.download; import slash.navigation.download.actions.Validator; import slash.navigation.download.executor.DownloadExecutor; import slash.navigation.download.executor.DownloadExecutorComparator; import slash.navigation.download.queue.QueuePersister; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.*; import java.util.logging.Logger; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.SECONDS; import static slash.navigation.download.Action.*; import static slash.navigation.download.State.*; /** * Manages {@link Download}s * * @author Christian Pesch */ public class DownloadManager { private static final Logger log = Logger.getLogger(DownloadManager.class.getName()); static final int WAIT_TIMEOUT = 600 * 1000; private static final int PARALLEL_DOWNLOAD_COUNT = 4; private final File queueFile; private final List<DownloadListener> downloadListeners = new CopyOnWriteArrayList<>(); private final DownloadTableModel model = new DownloadTableModel(); private final Map<Download,Future> downloadToFutures = new HashMap<>(); private final Map<Download,DownloadExecutor> downloadToExecutors = new HashMap<>(); private final ThreadPoolExecutor pool; public DownloadManager(File queueFile) { this.queueFile = queueFile; BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>(1, new DownloadExecutorComparator()); pool = new ThreadPoolExecutor(PARALLEL_DOWNLOAD_COUNT, PARALLEL_DOWNLOAD_COUNT * 2, 60, SECONDS, queue); pool.allowCoreThreadTimeOut(true); addDownloadListener(new DownloadListener() { public void initialized(Download download) { saveQueue(); } public void progressed(Download download) { } public void failed(Download download) { saveQueue(); } public void succeeded(Download download) { saveQueue(); } }); } public void loadQueue() { try { log.info(format("Loading download queue from '%s'", queueFile)); QueuePersister.Result result = new QueuePersister().load(queueFile); if (result == null) return; List<Download> downloads = result.getDownloads(); if (downloads != null) model.setDownloads(downloads); } catch (Exception e) { log.severe(format("Could not load download queue from '%s': %s", queueFile, e)); } restartDownloadsWithState(Running, Resuming, Downloading, Processing, Queued); } private void restartDownloadsWithState(State... states) { List<State> restartStates = asList(states); for (Download download : model.getDownloads()) { if (restartStates.contains(download.getState())) { log.info("Restarting download " + download + " from state " + download.getState()); startExecutor(download); } } } public void restartDownloads(List<Download> downloads) { for (Download download : downloads) { if(!COMPLETED.contains(download.getState())) continue; log.info("Restarting download " + download); startExecutor(download); } } public void stopDownloads(List<Download> downloads) { for (Download download : downloads) { if(COMPLETED.contains(download.getState())) continue; log.info("Stopping download " + download); Future future = downloadToFutures.get(download); if(future != null) future.cancel(true); DownloadExecutor executor = downloadToExecutors.get(download); if(executor != null) executor.stopped(); } pool.purge(); } public void saveQueue() { try { new QueuePersister().save(queueFile, model.getDownloads()); } catch (Exception e) { e.printStackTrace(); log.severe(format("Could not save %d download queue to '%s': %s", model.getRowCount(), queueFile, e)); } } public void clearQueue() { for (Download download : model.getDownloads()) model.removeDownload(download); } public void dispose() { pool.shutdownNow(); } public DownloadTableModel getModel() { return model; } public void addDownloadListener(DownloadListener listener) { downloadListeners.add(listener); } public void updateDownload(Download download) { model.updateDownload(download); } public void fireDownloadInitialized(Download download) { for (DownloadListener listener : downloadListeners) { listener.initialized(download); } } public void fireDownloadProgressed(Download download) { for (DownloadListener listener : downloadListeners) { listener.progressed(download); } } public void fireDownloadFailed(Download download) { for (DownloadListener listener : downloadListeners) { listener.failed(download); } } public void fireDownloadSucceeded(Download download) { for (DownloadListener listener : downloadListeners) { listener.succeeded(download); } } private void startExecutor(Download download) { DownloadExecutor executor = new DownloadExecutor(download, this); model.addOrUpdateDownload(download); Future<?> future = pool.submit(executor); downloadToFutures.put(download, future); downloadToExecutors.put(download, executor); fireDownloadInitialized(download); } public void finishedExecutor(DownloadExecutor executor) { Download download = executor.getDownload(); downloadToFutures.remove(download); downloadToExecutors.remove(download); } private static final Set<State> COMPLETED = new HashSet<>(asList(NotModified, Outdated, Succeeded, Stopped, NoFileError, ChecksumError, Failed)); Download queue(Download download, boolean startExecutor) { if (download.getFile().getFile() == null) throw new IllegalArgumentException("No file given for " + download); if (download.getAction().equals(Extract) || download.getAction().equals(Flatten)) { if (!download.getFile().getFile().isDirectory()) throw new IllegalArgumentException(format("Need a directory for extraction but got %s", download.getFile().getFile())); List<FileAndChecksum> fragments = download.getFragments(); if (fragments == null || fragments.size() == 0) throw new IllegalArgumentException("No fragments given for " + download); for (FileAndChecksum fragmentTarget : fragments) { if (fragmentTarget == null) throw new IllegalArgumentException("No fragment target given for " + download); } } Download queued = model.getDownload(download.getUrl()); if (queued != null) { // let a GET replace a HEAD if (queued.getAction().equals(Head) || queued.getAction().equals(GetRange)) model.removeDownload(queued); else { if (COMPLETED.contains(queued.getState()) && startExecutor) { log.info("Restarting completed download " + download); startExecutor(queued); } return queued; } } if(startExecutor) { log.info("Starting new download " + download); startExecutor(download); } else { log.info("Adding to queue " + download); model.addOrUpdateDownload(download); } return download; } public Download queueForDownload(String description, String url, Action action, FileAndChecksum file, List<FileAndChecksum> fragments) { return queue(new Download(description, url, action, file, fragments), true); } public Download addOrUpdateInQueue(String description, String url, Action action, FileAndChecksum file, List<FileAndChecksum> fragments) { Download queued = model.getDownload(url); if(queued != null) { queued.setAction(action); queued.setFile(file); queued.setFragments(fragments); model.updateDownload(queued); return queued; } else { Download download = new Download(description, url, action, file, fragments); download.setState(Succeeded); return queue(download, false); } } public void scanForOutdatedFilesInQueue() throws IOException { for(Download download : model.getDownloads()) { if (COMPLETED.contains(download.getState()) && !Outdated.equals(download.getState())) { Validator validator = new Validator(download); if (!validator.isChecksumsValid()) { log.info("Found outdated download " + download); download.setState(Outdated); getModel().updateDownload(download); } } } } private boolean isCompleted(Collection<Download> downloads) { for (Download download : downloads) { if (!COMPLETED.contains(download.getState())) return false; } return true; } private static final Object notificationMutex = new Object(); public void waitForCompletion(final Collection<Download> downloads) { final boolean[] found = new boolean[1]; found[0] = false; final long[] lastEvent = new long[1]; lastEvent[0] = currentTimeMillis(); TableModelListener l = new TableModelListener() { public void tableChanged(TableModelEvent e) { synchronized (notificationMutex) { lastEvent[0] = currentTimeMillis(); if (!isCompleted(downloads)) return; found[0] = true; notificationMutex.notifyAll(); } } }; model.addTableModelListener(l); try { while (!isCompleted(downloads)) { synchronized (notificationMutex) { if (found[0] || currentTimeMillis() - lastEvent[0] > WAIT_TIMEOUT) { break; } try { notificationMutex.wait(1000); } catch (InterruptedException e) { // intentionally left empty } } } } finally { model.removeTableModelListener(l); } if (!isCompleted(downloads)) throw new IllegalStateException(format("Waited %d seconds without all downloads to finish", WAIT_TIMEOUT / 1000)); } private static final Set<State> SUCCESSFUL = new HashSet<>(asList(NotModified, Succeeded)); public void executeDownload(String description, String url, Action action, File file, Runnable invokeAfterSuccessfulDownloadRunnable) { Download download = queueForDownload(description, url, action, new FileAndChecksum(file, null), null); if(!file.exists()) { waitForCompletion(singletonList(download)); if (!SUCCESSFUL.contains(download.getState())) return; } invokeAfterSuccessfulDownloadRunnable.run(); } }