/******************************************************************************* * Copyright (c) 2008, 2012 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation * Cloudsmith Inc - additional implementation * Sonatype Inc - additional implementation *******************************************************************************/ package org.eclipse.equinox.internal.p2.repository; import java.io.*; import java.net.*; import java.util.EventObject; import java.util.HashSet; import org.eclipse.core.runtime.*; import org.eclipse.equinox.internal.p2.core.helpers.LogHelper; import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus; import org.eclipse.equinox.internal.provisional.p2.core.eventbus.SynchronousProvisioningListener; import org.eclipse.equinox.internal.provisional.p2.repository.IStateful; import org.eclipse.equinox.internal.provisional.p2.repository.RepositoryEvent; import org.eclipse.equinox.p2.core.IAgentLocation; import org.eclipse.equinox.p2.core.ProvisionException; import org.eclipse.equinox.p2.repository.IRepository; import org.eclipse.osgi.util.NLS; /** * A class to manage metadata cache files. Creating the cache files will place * the file in the AgentData location in a cache directory. * * Using the bus listeners will allow the manager to listen for repository * events. When a repository is removed, it will remove the cache file if one * was created for the repository. */ public class CacheManager { /** * Service name for the internal cache manager service. */ public static final String SERVICE_NAME = CacheManager.class.getName(); private final IAgentLocation agentLocation; private final Transport transport; /** * IStateful implementation of BufferedOutputStream. Class is used to get the status from * a download operation. */ private static class StatefulStream extends BufferedOutputStream implements IStateful { private IStatus status; public StatefulStream(OutputStream stream) { super(stream); } public IStatus getStatus() { return status; } public void setStatus(IStatus aStatus) { status = aStatus; } } public CacheManager(IAgentLocation agentLocation, Transport transport) { this.agentLocation = agentLocation; this.transport = transport; } private static SynchronousProvisioningListener busListener; private static final String DOWNLOADING = "downloading"; //$NON-NLS-1$ private static final String JAR_EXTENSION = ".jar"; //$NON-NLS-1$ private static final String XML_EXTENSION = ".xml"; //$NON-NLS-1$ private final HashSet<String> knownPrefixes = new HashSet<String>(5); /** * Returns a hash of the repository location. */ private int computeHash(URI repositoryLocation) { return repositoryLocation.hashCode(); } public File createCacheFromFile(URI remoteFile, IProgressMonitor monitor) throws ProvisionException, IOException { if (!isURL(remoteFile)) { throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_NOT_FOUND, NLS.bind(Messages.CacheManager_CannotLoadNonUrlLocation, remoteFile), null)); } SubMonitor submonitor = SubMonitor.convert(monitor, 1000); try { File cacheFile = getCacheFile(remoteFile); boolean stale = true; long lastModified = cacheFile.lastModified(); long lastModifiedRemote = 0L; // bug 269588 - server may return 0 when file exists, so extra flag is needed try { lastModifiedRemote = transport.getLastModified(remoteFile, submonitor.newChild(1)); if (lastModifiedRemote <= 0) LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Server returned lastModified <= 0 for " + remoteFile)); //$NON-NLS-1$ } catch (AuthenticationFailedException e) { // it is not meaningful to continue - the credentials are for the server // do not pass the exception - it gives no additional meaningful user information throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_AUTHENTICATION, NLS.bind(Messages.CacheManager_AuthenticationFaileFor_0, remoteFile), null)); } catch (CoreException e) { // give up on a timeout - if we did not get a 404 on the jar, we will just prolong the pain // by (almost certainly) also timing out on the xml. if (e.getStatus() != null && e.getStatus().getException() != null) { Throwable ex = e.getStatus().getException(); if (ex.getClass() == java.net.SocketTimeoutException.class) throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, NLS.bind(Messages.CacheManager_FailedCommunicationWithRepo_0, remoteFile), ex)); } } catch (OperationCanceledException e) { // must pass this on throw e; } catch (Exception e) { // not ideal, just skip the jar on error, and try the xml instead - report errors for // the xml. } stale = lastModifiedRemote != lastModified; if (!stale) return cacheFile; // The cache is stale or missing, so we need to update it from the remote location updateCache(cacheFile, remoteFile, lastModifiedRemote, submonitor); return cacheFile; } finally { submonitor.done(); } } /** * Returns a local cache file with the contents of the given remote location, * or <code>null</code> if a local cache could not be created. * * @param repositoryLocation The remote location to be cached * @param prefix The prefix to use when creating the cache file * @param monitor a progress monitor * @return A {@link File} object pointing to the cache file or <code>null</code> * if the location is not a repository. * @throws FileNotFoundException if neither jar nor xml index file exists at given location * @throws AuthenticationFailedException if jar not available and xml causes authentication fail * @throws IOException on general IO errors * @throws ProvisionException on any error (e.g. user cancellation, unknown host, malformed address, connection refused, etc.) * @throws OperationCanceledException - if user canceled */ public File createCache(URI repositoryLocation, String prefix, IProgressMonitor monitor) throws IOException, ProvisionException { if (!isURL(repositoryLocation)) { throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_NOT_FOUND, NLS.bind(Messages.CacheManager_CannotLoadNonUrlLocation, repositoryLocation), null)); } SubMonitor submonitor = SubMonitor.convert(monitor, 1000); try { knownPrefixes.add(prefix); File cacheFile = getCache(repositoryLocation, prefix); URI jarLocation = URIUtil.append(repositoryLocation, prefix + JAR_EXTENSION); URI xmlLocation = URIUtil.append(repositoryLocation, prefix + XML_EXTENSION); int hashCode = computeHash(repositoryLocation); // Knowing if cache is stale is complicated by the fact that a jar could have been // produced after an xml index (and vice versa), and by the need to capture any // errors, as these needs to be reported to the user as something meaningful - instead of // just a general "can't read repository". // (Previous impl of stale checking ignored errors, and caused multiple round-trips) boolean stale = true; long lastModified = 0L; String name = null; String useExtension = JAR_EXTENSION; URI remoteFile = jarLocation; if (cacheFile != null) { lastModified = cacheFile.lastModified(); name = cacheFile.getName(); } // get last modified on jar long lastModifiedRemote = 0L; // bug 269588 - server may return 0 when file exists, so extra flag is needed boolean useJar = true; try { lastModifiedRemote = transport.getLastModified(jarLocation, submonitor.newChild(1)); if (lastModifiedRemote <= 0) LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Server returned lastModified <= 0 for " + jarLocation)); //$NON-NLS-1$ } catch (AuthenticationFailedException e) { // it is not meaningful to continue - the credentials are for the server // do not pass the exception - it gives no additional meaningful user information throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_AUTHENTICATION, NLS.bind(Messages.CacheManager_AuthenticationFaileFor_0, repositoryLocation), null)); } catch (CoreException e) { useJar = false; // give up on a timeout - if we did not get a 404 on the jar, we will just prolong the pain // by (almost certainly) also timing out on the xml. if (e.getStatus() != null && e.getStatus().getException() != null) { Throwable ex = e.getStatus().getException(); if (ex.getClass() == java.net.SocketTimeoutException.class) throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, NLS.bind(Messages.CacheManager_FailedCommunicationWithRepo_0, repositoryLocation), ex)); } } catch (OperationCanceledException e) { // must pass this on throw e; } catch (Exception e) { // not ideal, just skip the jar on error, and try the xml instead - report errors for // the xml. useJar = false; } if (submonitor.isCanceled()) throw new OperationCanceledException(); if (useJar) { // There is a jar, and it should be used - cache is stale if it is xml based or // if older (irrespective of jar or xml). // Bug 269588 - also stale if remote reports 0 stale = lastModifiedRemote != lastModified || (name != null && name.endsWith(XML_EXTENSION) || lastModifiedRemote <= 0); } else { // Also need to check remote XML file, and handle cancel, and errors // (Status is reported based on finding the XML file as giving up on certain errors // when checking for the jar may not be correct). try { lastModifiedRemote = transport.getLastModified(xmlLocation, submonitor.newChild(1)); // if lastModifiedRemote is 0 - something is wrong in the communication stack, as // a FileNotFound exception should have been thrown. // bug 269588 - server may return 0 when file exists - site is not correctly configured if (lastModifiedRemote <= 0) LogHelper.log(new Status(IStatus.WARNING, Activator.ID, "Server returned lastModified <= 0 for " + xmlLocation)); //$NON-NLS-1$ } catch (FileNotFoundException e) { throw new FileNotFoundException(NLS.bind(Messages.CacheManager_Neither_0_nor_1_found, jarLocation, xmlLocation)); } catch (AuthenticationFailedException e) { // do not pass the exception, it provides no additional meaningful user information throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_AUTHENTICATION, NLS.bind(Messages.CacheManager_AuthenticationFaileFor_0, repositoryLocation), null)); } catch (CoreException e) { IStatus status = e.getStatus(); if (status == null) throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_NOT_FOUND, NLS.bind(Messages.CacheManager_FailedCommunicationWithRepo_0, repositoryLocation), e)); else if (status.getException() instanceof FileNotFoundException) throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_NOT_FOUND, status.getMessage(), status.getException())); throw new ProvisionException(status); } // There is an xml, and it should be used - cache is stale if it is jar based or // if older (irrespective of jar or xml). // bug 269588 - server may return 0 when file exists - assume it is stale stale = lastModifiedRemote != lastModified || (name != null && name.endsWith(JAR_EXTENSION) || lastModifiedRemote <= 0); useExtension = XML_EXTENSION; remoteFile = xmlLocation; } if (!stale) return cacheFile; // The cache is stale or missing, so we need to update it from the remote location cacheFile = new File(getCacheDirectory(), prefix + hashCode + useExtension); updateCache(cacheFile, remoteFile, lastModifiedRemote, submonitor); return cacheFile; } finally { submonitor.done(); } } /** * Deletes the local cache file(s) for the given repository * @param repositoryLocation */ void deleteCache(URI repositoryLocation) { for (String prefix : knownPrefixes) { File[] cacheFiles = getCacheFiles(repositoryLocation, prefix); for (int i = 0; i < cacheFiles.length; i++) { // delete the cache file if it exists safeDelete(cacheFiles[i]); // delete a resumable download if it exists safeDelete(new File(new File(cacheFiles[i].getParentFile(), DOWNLOADING), cacheFiles[i].getName())); } } } /** * Determines the local file path of the repository's cache file. * @param repositoryLocation The location to compute the cache for * @param prefix The prefix to use for this location * @return A {@link File} pointing to the cache file or <code>null</code> if * the cache file does not exist. */ protected File getCache(URI repositoryLocation, String prefix) { File[] files = getCacheFiles(repositoryLocation, prefix); if (files[0].exists()) return files[0]; return files[1].exists() ? files[1] : null; } /** * Returns the file corresponding to the data area to be used by the cache manager. */ protected File getCacheDirectory() { return URIUtil.toFile(agentLocation.getDataArea(Activator.ID + "/cache/")); //$NON-NLS-1$ } /** * Determines the local file paths of the repository's potential cache files. * @param repositoryLocation The location to compute the cache for * @param prefix The prefix to use for this location * @return A {@link File} array with the cache files for JAR and XML extensions. */ private File[] getCacheFiles(URI repositoryLocation, String prefix) { File[] files = new File[2]; File dataAreaFile = getCacheDirectory(); int hashCode = computeHash(repositoryLocation); files[0] = new File(dataAreaFile, prefix + hashCode + JAR_EXTENSION); files[1] = new File(dataAreaFile, prefix + hashCode + XML_EXTENSION); return files; } private File getCacheFile(URI url) { File dataAreaFile = getCacheDirectory(); int hashCode = computeHash(url); return new File(dataAreaFile, Integer.toString(hashCode)); } private static boolean isURL(URI location) { try { new URL(location.toASCIIString()); } catch (MalformedURLException e) { return false; } return true; } /** * Adds a {@link SynchronousProvisioningListener} to the event bus for * deleting cache files when the corresponding repository is deleted. */ private void registerRepoEventListener(IProvisioningEventBus eventBus) { if (busListener == null) { busListener = new SynchronousProvisioningListener() { public void notify(EventObject o) { if (o instanceof RepositoryEvent) { RepositoryEvent event = (RepositoryEvent) o; if (RepositoryEvent.REMOVED == event.getKind() && IRepository.TYPE_METADATA == event.getRepositoryType()) { deleteCache(event.getRepositoryLocation()); } } } }; } // the bus could have disappeared and is now back again - so do this every time eventBus.addListener(busListener); } private boolean safeDelete(File file) { if (file.exists()) { if (!file.delete()) { file.deleteOnExit(); return true; } } return false; } public void setEventBus(IProvisioningEventBus newBus) { registerRepoEventListener(newBus); } public void unsetEventBus(IProvisioningEventBus oldBus) { unregisterRepoEventListener(oldBus); } /** * Removes the {@link SynchronousProvisioningListener} that cleans up the * cache file from the event bus. */ private void unregisterRepoEventListener(IProvisioningEventBus bus) { if (bus != null && busListener != null) bus.removeListener(busListener); } protected void updateCache(File cacheFile, URI remoteFile, long lastModifiedRemote, SubMonitor submonitor) throws FileNotFoundException, IOException, ProvisionException { cacheFile.getParentFile().mkdirs(); File downloadDir = new File(cacheFile.getParentFile(), DOWNLOADING); if (!downloadDir.exists()) downloadDir.mkdir(); File tempFile = new File(downloadDir, cacheFile.getName()); // Ensure that the file from a previous download attempt is removed if (tempFile.exists()) safeDelete(tempFile); tempFile.createNewFile(); StatefulStream stream = null; try { stream = new StatefulStream(new FileOutputStream(tempFile)); } catch (Exception e) { throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, e.getMessage(), e)); } IStatus result = null; try { submonitor.setWorkRemaining(1000); result = transport.download(remoteFile, stream, submonitor.newChild(1000)); } catch (OperationCanceledException e) { // need to pick up the status - a new operation canceled exception is thrown at the end // as status will be CANCEL. result = stream.getStatus(); } finally { stream.close(); // If there was any problem fetching the file, delete the temp file if (result == null || !result.isOK()) safeDelete(tempFile); } if (result.isOK()) { if (cacheFile.exists()) safeDelete(cacheFile); if (tempFile.renameTo(cacheFile)) { if (lastModifiedRemote != -1 && lastModifiedRemote != 0) { //local cache file should have the same lastModified as the server's file. bug 324200 cacheFile.setLastModified(lastModifiedRemote); } return; } result = new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.CacheManage_ErrorRenamingCache, new Object[] {remoteFile.toString(), tempFile.getAbsolutePath(), cacheFile.getAbsolutePath()})); } if (result.getSeverity() == IStatus.CANCEL || submonitor.isCanceled()) throw new OperationCanceledException(); throw new ProvisionException(result); } }