/******************************************************************************* * Copyright (c) 2012, 2013 Pivotal Software, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URISyntaxException; import java.net.URL; import org.apache.commons.io.FileUtils; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.URIUtil; import org.eclipse.swt.widgets.Display; import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil; import org.springsource.ide.eclipse.commons.frameworks.core.FrameworkCoreActivator; import org.springsource.ide.eclipse.commons.frameworks.core.util.FileUtil; /** * Manages a cache of downloaded content. * * @author Kris De Volder */ public class DownloadManager { /** * An instance of this interface represent an action to execute on a downloaded * File. The action may indicate failure by throwing an exception or by * returning false. A failed action may trigger the DownloadManager to * clear the cache and try again for a limited number of times. */ public interface DownloadRequestor { void exec(File downloadedFile) throws Exception; } /** * An instance of this class represents some type of service able to fetch the * content data. */ public interface DownloadService { void fetch(URL url, OutputStream writeTo) throws IOException; } private final File cacheDirectory; private final DownloadService downloader; private boolean deleteCacheOnDispose = false; private boolean allowUIThread = false; private int tries = 5; //5 retries by default private long retryInterval = 0; //no wait between retries by default. public DownloadManager(DownloadService downloader, File cacheDir) throws IOException { if (cacheDir==null) { cacheDir = FileUtil.createTempDirectory("downloadCache"); this.deleteCacheOnDispose = true; // This dir can not be used by anynone after this DLM is diposed so better delete it. } if (downloader==null) { downloader = new SimpleDownloadService(); } this.downloader = downloader; this.cacheDirectory = cacheDir; if (!cacheDir.isDirectory()) { Assert.isTrue(cacheDir.mkdirs(), "Couldn't create cache directory at "+cacheDir); } } public DownloadManager clearCache() { FileUtils.deleteQuietly(cacheDirectory); cacheDirectory.mkdirs(); return this; } /** * Deprecated use one of the other contructors (probably you want to use * the one that takes a {@link URLConnectionFactory} if you where using * this one. */ @Deprecated public DownloadManager() throws IOException { this(new URLConnectionFactory()); } /** * Create a Default downloadmanager. This downloadmanager does not use authentication and * simply uses standard JavaApi to fetch url content. * <p> * A new temp directory is created to use as the cache directory. * <p> * When this DownloadManager is disposed the cache directory is deleted (since the next * DownloadManager created in this way will use a different cache dir anyway there isn't * much use leaving it around). */ public DownloadManager(URLConnectionFactory urlConnectionFactory) throws IOException { this(new SimpleDownloadService(urlConnectionFactory), null); this.deleteCacheOnDispose = true; } /** * This method is deprecated, please use doWithDownload to provide proper recovery * for cache corruption. */ @Deprecated public File downloadFile(DownloadableItem item) throws URISyntaxException, FileNotFoundException, CoreException, IOException, UIThreadDownloadDisallowed { File target = getLocalLocation(item); if (target.exists()) { return target; } if (!allowUIThread && Display.getCurrent()!=null) { throw new UIThreadDownloadDisallowed("Don't call download manager from the UI Thread unless the data is already cached."); } // // ); //It is important not to lock the UI thread for downloads!!! // If the UI thread is well behaved, we assume it will be careful not to call this method unless the // content is already cached. So once we get past the exists check it is ok to grab the lock. synchronized (this) { //It is possible that multiple thread where waiting to enter here... only one of them should proceed // to actually download. So make sure to retest the target.exists() condition to avoid multiple downloads if (target.exists()) { return target; } if (!cacheDirectory.exists()) { cacheDirectory.mkdirs(); } File targetPart = new File(target.toString()+".part"); FileOutputStream out = new FileOutputStream(targetPart); try { URL url = item.getURL(); System.out.println("Downloading " + url + " to " + target); downloader.fetch(url, out); } finally { out.close(); } if (!targetPart.renameTo(target)) { throw new IOException("Error while renaming " + targetPart + " to " + target); } return target; } } /** * Get the local location of given item. Note that the item may or may not yet be downloaded so * there are no guarantees there is anything meaningful to be found there. */ public File getLocalLocation(DownloadableItem item) { URL url = item.getURL(); String protocol = url.getProtocol(); try { if ("file".equals(protocol)) { //already local, so don't bother downloading. return new File(URIUtil.toURI(url)); } } catch (URISyntaxException e) { FrameworkCoreActivator.log(e); } String filename = item.getFileName(); File target = new File(cacheDirectory, filename); return target; } /** * This method tries to download or fetch a File from the cache, then passes the * downloaded file to the DownloadRequestor. * <p> * If the requestor fails to properly execute on the downloaded file, the cache * will be presumed to be corrupt. The file will be deleted from the cache * and the download will be tried again. (for a limited number of times) */ public void doWithDownload(DownloadableItem target, DownloadRequestor action) throws Exception { int tries = getTries(); // try at most X times Throwable e = null; File downloadedFile = null; do { tries--; try { downloadedFile = downloadFile(target); action.exec(downloadedFile); return; // action succeeded without exceptions } catch (UIThreadDownloadDisallowed caught) { //No sense retrying this as it will just fail again... throw caught; } catch (Throwable caught) { FrameworkCoreActivator.log(caught); //Presume the cache may be corrupt! //System.out.println("Delete corrupt download: "+downloadedFile); if (downloadedFile!=null) { downloadedFile.delete(); downloadedFile = null; } e = caught; if (tries>0 && retryInterval>0) { Thread.sleep(retryInterval); } } } while (tries>0); //Can only get here if action failed to execute on downloaded file... //thus, e can not be null. throw ExceptionUtil.exception(e); } /** * @since 3.6.3 */ public int getTries() { return tries; } /** * Sets the maximum number of times DownloadManager will try and retyr to fetch * a failed download. Note that the first try also counts. So if 'tries' is * set to 3 then a failed download will be retried upto 2 times. I.e. one * try and then 2 retries. * * @since 3.6.3 */ public DownloadManager setTries(int r) { //Setting to 0 is not sensible because it would mean to just fail without even trying. Assert.isLegal(r>1); this.tries = r; return this; } /** * Sets the time in milliseconds to wait between retries. The default is 0 meaning no * wait. * * @since 3.6.3 */ public void setRetryInterval(long retryInterval) { Assert.isLegal(retryInterval>=0); this.retryInterval = retryInterval; } /** * @since 3.6.3 */ public long getRetryInterval(long retryInterval) { return retryInterval; } public File getCacheDir() { return cacheDirectory; } public boolean isDownloaded(DownloadableItem item) { File target = getLocalLocation(item); return target!=null && target.exists(); } /** * Dispose this download manager. Optionally delete its cache directory as well. */ public void dispose() { if (deleteCacheOnDispose) { FileUtils.deleteQuietly(cacheDirectory); deleteCacheOnDispose = false; } } /** * If this is false, an exception will be thrown if download is attempted on * the UI thread. The default value is true. */ public DownloadManager allowUIThread(boolean allow) { allowUIThread = allow; return this; } }