blob: 0e4eba8e53b1e4cab4dddcedf7078f0c79f20997 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2009 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
*******************************************************************************/
package org.eclipse.equinox.internal.p2.metadata.repository;
import org.eclipse.equinox.p2.core.ProvisionException;
import java.io.*;
import java.net.URI;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.ecf.filetransfer.UserCancelledException;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.repository.*;
import org.eclipse.equinox.internal.p2.repository.Activator;
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.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;
/**
* 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) {
this.agentLocation = agentLocation;
}
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 PROP_RESUMABLE = "org.eclipse.equinox.p2.metadata.repository.resumable"; //$NON-NLS-1$
private static final String RESUME_DEFAULT = "true"; //$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();
}
/**
* 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 {
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 = getTransport().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 = getTransport().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 (UserCancelledException e) {
throw new OperationCanceledException();
} 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(), null));
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.
*/
private 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.
*/
private 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 RepositoryTransport getTransport() {
return RepositoryTransport.getInstance();
}
private boolean isResumeEnabled() {
String resumeProp = System.getProperty(PROP_RESUMABLE, RESUME_DEFAULT);
return Boolean.valueOf(resumeProp).booleanValue();
}
/**
* Make cacheFile resumable and return true if it was possible.
* @param cacheFile - the partially downloaded file to make resumeable
* @param remoteFile The remote file being cached
* @param status - the download status reported for the partial download
* @return true if the file was made resumable, false otherwise
*/
private boolean makeResumeable(File cacheFile, URI remoteFile, IStatus status) {
if (status == null || status.isOK() || cacheFile == null || !(status instanceof DownloadStatus))
return false;
// check if resume feature is turned off
if (!isResumeEnabled())
return false;
DownloadStatus downloadStatus = (DownloadStatus) status;
long currentLength = cacheFile.length();
// if cache file does not exist, or nothing was written to it, there is nothing to resume
if (currentLength == 0L)
return false;
long reportedSize = downloadStatus.getFileSize();
long reportedModified = downloadStatus.getLastModified();
if (reportedSize == DownloadStatus.UNKNOWN_SIZE || reportedSize == 0L) {
LogHelper.log(new Status(IStatus.WARNING, Activator.ID, NLS.bind("Download of {0} not resumable because filesize not reported.", remoteFile))); //$NON-NLS-1$
return false;
}
if (reportedModified <= 0) {
LogHelper.log(new Status(IStatus.WARNING, Activator.ID, NLS.bind("Download of {0} not resumable because last-modified not reported.", remoteFile))); //$NON-NLS-1$
return false;
}
// if more than what was reported has been written something odd is going on, and we can't
// trust the reported size.
// There is a small chance that user canceled in the time window after the full download is seen, and the result is returned. In this
// case the reported and current lengths will be equal.
if (reportedSize < currentLength) {
LogHelper.log(new Status(IStatus.WARNING, Activator.ID, NLS.bind("Download of {0} not resumable because more was read then reported size.", remoteFile))); //$NON-NLS-1$
return false;
}
File resumeDir = new File(cacheFile.getParentFile(), DOWNLOADING);
if (!resumeDir.mkdir()) {
if (!(resumeDir.exists() && resumeDir.isDirectory())) {
LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind("Could not create directory {0} for resumable download of {1}", resumeDir, remoteFile))); //$NON-NLS-1$
return false;
}
}
// move partial cache file to "downloading" directory
File resumeFile = new File(resumeDir, cacheFile.getName());
if (!cacheFile.renameTo(resumeFile)) {
LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind("Could not move {0} to {1} for resumed download", cacheFile, resumeFile))); //$NON-NLS-1$
return false;
}
// touch the file with remote modified time
if (!resumeFile.setLastModified(reportedModified)) {
LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind("Could not set last modified time on {0} for resumed download", resumeFile))); //$NON-NLS-1$
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);
}
private void updateCache(File cacheFile, URI remoteFile, long lastModifiedRemote, SubMonitor submonitor) throws FileNotFoundException, IOException, ProvisionException {
cacheFile.getParentFile().mkdirs();
File resumeFile = new File(new File(cacheFile.getParentFile(), DOWNLOADING), cacheFile.getName());
// if append should be performed or not
boolean append = false;
if (resumeFile.exists()) {
// the resume file can be too old
if (lastModifiedRemote != resumeFile.lastModified() || lastModifiedRemote <= 0)
safeDelete(resumeFile);
else {
if (resumeFile.renameTo(cacheFile))
append = true;
else
LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind("Could not move resumable file {0} into cache", resumeFile))); //$NON-NLS-1$
}
}
StatefulStream metadata = new StatefulStream(new FileOutputStream(cacheFile, append));
IStatus result = null;
try {
submonitor.setWorkRemaining(1000);
// resume from cache file's length if in append mode
result = getTransport().download(remoteFile, metadata, append ? cacheFile.length() : -1, 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 = metadata.getStatus();
} finally {
metadata.close();
// result is null if a runtime error (other than OperationCanceledException)
// occurred, just delete the cache file (or a later attempt could fail
// with "premature end of file").
if (result == null)
cacheFile.delete();
}
if (result.isOK())
return;
// if possible, keep a partial download to be resumed.
if (!makeResumeable(cacheFile, remoteFile, result))
cacheFile.delete();
if (result.getSeverity() == IStatus.CANCEL || submonitor.isCanceled())
throw new OperationCanceledException();
throw new ProvisionException(result);
}
}