| /******************************************************************************* |
| * Copyright (c) 2008, 2010 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 |
| * Sonatype, Inc. - transport split |
| *******************************************************************************/ |
| package org.eclipse.equinox.internal.p2.updatesite; |
| |
| import java.io.*; |
| import java.lang.ref.SoftReference; |
| import java.net.*; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.zip.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.equinox.internal.p2.core.helpers.LogHelper; |
| import org.eclipse.equinox.internal.p2.publisher.eclipse.FeatureParser; |
| import org.eclipse.equinox.internal.p2.repository.Transport; |
| import org.eclipse.equinox.p2.core.ProvisionException; |
| import org.eclipse.equinox.p2.publisher.eclipse.*; |
| import org.eclipse.osgi.util.NLS; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * @since 1.0 |
| */ |
| public class UpdateSite { |
| |
| private static final String VERSION_SEPARATOR = "_"; //$NON-NLS-1$ |
| private static final String JAR_EXTENSION = ".jar"; //$NON-NLS-1$ |
| private static final String FEATURE_DIR = "features/"; //$NON-NLS-1$ |
| private static final String PLUGIN_DIR = "plugins/"; //$NON-NLS-1$ |
| private static final String FEATURE_TEMP_FILE = "feature"; //$NON-NLS-1$ |
| private static final String SITE_FILE = "site.xml"; //$NON-NLS-1$ |
| private static final String PROTOCOL_FILE = "file"; //$NON-NLS-1$ |
| private static final int RETRY_COUNT = 2; |
| private static final String DOT_XML = ".xml"; //$NON-NLS-1$ |
| private static final String SITE = "site"; //$NON-NLS-1$ |
| private String checksum; |
| private URI location; |
| private URI rootLocation; |
| private SiteModel site; |
| |
| /* |
| * Some variables for caching. |
| */ |
| // map of String (URI.toString()) to UpdateSite |
| private static Map<String, SoftReference<UpdateSite>> siteCache = new HashMap<String, SoftReference<UpdateSite>>(); |
| // map of String (URI.toString()) to UpdateSite (for category xmls) |
| private static Map<String, SoftReference<UpdateSite>> categoryCache = new HashMap<String, SoftReference<UpdateSite>>(); |
| // map of String (featureID_featureVersion) to Feature |
| private Map<String, Feature> featureCache = new HashMap<String, Feature>(); |
| private Transport transport; |
| |
| /* |
| * Return a URI based on the given URI, which points to a site.xml file. |
| */ |
| private static URI getSiteURI(URI baseLocation) { |
| String segment = URIUtil.lastSegment(baseLocation); |
| if (constainsUpdateSiteFileName(segment)) |
| return baseLocation; |
| return URIUtil.append(baseLocation, SITE_FILE); |
| } |
| |
| /** |
| * Be lenient about accepting any location with *site*.xml at the end. |
| */ |
| private static boolean constainsUpdateSiteFileName(String segment) { |
| return segment != null && segment.endsWith(DOT_XML) && segment.indexOf(SITE) != -1; |
| } |
| |
| /** |
| * Loads and returns a category file |
| * @param location |
| * @param monitor |
| * @return A CategoryFile |
| * @throws ProvisionException |
| */ |
| public static synchronized UpdateSite loadCategoryFile(URI location, Transport transport, IProgressMonitor monitor) throws ProvisionException { |
| if (location == null) |
| return null; |
| UpdateSite result = null; |
| if (!PROTOCOL_FILE.equals(location.getScheme()) && categoryCache.containsKey(location.toString())) { |
| result = categoryCache.get(location.toString()).get(); |
| if (result != null) |
| return result; |
| //else soft reference has been cleared, take it out of the cache |
| categoryCache.remove(location.toString()); |
| } |
| |
| InputStream input = null; |
| File siteFile = loadActualSiteFile(location, location, transport, monitor); |
| try { |
| CategoryParser siteParser = new CategoryParser(location); |
| Checksum checksum = new CRC32(); |
| input = new CheckedInputStream(new BufferedInputStream(new FileInputStream(siteFile)), checksum); |
| SiteModel siteModel = siteParser.parse(input); |
| String checksumString = Long.toString(checksum.getValue()); |
| result = new UpdateSite(siteModel, location, transport, checksumString); |
| if (!PROTOCOL_FILE.equals(location.getScheme())) |
| categoryCache.put(location.toString(), new SoftReference<UpdateSite>(result)); |
| return result; |
| } catch (SAXException e) { |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, msg, e)); |
| } catch (IOException e) { |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, msg, e)); |
| } finally { |
| try { |
| if (input != null) |
| input.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| if (!PROTOCOL_FILE.equals(location.getScheme())) |
| siteFile.delete(); |
| } |
| } |
| |
| /* |
| * Load and return an update site object from the given location. |
| */ |
| public static synchronized UpdateSite load(URI location, Transport transport, IProgressMonitor monitor) throws ProvisionException { |
| if (location == null) |
| return null; |
| |
| UpdateSite result = null; |
| //only caching remote sites |
| if (!PROTOCOL_FILE.equals(location.getScheme()) && siteCache.containsKey(location.toString())) { |
| result = siteCache.get(location.toString()).get(); |
| if (result != null) |
| return result; |
| //else soft reference has been cleared, take it out of the cache |
| siteCache.remove(location.toString()); |
| } |
| |
| InputStream input = null; |
| File siteFile = loadActualSiteFile(location, getSiteURI(location), transport, monitor); |
| try { |
| DefaultSiteParser siteParser = new DefaultSiteParser(location); |
| Checksum checksum = new CRC32(); |
| input = new CheckedInputStream(new BufferedInputStream(new FileInputStream(siteFile)), checksum); |
| SiteModel siteModel = siteParser.parse(input); |
| String checksumString = Long.toString(checksum.getValue()); |
| result = new UpdateSite(siteModel, getSiteURI(location), transport, checksumString); |
| if (!PROTOCOL_FILE.equals(location.getScheme())) |
| siteCache.put(location.toString(), new SoftReference<UpdateSite>(result)); |
| return result; |
| } catch (SAXException e) { |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, msg, e)); |
| } catch (IOException e) { |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, msg, e)); |
| } finally { |
| try { |
| if (input != null) |
| input.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| if (!PROTOCOL_FILE.equals(location.getScheme())) |
| siteFile.delete(); |
| } |
| } |
| |
| /** |
| * Returns a local file containing the contents of the update site at the given location. |
| */ |
| private static File loadActualSiteFile(URI location, URI actualLocation, Transport transport, IProgressMonitor monitor) throws ProvisionException { |
| SubMonitor submonitor = SubMonitor.convert(monitor, 1000); |
| try { |
| File siteFile = null; |
| IStatus transferResult = null; |
| boolean deleteSiteFile = false; |
| try { |
| if (PROTOCOL_FILE.equals(actualLocation.getScheme())) { |
| siteFile = URIUtil.toFile(actualLocation); |
| if (siteFile.exists()) |
| transferResult = Status.OK_STATUS; |
| else { |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| transferResult = new Status(IStatus.ERROR, Activator.ID, ProvisionException.ARTIFACT_NOT_FOUND, msg, new FileNotFoundException(siteFile.getAbsolutePath())); |
| } |
| } else { |
| // creating a temp file. In the event of an error we want to delete it. |
| deleteSiteFile = true; |
| OutputStream destination = null; |
| try { |
| siteFile = File.createTempFile("site", ".xml"); //$NON-NLS-1$//$NON-NLS-2$ |
| destination = new BufferedOutputStream(new FileOutputStream(siteFile)); |
| } catch (IOException e) { |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.INTERNAL_ERROR, "Can not create tempfile for site.xml", e)); //$NON-NLS-1$ |
| } |
| try { |
| transferResult = transport.download(actualLocation, destination, submonitor.newChild(999)); |
| } finally { |
| try { |
| destination.close(); |
| } catch (IOException e) { |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.INTERNAL_ERROR, "Failing to close tempfile for site.xml", e)); //$NON-NLS-1$ |
| } |
| } |
| } |
| if (monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| if (transferResult.isOK()) { |
| // successful. If the siteFile is the download of a remote site.xml it will get cleaned up later |
| deleteSiteFile = false; |
| return siteFile; |
| } |
| |
| // The transferStatus from download has a well formatted message that should |
| // be used as it contains useful feedback to the user. |
| // The only thing needed is to translate the error code ARTIFACT_NOT_FOUND to |
| // REPOSITORY_NOT_FOUND as the download does not know what the file represents. |
| // |
| IStatus ms = null; |
| if (transferResult.getException() instanceof FileNotFoundException) |
| ms = new MultiStatus(Activator.ID, // |
| ProvisionException.REPOSITORY_NOT_FOUND, |
| // (code == ProvisionException.ARTIFACT_NOT_FOUND || code == ProvisionException.REPOSITORY_NOT_FOUND ? ProvisionException.REPOSITORY_NOT_FOUND : ProvisionException.REPOSITORY_FAILED_READ), // |
| new IStatus[] {transferResult}, // |
| NLS.bind(Messages.ErrorReadingSite, location), null); |
| else |
| ms = transferResult; |
| throw new ProvisionException(ms); |
| |
| } finally { |
| if (deleteSiteFile && siteFile != null) |
| siteFile.delete(); |
| } |
| } finally { |
| if (monitor != null) |
| monitor.done(); |
| } |
| } |
| |
| /* |
| * Parse the feature.xml specified by the given input stream and return the feature object. |
| * In case of failure, the failure is logged and null is returned |
| */ |
| private Feature parseFeature(FeatureParser featureParser, URI featureURI, IProgressMonitor monitor) { |
| File featureFile = null; |
| if (PROTOCOL_FILE.equals(featureURI.getScheme())) { |
| featureFile = URIUtil.toFile(featureURI); |
| return featureParser.parse(featureFile); |
| } |
| try { |
| featureFile = File.createTempFile(FEATURE_TEMP_FILE, JAR_EXTENSION); |
| IStatus transferResult = null; |
| //try the download twice in case of transient network problems |
| for (int i = 0; i < RETRY_COUNT; i++) { |
| if (monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| OutputStream destination = new BufferedOutputStream(new FileOutputStream(featureFile)); |
| try { |
| transferResult = transport.download(featureURI, destination, monitor); |
| } finally { |
| try { |
| destination.close(); |
| } catch (IOException e) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, featureURI), e)); |
| return null; |
| } |
| } |
| if (transferResult.isOK()) |
| break; |
| } |
| if (monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| if (!transferResult.isOK()) { |
| LogHelper.log(new ProvisionException(transferResult)); |
| return null; |
| } |
| return featureParser.parse(featureFile); |
| } catch (IOException e) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, featureURI), e)); |
| } finally { |
| if (featureFile != null) |
| featureFile.delete(); |
| } |
| return null; |
| } |
| |
| /* |
| * Constructor for the class. |
| */ |
| private UpdateSite(SiteModel site, URI location, Transport transport, String checksum) { |
| super(); |
| this.site = site; |
| this.location = location; |
| this.checksum = checksum; |
| this.rootLocation = getRootLocation(); |
| this.transport = transport; |
| } |
| |
| private URI getRootLocation() { |
| String locationString = location.toString(); |
| int slashIndex = locationString.lastIndexOf('/'); |
| if (slashIndex == -1 || slashIndex == (locationString.length() - 1)) |
| return location; |
| |
| return URI.create(locationString.substring(0, slashIndex + 1)); |
| } |
| |
| /* |
| * Iterate over the archive entries in this site and return the matching URI string for |
| * the given identifier, if there is one. |
| */ |
| private URI getArchiveURI(URI base, String identifier) { |
| URLEntry[] archives = site.getArchives(); |
| for (int i = 0; archives != null && i < archives.length; i++) { |
| URLEntry entry = archives[i]; |
| if (identifier.equals(entry.getAnnotation())) |
| return internalGetURI(base, entry.getURL()); |
| } |
| return null; |
| } |
| |
| /* |
| * Return the checksum for this site. |
| */ |
| public String getChecksum() { |
| return checksum; |
| } |
| |
| /* |
| * Return a URI which represents the location of the given feature. |
| */ |
| public URI getSiteFeatureURI(SiteFeature siteFeature) { |
| URL url = siteFeature.getURL(); |
| try { |
| if (url != null) |
| return URIUtil.toURI(url); |
| } catch (URISyntaxException e) { |
| //fall through and resolve the URI ourselves |
| } |
| URI base = getBaseURI(); |
| String featureURIString = siteFeature.getURLString(); |
| return internalGetURI(base, featureURIString); |
| } |
| |
| /* |
| * Return a URI which represents the location of the given feature. |
| */ |
| public URI getFeatureURI(String id, String version) { |
| SiteFeature[] entries = site.getFeatures(); |
| for (int i = 0; i < entries.length; i++) { |
| if (id.equals(entries[i].getFeatureIdentifier()) && version.equals(entries[i].getFeatureVersion())) { |
| return getSiteFeatureURI(entries[i]); |
| } |
| } |
| |
| URI base = getBaseURI(); |
| URI url = getArchiveURI(base, FEATURE_DIR + id + VERSION_SEPARATOR + version + JAR_EXTENSION); |
| if (url != null) |
| return url; |
| return URIUtil.append(base, FEATURE_DIR + id + VERSION_SEPARATOR + version + JAR_EXTENSION); |
| } |
| |
| /* |
| * Return the location of this site. |
| */ |
| public URI getLocation() { |
| return location; |
| } |
| |
| public String getMirrorsURI() { |
| //copy mirror information from update site to p2 repositories |
| String mirrors = site.getMirrorsURI(); |
| if (mirrors == null) |
| return null; |
| //remove site.xml file reference |
| int index = mirrors.indexOf("site.xml"); //$NON-NLS-1$ |
| if (index != -1) |
| mirrors = mirrors.substring(0, index) + mirrors.substring(index + "site.xml".length()); //$NON-NLS-1$ |
| return mirrors; |
| } |
| |
| /* |
| * Return a URI which represents the location of the given plug-in. |
| */ |
| public URI getPluginURI(FeatureEntry plugin) { |
| URI base = getBaseURI(); |
| String path = PLUGIN_DIR + plugin.getId() + VERSION_SEPARATOR + plugin.getVersion() + JAR_EXTENSION; |
| URI url = getArchiveURI(base, path); |
| if (url != null) |
| return url; |
| return URIUtil.append(base, path); |
| } |
| |
| private URI getBaseURI() { |
| URI base = null; |
| String siteURIString = site.getLocationURIString(); |
| if (siteURIString != null) { |
| if (!siteURIString.endsWith("/")) //$NON-NLS-1$ |
| siteURIString += "/"; //$NON-NLS-1$ |
| base = internalGetURI(rootLocation, siteURIString); |
| } |
| if (base == null) |
| base = rootLocation; |
| return base; |
| } |
| |
| /* |
| * Return the site model. |
| */ |
| public SiteModel getSite() { |
| return site; |
| } |
| |
| /* |
| * The trailing parameter can be either null, relative or absolute. If it is null, |
| * then return null. If it is absolute, then create a new url and return it. If it is |
| * relative, then make it relative to the given base url. |
| */ |
| private URI internalGetURI(URI base, String trailing) { |
| if (trailing == null) |
| return null; |
| return URIUtil.makeAbsolute(URI.create(trailing), base); |
| } |
| |
| /* |
| * Load and return the features references in this update site. |
| */ |
| public synchronized Feature[] loadFeatures(IProgressMonitor monitor) throws ProvisionException { |
| if (!featureCache.isEmpty()) |
| return featureCache.values().toArray(new Feature[featureCache.size()]); |
| Feature[] result = loadFeaturesFromDigest(monitor); |
| return result == null ? loadFeaturesFromSite(monitor) : result; |
| } |
| |
| /* |
| * Try and load the feature information from the update site's |
| * digest file, if it exists. |
| */ |
| private Feature[] loadFeaturesFromDigest(IProgressMonitor monitor) { |
| File digestFile = null; |
| boolean local = false; |
| try { |
| URI digestURI = getDigestURI(); |
| if (PROTOCOL_FILE.equals(digestURI.getScheme())) { |
| digestFile = URIUtil.toFile(digestURI); |
| if (!digestFile.exists()) |
| return null; |
| local = true; |
| } else { |
| digestFile = File.createTempFile("digest", ".zip"); //$NON-NLS-1$ //$NON-NLS-2$ |
| BufferedOutputStream destination = new BufferedOutputStream(new FileOutputStream(digestFile)); |
| IStatus result = null; |
| try { |
| result = transport.download(digestURI, destination, monitor); |
| } finally { |
| try { |
| destination.close(); |
| } catch (IOException e) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, location), e)); |
| return null; |
| } |
| } |
| if (result.getSeverity() == IStatus.CANCEL || monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| if (!result.isOK()) |
| return null; |
| } |
| Feature[] features = new DigestParser().parse(digestFile, digestURI); |
| if (features == null) |
| return null; |
| Map<String, Feature> tmpFeatureCache = new HashMap<String, Feature>(features.length); |
| for (int i = 0; i < features.length; i++) { |
| String key = features[i].getId() + VERSION_SEPARATOR + features[i].getVersion(); |
| tmpFeatureCache.put(key, features[i]); |
| } |
| featureCache = tmpFeatureCache; |
| return features; |
| } catch (FileNotFoundException fnfe) { |
| // we do not track FNF exceptions as we will fall back to the |
| // standard feature parsing from the site itself, see bug 225587. |
| } catch (IOException e) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingDigest, location), e)); |
| } finally { |
| if (!local && digestFile != null) |
| digestFile.delete(); |
| } |
| return null; |
| } |
| |
| private URI getDigestURI() { |
| URI digestBase = null; |
| String digestURIString = site.getDigestURIString(); |
| if (digestURIString != null) { |
| if (!digestURIString.endsWith("/")) //$NON-NLS-1$ |
| digestURIString += "/"; //$NON-NLS-1$ |
| digestBase = internalGetURI(rootLocation, digestURIString); |
| } |
| |
| if (digestBase == null) |
| digestBase = rootLocation; |
| |
| return URIUtil.append(digestBase, "digest.zip"); //$NON-NLS-1$ |
| } |
| |
| /* |
| * Load and return the features that are referenced by this update site. Note this |
| * requires downloading and parsing the feature manifest locally. |
| */ |
| private Feature[] loadFeaturesFromSite(IProgressMonitor monitor) throws ProvisionException { |
| SiteFeature[] siteFeatures = site.getFeatures(); |
| FeatureParser featureParser = new FeatureParser(); |
| Map<String, Feature> tmpFeatureCache = new HashMap<String, Feature>(siteFeatures.length); |
| |
| for (int i = 0; i < siteFeatures.length; i++) { |
| if (monitor.isCanceled()) { |
| throw new OperationCanceledException(); |
| } |
| SiteFeature siteFeature = siteFeatures[i]; |
| String key = null; |
| if (siteFeature.getFeatureIdentifier() != null && siteFeature.getFeatureVersion() != null) { |
| key = siteFeature.getFeatureIdentifier() + VERSION_SEPARATOR + siteFeature.getFeatureVersion(); |
| if (tmpFeatureCache.containsKey(key)) |
| continue; |
| } |
| URI featureURI = getSiteFeatureURI(siteFeature); |
| Feature feature = parseFeature(featureParser, featureURI, new NullProgressMonitor()); |
| if (feature == null) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, featureURI))); |
| } else { |
| if (key == null) { |
| siteFeature.setFeatureIdentifier(feature.getId()); |
| siteFeature.setFeatureVersion(feature.getVersion()); |
| key = siteFeature.getFeatureIdentifier() + VERSION_SEPARATOR + siteFeature.getFeatureVersion(); |
| } |
| tmpFeatureCache.put(key, feature); |
| loadIncludedFeatures(feature, featureParser, tmpFeatureCache, monitor); |
| } |
| } |
| featureCache = tmpFeatureCache; |
| return featureCache.values().toArray(new Feature[featureCache.size()]); |
| } |
| |
| /* |
| * Load the features that are included by the given feature. |
| */ |
| private void loadIncludedFeatures(Feature feature, FeatureParser featureParser, Map<String, Feature> features, IProgressMonitor monitor) throws ProvisionException { |
| FeatureEntry[] featureEntries = feature.getEntries(); |
| for (int i = 0; i < featureEntries.length; i++) { |
| if (monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| FeatureEntry entry = featureEntries[i]; |
| if (entry.isRequires() || entry.isPlugin()) |
| continue; |
| String key = entry.getId() + VERSION_SEPARATOR + entry.getVersion(); |
| if (features.containsKey(key)) |
| continue; |
| |
| URI includedFeatureURI = getFeatureURI(entry.getId(), entry.getVersion()); |
| Feature includedFeature = parseFeature(featureParser, includedFeatureURI, monitor); |
| if (includedFeature == null) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, includedFeatureURI))); |
| } else { |
| features.put(key, includedFeature); |
| loadIncludedFeatures(includedFeature, featureParser, features, monitor); |
| } |
| } |
| } |
| } |