| /******************************************************************************* |
| * Copyright (c) 2008 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 |
| *******************************************************************************/ |
| package org.eclipse.equinox.internal.p2.updatesite; |
| |
| import java.io.*; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| 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.core.helpers.URLUtil; |
| import org.eclipse.equinox.internal.p2.publisher.eclipse.FeatureParser; |
| import org.eclipse.equinox.internal.provisional.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 DIR_SEPARATOR = "/"; //$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 URL location; |
| private SiteModel site; |
| |
| /* |
| * Some variables for caching. |
| */ |
| // map of String (URL.toExternalForm()) to UpdateSite |
| private static Map siteCache = new HashMap(); |
| // map of String (featureID_featureVersion) to Feature |
| private Map featureCache = new HashMap(); |
| |
| /* |
| * Return a new URL for the given file which is based from the specified root. |
| */ |
| public static URL getFileURL(URL root, String fileName) throws MalformedURLException { |
| String path = root.getPath(); |
| if (path.endsWith(fileName)) |
| return root; |
| |
| if (constainsUpdateSiteFileName(path)) |
| return new URL(root, fileName); |
| |
| if (path.endsWith(DIR_SEPARATOR)) |
| return new URL(root.toExternalForm() + fileName); |
| return new URL(root.toExternalForm() + DIR_SEPARATOR + fileName); |
| } |
| |
| /* |
| * Return a URL based on the given URL, which points to a site.xml file. |
| */ |
| private static URL getSiteURL(URL url) throws MalformedURLException { |
| String path = url.getPath(); |
| if (constainsUpdateSiteFileName(path)) |
| return url; |
| |
| if (path.endsWith(DIR_SEPARATOR)) |
| return new URL(url.toExternalForm() + SITE_FILE); |
| return new URL(url.toExternalForm() + DIR_SEPARATOR + SITE_FILE); |
| } |
| |
| private static boolean constainsUpdateSiteFileName(String path) { |
| if (path.endsWith(DOT_XML)) { |
| int lastSlash = path.lastIndexOf('/'); |
| String lastSegment = lastSlash == -1 ? path : path.substring(lastSlash + 1); |
| if (lastSegment.indexOf(SITE) != -1) |
| return true; |
| } |
| return false; |
| } |
| |
| /* |
| * Load and return an update site object from the given location. |
| */ |
| public static synchronized UpdateSite load(URL location, IProgressMonitor monitor) throws ProvisionException { |
| if (location == null) |
| return null; |
| UpdateSite result = (UpdateSite) siteCache.get(location.toExternalForm()); |
| if (result != null) |
| return result; |
| InputStream input = null; |
| File siteFile = loadSiteFile(location, monitor); |
| try { |
| DefaultSiteParser siteParser = new DefaultSiteParser(); |
| 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, getSiteURL(location), checksumString); |
| siteCache.put(location.toExternalForm(), 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.getProtocol())) |
| siteFile.delete(); |
| } |
| } |
| |
| /** |
| * Returns a local file containing the contents of the update site at the given location. |
| */ |
| private static File loadSiteFile(URL location, IProgressMonitor monitor) throws ProvisionException { |
| Throwable failure; |
| File siteFile = null; |
| IStatus transferResult; |
| boolean deleteSiteFile = false; |
| try { |
| URL actualLocation = getSiteURL(location); |
| if (PROTOCOL_FILE.equals(actualLocation.getProtocol())) { |
| siteFile = new File(actualLocation.getPath()); |
| if (siteFile.exists()) |
| transferResult = Status.OK_STATUS; |
| else { |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| transferResult = new Status(IStatus.ERROR, Activator.ID, msg, new FileNotFoundException(siteFile.getAbsolutePath())); |
| } |
| } else { |
| // creating a temp file. In the event of an error we want to delete it. |
| deleteSiteFile = true; |
| siteFile = File.createTempFile("site", ".xml"); //$NON-NLS-1$//$NON-NLS-2$ |
| OutputStream destination = new BufferedOutputStream(new FileOutputStream(siteFile)); |
| transferResult = getTransport().download(actualLocation.toExternalForm(), destination, monitor); |
| } |
| 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; |
| } |
| failure = transferResult.getException(); |
| } catch (IOException e) { |
| failure = e; |
| } finally { |
| if (deleteSiteFile && siteFile != null) |
| siteFile.delete(); |
| } |
| int code = (failure instanceof FileNotFoundException) ? ProvisionException.REPOSITORY_NOT_FOUND : ProvisionException.REPOSITORY_FAILED_READ; |
| String msg = NLS.bind(Messages.ErrorReadingSite, location); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, code, msg, failure)); |
| } |
| |
| /* |
| * 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 static Feature parseFeature(FeatureParser featureParser, URL featureURL, IProgressMonitor monitor) { |
| File featureFile = null; |
| if (PROTOCOL_FILE.equals(featureURL.getProtocol())) { |
| featureFile = new File(featureURL.getPath()); |
| 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)); |
| transferResult = getTransport().download(featureURL.toExternalForm(), destination, monitor); |
| 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, featureURL), e)); |
| } finally { |
| if (featureFile != null) |
| featureFile.delete(); |
| } |
| return null; |
| } |
| |
| /* |
| * Throw an exception if the site pointed to by the given URL is not valid. |
| */ |
| public static void validate(URL url, IProgressMonitor monitor) throws ProvisionException { |
| try { |
| URL siteURL = getSiteURL(url); |
| long lastModified = getTransport().getLastModified(siteURL); |
| if (lastModified == 0) { |
| String msg = NLS.bind(Messages.ErrorReadingSite, url); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, msg, null)); |
| } |
| } catch (MalformedURLException e) { |
| String msg = NLS.bind(Messages.ErrorReadingSite, url); |
| throw new ProvisionException(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_FAILED_READ, msg, e)); |
| } |
| } |
| |
| /* |
| * Constructor for the class. |
| */ |
| private UpdateSite(SiteModel site, URL location, String checksum) { |
| super(); |
| this.site = site; |
| this.location = location; |
| this.checksum = checksum; |
| } |
| |
| /* |
| * Iterate over the archive entries in this site and return the matching URL string for |
| * the given identifier, if there is one. |
| */ |
| private URL getArchiveURL(URL 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 internalGetURL(base, entry.getURL()); |
| } |
| return null; |
| } |
| |
| /* |
| * Return the checksum for this site. |
| */ |
| public String getChecksum() { |
| return checksum; |
| } |
| |
| /* |
| * Return a URL which represents the location of the given feature. |
| */ |
| public URL getSiteFeatureURL(SiteFeature siteFeature) { |
| URL url = siteFeature.getURL(); |
| if (url != null) |
| return url; |
| |
| URL base = getBaseURL(); |
| String featureURLString = siteFeature.getURLString(); |
| return internalGetURL(base, featureURLString); |
| } |
| |
| /* |
| * Return a URL which represents the location of the given feature. |
| */ |
| public URL getFeatureURL(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 getSiteFeatureURL(entries[i]); |
| } |
| } |
| |
| URL base = getBaseURL(); |
| URL url = getArchiveURL(base, FEATURE_DIR + id + VERSION_SEPARATOR + version + JAR_EXTENSION); |
| if (url != null) |
| return url; |
| |
| // fall through to default URL |
| try { |
| return getFileURL(base, FEATURE_DIR + id + VERSION_SEPARATOR + version + JAR_EXTENSION); |
| } catch (MalformedURLException e) { |
| // shouldn't happen |
| } |
| return null; |
| } |
| |
| /* |
| * Return the location of this site. |
| */ |
| public URL getLocation() { |
| return location; |
| } |
| |
| public String getMirrorsURL() { |
| //copy mirror information from update site to p2 repositories |
| String mirrors = site.getMirrorsURL(); |
| 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 URL which represents the location of the given plug-in. |
| */ |
| public URL getPluginURL(FeatureEntry plugin) { |
| URL base = getBaseURL(); |
| String path = PLUGIN_DIR + plugin.getId() + VERSION_SEPARATOR + plugin.getVersion() + JAR_EXTENSION; |
| URL url = getArchiveURL(base, path); |
| if (url != null) |
| return url; |
| try { |
| return getFileURL(base, path); |
| } catch (MalformedURLException e) { |
| // shouldn't happen |
| } |
| return null; |
| } |
| |
| private URL getBaseURL() { |
| URL base = null; |
| String siteURLString = site.getLocationURLString(); |
| if (siteURLString != null) { |
| if (!siteURLString.endsWith("/")) //$NON-NLS-1$ |
| siteURLString += "/"; //$NON-NLS-1$ |
| base = internalGetURL(location, siteURLString); |
| } |
| if (base == null) |
| base = location; |
| 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 URL internalGetURL(URL base, String trailing) { |
| if (trailing == null) |
| return null; |
| try { |
| return new URL(trailing); |
| } catch (MalformedURLException e) { |
| try { |
| return new URL(base, trailing); |
| } catch (MalformedURLException e2) { |
| // shouldn't happen |
| } |
| } |
| return null; |
| } |
| |
| /* |
| * Load and return the features references in this update site. |
| */ |
| public synchronized Feature[] loadFeatures(IProgressMonitor monitor) throws ProvisionException { |
| if (!featureCache.isEmpty()) |
| return (Feature[]) 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 { |
| URL digestURL = getDigestURL(); |
| if (PROTOCOL_FILE.equals(digestURL.getProtocol())) { |
| digestFile = URLUtil.toFile(digestURL); |
| 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 = getTransport().download(digestURL.toExternalForm(), destination, monitor); |
| if (result.getSeverity() == IStatus.CANCEL || monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| if (!result.isOK()) |
| return null; |
| } |
| Feature[] features = new DigestParser().parse(digestFile); |
| if (features == null) |
| return null; |
| Map tmpFeatureCache = new HashMap(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 (MalformedURLException e) { |
| String msg = NLS.bind(Messages.InvalidRepositoryLocation, location); |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, ProvisionException.REPOSITORY_INVALID_LOCATION, msg, e)); |
| } 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 URL getDigestURL() throws MalformedURLException { |
| URL digestBase = location; |
| String digestURLString = site.getDigestURLString(); |
| if (digestURLString != null) { |
| if (!digestURLString.endsWith("/")) //$NON-NLS-1$ |
| digestURLString += "/"; //$NON-NLS-1$ |
| digestBase = internalGetURL(location, digestURLString); |
| } |
| |
| return getFileURL(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 tmpFeatureCache = new HashMap(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; |
| } |
| URL featureURL = getSiteFeatureURL(siteFeature); |
| Feature feature = parseFeature(featureParser, featureURL, monitor); |
| if (feature == null) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, featureURL))); |
| } 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 (Feature[]) 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 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; |
| |
| URL includedFeatureURL = getFeatureURL(entry.getId(), entry.getVersion()); |
| Feature includedFeature = parseFeature(featureParser, includedFeatureURL, monitor); |
| if (includedFeature == null) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.ErrorReadingFeature, includedFeatureURL))); |
| } else { |
| features.put(key, includedFeature); |
| loadIncludedFeatures(includedFeature, featureParser, features, monitor); |
| } |
| } |
| } |
| |
| private static ECFTransport getTransport() { |
| return ECFTransport.getInstance(); |
| } |
| } |