/******************************************************************************* * Copyright (c) 2013 Red Hat, Inc. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is 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: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.jboss.tools.foundation.core.ecf.internal; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.SecureRandom; import java.util.HashMap; import java.util.Iterator; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.jboss.tools.foundation.core.digest.DigestUtils; import org.jboss.tools.foundation.core.ecf.Messages; import org.jboss.tools.foundation.core.ecf.URLTransportUtility; import org.jboss.tools.foundation.core.internal.FoundationCorePlugin; import org.jboss.tools.foundation.core.internal.Trace; public class URLTransportCache { /** * Encoding for this file */ private static final String ENCODING = "UTF-8"; /** * The default cache folder name inside foundation.core's metadata */ private static final String LOCAL_CACHE_LOCATION_FOLDER = "ECF_REMOTE_CACHE"; /** * The legacy key for pulling the cache map from plugin preferences */ private static final String CACHE_MAP_KEY = "URLTransportCache.CacheMapKey"; /** * The new index file, to be stored in each cache root folder. */ private static final String CACHE_INDEX_FILE = "URLTransportCache.cacheIndex.properties"; /** * The default cache folder */ private static final IPath DEFAULT_CACHE_FOLDER = FoundationCorePlugin.getDefault().getStateLocation().append(LOCAL_CACHE_LOCATION_FOLDER); /** * A collection of all caches currently in use. * This is to ensure multiple clients don't try using * two instances with the same basedir, which could lead to corruption of the cache. */ private static final HashMap<IPath, URLTransportCache> cacheDirToCache = new HashMap<IPath, URLTransportCache>(); public synchronized static URLTransportCache getDefault() { return getCache(DEFAULT_CACHE_FOLDER); } public synchronized static URLTransportCache getCache(IPath root) { URLTransportCache c = cacheDirToCache.get(root); if( c == null ) { c = new URLTransportCache(root); cacheDirToCache.put(root, c); } return c; } private HashMap<String, String> cache; private IPath cacheRoot; protected URLTransportCache(IPath cacheRoot) { this.cacheRoot = cacheRoot; this.cache = new HashMap<String, String>(); load(); } /** * Get a cached file for the given url only if it is downloaded and exists. * * @param url * @return */ public File getCachedFile(String url) { String cacheVal = cache.get(url); if (cacheVal == null) return null; File f = new File(cacheVal); if (f.exists()) return f; return null; } /** * Check whether the cache is outdated * @throws CoreException if the remote url is invalid, or the remote url cannot be reached */ public boolean isCacheOutdated(String url, IProgressMonitor monitor) throws CoreException { Trace.trace(Trace.STRING_FINER, "Checking if cache is outdated for " + url); File f = getCachedFile(url); if (f == null) return true; URL url2 = null; try { url2 = new URL(url); } catch (MalformedURLException murle) { throw new CoreException(FoundationCorePlugin.statusFactory() .errorStatus(Messages.ECFExamplesTransport_IO_error, murle)); } long remoteModified = new URLTransportUtility().getLastModified(url2, monitor); // If the remoteModified is -1 but we have a local cache, use that (not outdated) if (remoteModified == -1 ) { if (f.exists()) return false; } // !!! urlModified == 0 when querying files from github // It means that files from github can not be cached! if (f.exists()) { long modified = f.lastModified(); if( remoteModified > modified ) { // The remote file has been updated *after* the local file was created, so, outdated return true; } if( remoteModified == 0 ) { // File comes from github or some other server not keeping accurate timestamps // so, possibly oudated, and must re-fetch return true; } // Our local copy has a higher timestamp, so was fetched after return false; } // Local file doesn't exist, so, cache is outdated return true; } public File downloadAndCache(String url, String displayName, int lifespan, URLTransportUtility util, IProgressMonitor monitor) throws CoreException { return downloadAndCache(url, displayName, lifespan, util, -1, monitor); } public File downloadAndCache(String url, String displayName, int lifespan, URLTransportUtility util, int timeout, IProgressMonitor monitor) throws CoreException { Trace.trace(Trace.STRING_FINER, "Downloading and caching " + url + " with lifespan=" + lifespan); File existing = getExistingRemoteFileCacheLocation(url); File target = createNewRemoteFileCacheLocation(url); try { OutputStream os = new FileOutputStream(target); IStatus s = util.download(displayName, url, os, timeout, monitor); if (s.isOK()) { // Download completed successfully, add to cache, delete old copy if (lifespan == URLTransportUtility.CACHE_UNTIL_EXIT) target.deleteOnExit(); addToCache(url, target); if( existing != null && existing.exists()) existing.delete(); return target != null && target.exists() ? target : null; } // Download did not go as planned. Delete the new, return the old if( target != null && target.exists()) { target.delete(); } return existing; } catch (IOException ioe) { throw new CoreException(FoundationCorePlugin.statusFactory() .errorStatus(Messages.ECFExamplesTransport_IO_error, ioe)); } } private void addToCache(String url, File target) { cache.put(url, target.getAbsolutePath()); savePreferences(); } private void load() { if( cacheRoot.equals(DEFAULT_CACHE_FOLDER)) { // Load from the legacy preferences first. // These values will be overridden by those in a concrete index file IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(FoundationCorePlugin.PLUGIN_ID); String val = prefs.get(CACHE_MAP_KEY, ""); loadIndexFromString(val); } File index = cacheRoot.append(CACHE_INDEX_FILE).toFile(); if( index.exists() && index.isFile()) { try { String contents = getContents(index); loadIndexFromString(contents); } catch(IOException ioe) { FoundationCorePlugin.pluginLog().logError(ioe); } } Trace.trace(Trace.STRING_FINER, "Loaded " + cache.size() + " cache file locations from preferences"); } private void loadIndexFromString(String val) { if( !isEmpty(val)) { String[] byLine = val.split("\n"); for( int i = 0; i < byLine.length; i++ ) { if( isEmpty(byLine[i])) continue; String[] kv = byLine[i].split("="); if( kv.length == 2 && !isEmpty(kv[0]) && !isEmpty(kv[1])) { try { String decodedUrl = URLDecoder.decode(kv[0],ENCODING); if( new File(kv[1]).exists() ) cache.put(decodedUrl,kv[1]); } catch(UnsupportedEncodingException uee) { // Should not be hit FoundationCorePlugin.pluginLog().logError(uee); } } } } } private boolean isEmpty(String s) { return s == null || "".equals(s); } private void savePreferences() { // Saves are now done to an index file in the cache root. File index = cacheRoot.append(CACHE_INDEX_FILE).toFile(); Trace.trace(Trace.STRING_FINER, "Saving " + cache.size() + " cache file locations to " + index.getAbsolutePath()); StringBuffer sb = new StringBuffer(); Iterator<String> it = cache.keySet().iterator(); while(it.hasNext()) { String k = it.next(); String v = cache.get(k); String encodedURL = null; try { encodedURL = URLEncoder.encode(k, ENCODING); } catch(UnsupportedEncodingException uee) { // Should never happen } if( encodedURL != null ) sb.append(encodedURL + "=" + v + "\n"); } try { setContents(index, sb.toString()); } catch (IOException e) { FoundationCorePlugin.pluginLog().logError(e); } } /* * Get the existing cache location for the given url if it exists * @param url * @return */ private synchronized File getExistingRemoteFileCacheLocation(String url) { // If this url is already cached, use it String cachedLoc = cache.get(url); if (cachedLoc != null) { File f = new File(cache.get(url)); return f; } return null; } /* * Get a file in the core plugin's state location which is where the local * cache of remote file would be */ private synchronized File getRemoteFileCacheLocation(String url) { File existing = getExistingRemoteFileCacheLocation(url); if( existing != null) { return existing; } return createNewRemoteFileCacheLocation(url); } private synchronized File createNewRemoteFileCacheLocation(String url) { // Otherwise, make a new one File root = getLocalCacheFolder().toFile(); root.mkdirs(); String tmp; try { tmp = DigestUtils.sha1(url); } catch (IOException O_o) { //That really can't happen tmp = url.replaceAll("[\\p{Punct}&&[^_]]", "_"); } File cached = null; do { cached = new File(root, tmp + new SecureRandom().nextLong() + ".tmp"); } while (cached.exists()); return cached; } private IPath getLocalCacheFolder() { return cacheRoot; } /* * foundation.core has no IO utility classes. * If it gets some, this should be saved there. */ private static String getContents(File aFile) throws IOException { return new String(getBytesFromFile(aFile), ENCODING); } private static byte[] getBytesFromFile(File file) throws IOException { InputStream is = new FileInputStream(file); try { byte[] bytes = new byte[(int)file.length()]; int offset = 0; int numRead = 0; while (offset < bytes.length && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) { offset += numRead; } return bytes; } finally { is.close(); } } private static void setContents(File file, String contents) throws IOException { Writer out = null; try { out = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), ENCODING)); out.append(contents); } finally { if (out != null) { out.close(); } } } }