/***************************************************************************** * Copyright (c) 2006-2007, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. *****************************************************************************/ package org.eclipse.buckminster.jnlp.cache; import static org.eclipse.buckminster.jnlp.bootstrap.BootstrapConstants.ERROR_CODE_FILE_IO_EXCEPTION; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.eclipse.buckminster.jnlp.bootstrap.BootstrapConstants; import org.eclipse.buckminster.jnlp.bootstrap.JNLPException; import org.eclipse.buckminster.jnlp.bootstrap.Messages; import org.eclipse.buckminster.jnlp.bootstrap.OperationCanceledException; import org.w3c.dom.DOMException; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * @author Filip Hrbek * * An alternative and simplified implementation of JNLP cache. <br> * <br> * It's main purpose is to parse a JNLP specified by an URL and possibly download its referenced resources. If * the JNLP is from the same location and the timestamp is equal to the cached one, no action is performed even * if the content of the JNLP file has changed. This enables using random mirrors inside the JNLP file. To force * updating resources referred to a JNLP file, the JNLP timestamp must change or the resources must be deleted * from cache first. <br> * The cache is safe to be used in concurrent environment. If two different processes materialize the same JNLP, * no conflict should occur, however the stuff will be cached twice. Following registrations of the JNLP will * delete obsolete stuff from the cache. <br> * Description: <br> * * <pre> * CACHE ROOT * | * +[MD5 hash for the URL] * | * +jnlptimestamp.entrytimestamp * | * +jars * | * +00000000001.jar * +00000000002.jar * +resources * +jnlptimestamp.entrytimestamp * +jnlptimestamp.entrytimestamp.temp * </pre> * * How registration of a JNLP to cache works:<br> *<br> * 1) Obtain connection headers and contents<br> * 2) Create MD5 hash for the URL<br> * 3) Find all folders in the URL's cache root<br> * 4) Select the latest non-temp folder<br> * 5) Delete recursively all non-latest folders older than a threshold of 1 day (if an exception is thrown, * ignore it)<br> * 6) If the latest folder timestamp equals to the last modified timestamp, delegate the contents to the class * loader and return "no change"<br> * 7) Create new folder jnlptimestamp.entrytimestamp.temp<br> * 8) Download all resources referenced by the JNLP<br> * 9) If everything was downloaded successfully, remove the ".temp" suffix from the folder, or delete it * otherwise (if an exception is thrown, ignore it)<br> * 10) Delegate the contents to the class loader and return "updated"<br> */ public class SimpleJNLPCache { private static final String TEMP_SEGMENT = "temp"; //$NON-NLS-1$ // Delete everything which is not the latest and is older than 24 hours private static final long OBSOLETE_THRESHOLD = 86400000; private static final String CORRUPTED_DOWNLOAD_FOLDER = "corrupted.download"; //$NON-NLS-1$ private final SimpleJNLPCacheClassLoader m_classLoader; private final File m_location; private List<ISimpleJNLPCacheListener> m_listeners; private File m_latestFile = null; public SimpleJNLPCache(File location) { m_location = location; m_classLoader = new SimpleJNLPCacheClassLoader(new URL[0], getClass().getClassLoader()); m_listeners = new ArrayList<ISimpleJNLPCacheListener>(); } public void addListener(ISimpleJNLPCacheListener listener) { m_listeners.add(listener); } public ClassLoader getClassLoader() { return m_classLoader; } public boolean registerJNLP(URL jnlp, IDownloadMonitor progress) throws JNLPException, DOMException, OperationCanceledException { boolean updated = false; for(ISimpleJNLPCacheListener listener : m_listeners) listener.initializing(jnlp); JNLPResource resource = new JNLPResource(jnlp); File jnlpCacheBase = new File(m_location, Utils.createHash(jnlp)); jnlpCacheBase.mkdirs(); if(!jnlpCacheBase.isDirectory()) throw new JNLPException( Messages.getString("unable_to_create_a_cache_entry_for") + jnlp.toString(), //$NON-NLS-1$ Messages.getString("check_your_access_permissions"), BootstrapConstants.ERROR_CODE_DIRECTORY_EXCEPTION); //$NON-NLS-1$ long currentTimestatmp = new Date().getTime(); long thresholdTimestamp = currentTimestatmp - OBSOLETE_THRESHOLD; long latestTimestamp = 0; File latestFile = null; List<File> obsoleteCandidates = new ArrayList<File>(); for(File file : jnlpCacheBase.listFiles()) { String[] segments = file.getName().split("\\."); //$NON-NLS-1$ // Check if the file is a folder and its name is not broken if(!file.isDirectory() || segments.length < 2 || segments.length > 3) continue; try { if(!(segments.length == 3 && TEMP_SEGMENT.equals(segments[2]))) { long timestamp = Long.parseLong(segments[0]); if(timestamp > latestTimestamp) { // tests if the latest cache is not corrupted if(!new File(file, CORRUPTED_DOWNLOAD_FOLDER).exists()) { latestTimestamp = timestamp; latestFile = file; } } } long entryTimestamp = Long.parseLong(segments[1]); if(entryTimestamp < thresholdTimestamp) obsoleteCandidates.add(file); } catch(NumberFormatException e) { // probably corrupted folder name } } if(latestTimestamp != resource.getLastModified().getTime()) { for(ISimpleJNLPCacheListener listener : m_listeners) listener.updateStarted(jnlp); updated = true; String dirname = Utils.formatDate(resource.getLastModified()) + "." //$NON-NLS-1$ + Utils.formatDate(Long.valueOf(currentTimestatmp)); String tempdirname = dirname + "." + TEMP_SEGMENT; //$NON-NLS-1$ File tempRoot = new File(jnlpCacheBase, tempdirname); File jarDir = new File(tempRoot, "jars"); //$NON-NLS-1$ File resourceDir = new File(tempRoot, "resources"); //$NON-NLS-1$ try { if(!tempRoot.mkdir() || !jarDir.mkdir() || !resourceDir.mkdir()) throw new JNLPException( Messages.getString("unable_to_create_a_cache_entry_for") + jnlp.toString(), //$NON-NLS-1$ Messages.getString("check_your_access_permissions"), BootstrapConstants.ERROR_CODE_DIRECTORY_EXCEPTION); //$NON-NLS-1$ try { PrintWriter jnlpOutput = new PrintWriter(new File(tempRoot, "cached.jnlp")); //$NON-NLS-1$ jnlpOutput.print(resource.getContent()); jnlpOutput.close(); } catch(Throwable e) { throw new JNLPException( Messages.getString("unable_to_create_a_cache_entry_for") + jnlp.toString() + ": " //$NON-NLS-1$ //$NON-NLS-2$ + e.getMessage(), Messages.getString("check_your_access_permissions"), //$NON-NLS-1$ BootstrapConstants.ERROR_CODE_DIRECTORY_EXCEPTION, e); } performDownloads(resource, jarDir, resourceDir, progress); latestFile = new File(jnlpCacheBase, dirname); if(!tempRoot.renameTo(latestFile)) throw new JNLPException( Messages.getString("unable_to_commit_a_cache_entry_for") + jnlp.toString(), //$NON-NLS-1$ Messages.getString("check_your_access_permissions"), BootstrapConstants.ERROR_CODE_DIRECTORY_EXCEPTION); //$NON-NLS-1$ } finally { try { Utils.deleteRecursive(tempRoot); } catch(Throwable e) { // ignore, perhaps something is broken } } } else if(latestFile != null) obsoleteCandidates.remove(latestFile); File jarDir = new File(latestFile, "jars"); //$NON-NLS-1$ File resourceDir = new File(latestFile, "resources"); //$NON-NLS-1$ try { m_classLoader.addUrl(resourceDir.toURI().toURL()); for(File jarFile : jarDir.listFiles()) { m_classLoader.addUrl(jarFile.toURI().toURL()); } } catch(MalformedURLException e) { throw new JNLPException( Messages.getString("unable_to_delegate_a_cache_entry_to_the_class_loader"), Messages.getString("report_to_vendor"), //$NON-NLS-1$ //$NON-NLS-2$ BootstrapConstants.ERROR_CODE_DIRECTORY_EXCEPTION); } for(File file : obsoleteCandidates) { try { Utils.deleteRecursive(file); } catch(Throwable e) { // ignore, perhaps something is broken } } for(ISimpleJNLPCacheListener listener : m_listeners) listener.finished(jnlp); m_latestFile = latestFile; return updated; } public void removeLatest() throws JNLPException { if(m_latestFile != null) { // mark this cache as corrupted - cannot deleted right now, it could be locked by browser try { new File(m_latestFile, CORRUPTED_DOWNLOAD_FOLDER).createNewFile(); } catch(IOException e) { throw new JNLPException( Messages.getString("can_not_create_a_new_file"), //$NON-NLS-1$ Messages.getString("check_disk_space_system_permissions_and_try_again"), ERROR_CODE_FILE_IO_EXCEPTION, e); //$NON-NLS-1$ } m_latestFile = null; } } public void removeListener(ISimpleJNLPCacheListener listener) { m_listeners.remove(listener); } private void performDownload(File jarDir, String urlString, String fileName, IDownloadMonitor progress) throws JNLPException, OperationCanceledException { URL url = null; try { url = new URL(urlString); URLConnection conn = url.openConnection(); long len = conn.getContentLength(); InputStream input = conn.getInputStream(); OutputStream output = new FileOutputStream(new File(jarDir, fileName)); int count; long read = 0; byte[] buf = new byte[0x2000]; while((count = input.read(buf)) >= 0) { progress.checkCanceled(); output.write(buf, 0, count); read += count; progress.progress(url, null, read, len, 0); } input.close(); output.close(); } catch(OperationCanceledException e) { throw e; } catch(Throwable e) { progress.downloadFailed(url, null); throw new JNLPException( Messages.getString("download_failed_for") + url.toString() + ": " + e.getMessage(), Messages.getString("try_again_later"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ BootstrapConstants.ERROR_CODE_DOWNLOAD_EXCEPTION, e); } } private void performDownloads(JNLPResource resource, File jarDir, File resourceDir, IDownloadMonitor progress) throws DOMException, JNLPException, OperationCanceledException { NodeList allJars = resource.getDocument().getElementsByTagName("jar"); //$NON-NLS-1$ String osName = System.getProperty("os.name"); //$NON-NLS-1$ String osArch = System.getProperty("os.arch"); //$NON-NLS-1$ if(osName != null) osName = osName.toLowerCase(); else osName = ""; //$NON-NLS-1$ if(osArch != null) osArch = osArch.toLowerCase(); else osArch = ""; //$NON-NLS-1$ int fileId = 0; int len = allJars.getLength(); for(int i = 0; i < len; i++) { Node jarNode = allJars.item(i); Node resourcesNode = jarNode.getParentNode(); if(resourcesNode == null || !"resources".equals(resourcesNode.getNodeName())) //$NON-NLS-1$ continue; NamedNodeMap resourcesAttributes = resourcesNode.getAttributes(); Node osNode = resourcesAttributes.getNamedItem("os"); //$NON-NLS-1$ if(osNode != null) { String osReq = osNode.getNodeValue().toLowerCase(); if(!osName.startsWith(osReq)) continue; } Node archNode = resourcesAttributes.getNamedItem("arch"); //$NON-NLS-1$ if(archNode != null) { String archReq = archNode.getNodeValue().toLowerCase(); if(!osArch.startsWith(archReq)) continue; } NamedNodeMap jarAttributes = jarNode.getAttributes(); String fileName = String.format("%010d", Integer.valueOf(fileId++)) + ".jar"; //$NON-NLS-1$ //$NON-NLS-2$ performDownload(jarDir, jarAttributes.getNamedItem("href").getNodeValue(), fileName, progress); //$NON-NLS-1$ } } }