Java tutorial
/* * Copyright 2007 Alin Dreghiciu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. * * See the License for the specific language governing permissions and * limitations under the License. */ package org.ops4j.pax.url.mvn.internal; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; import org.ops4j.lang.NullArgumentException; import org.ops4j.net.URLUtils; import org.ops4j.pax.url.maven.commons.MavenConfiguration; import org.ops4j.pax.url.maven.commons.MavenRepositoryURL; import org.ops4j.util.xml.XmlUtils; /** * An URLConnextion that supports mvn: protocol.<br/> * Syntax:<br> * mvn:[repository_url!]groupId/artifactId[/version[/type]]<br/> * where:<br/> * - repository_url = an url that points to a maven 2 repository; optional, if not sepecified the repositories are * resolved based on the repository/localRepository.<br/> * - groupId = group id of maven artifact; mandatory<br/> * - artifactId = artifact id of maven artifact; mandatory<br/> * - version = version of maven artifact; optional, if not specified uses LATEST and will try to resolve the version * from available maven metadata. If version is a SNAPSHOT version, SNAPSHOT will be resolved from available maven * metadata<br/> * - type = type of maven artifact; optional, if not specified uses JAR<br/> * Examples:<br> * mvn:http://repository.ops4j.org/mvn-releases!org.ops4j.pax.runner/runner/0.4.0 - an artifact from an http repository<br/> * mvn:http://user:password@repository.ops4j.org/mvn-releases!org.ops4j.pax.runner/runner/0.4.0 - an artifact from an http * repository with authentication<br/> * mvn:file://c:/localRepo!org.ops4j.pax.runner/runner/0.4.0 - an artifact from a directory<br/> * mvn:jar:file://c:/repo.zip!/repository!org.ops4j.pax.runner/runner/0.4.0 - an artifact from a zip file<br/> * mvn:org.ops4j.pax.runner/runner/0.4.0 - an artifact that will be resolved based on the configured repositories<br/> * <br/> * The service can be configured in two ways: via configuration admin if available and via framework/system properties * where the configuration via config admin has priority.<br/> * Service configuration:<br/> * - org.ops4j.pax.url.mvn.settings = the path to settings.xml;<br/> * - org.ops4j.pax.url.mvn.localRepository = the path to local repository directory;<br> * - org.ops4j.pax.url.mvn.repository = a comma separated list for repositories urls;<br/> * - org.ops4j.pax.url.mvn.certicateCheck = true/false if the SSL certificate check should be done. * Default false. * * @author Alin Dreghiciu * @since August 10, 2007 */ public class Connection extends URLConnection { /** * Logger. */ private static final Log LOG = LogFactory.getLog(Connection.class); /** * 2 spacess indent; */ private static final String Ix2 = " "; /** * 4 spacess indent; */ private static final String Ix4 = " "; /** * Parsed url. */ private Parser m_parser; /** * Service configuration. */ private final MavenConfiguration m_configuration; /** * Creates a new connection. * * @param url the url; cannot be null. * @param configuration service configuration; cannot be null * * @throws MalformedURLException in case of a malformed url */ public Connection(final URL url, final MavenConfiguration configuration) throws MalformedURLException { super(url); NullArgumentException.validateNotNull(url, "URL cannot be null"); NullArgumentException.validateNotNull(configuration, "Service configuration"); m_configuration = configuration; m_parser = new Parser(url.getPath()); } /** * Does nothing. * * @see java.net.URLConnection#connect() */ @Override public void connect() { // do nothing } /** * Returns the input stream denoted by the url.<br/> * If the url does not contain a repository the resource is searched in every repository if available, in the order * provided by the repository setting. * * @return the input stream for the resource denoted by url * * @throws IOException in case of an exception during accessing the resource * @see java.net.URLConnection#getInputStream() */ @Override public InputStream getInputStream() throws IOException { connect(); LOG.debug("Resolving [" + url.toExternalForm() + "]"); final Set<DownloadableArtifact> defaultDownloadables = collectDefaultPossibleDownloads(); if (LOG.isTraceEnabled()) { LOG.trace("Possible default download locations for [" + url.toExternalForm() + "]"); for (DownloadableArtifact artifact : defaultDownloadables) { LOG.trace(" " + artifact); } } for (DownloadableArtifact artifact : defaultDownloadables) { LOG.trace("Downloading [" + artifact + "]"); try { m_configuration.enableProxy(artifact.getArtifactURL()); return artifact.getInputStream(); } catch (IOException ignore) { // go on with next repository LOG.debug(Ix2 + "Could not download [" + artifact + "]"); LOG.trace(Ix2 + "Reason [" + ignore.getClass().getName() + ": " + ignore.getMessage() + "]"); } } final Set<DownloadableArtifact> downloadables = collectPossibleDownloads(); if (LOG.isTraceEnabled()) { LOG.trace("Possible download locations for [" + url.toExternalForm() + "]"); for (DownloadableArtifact artifact : downloadables) { LOG.trace(" " + artifact); } } for (DownloadableArtifact artifact : downloadables) { LOG.trace("Downloading [" + artifact + "]"); try { m_configuration.enableProxy(artifact.getArtifactURL()); return artifact.getInputStream(); } catch (IOException ignore) { // go on with next repository LOG.debug(Ix2 + "Could not download [" + artifact + "]"); LOG.trace(Ix2 + "Reason [" + ignore.getClass().getName() + ": " + ignore.getMessage() + "]"); } } // no artifact found throw new RuntimeException("URL [" + url.toExternalForm() + "] could not be resolved."); } /** * Searches all available repositories for possible artifacts to download. The returned set of downloadable * artifacts (never null, but maybe empty) will be sorted descending by version of the artifact and by positon of * repository in the list of repositories to be searched. * * @return a non null sorted set of artifacts * * @throws java.net.MalformedURLException re-thrown */ private Set<DownloadableArtifact> collectPossibleDownloads() throws MalformedURLException { final List<MavenRepositoryURL> repositories = new ArrayList<MavenRepositoryURL>(); repositories.addAll(m_configuration.getRepositories()); // if the url contains a prefered repository add that repository as the first repository to be searched if (m_parser.getRepositoryURL() != null) { repositories.add(repositories.size() == 0 ? 0 : 1, m_parser.getRepositoryURL()); } return doCollectPossibleDownloads(repositories); } /** * Search the default repositories for possible artifacts to download. */ private Set<DownloadableArtifact> collectDefaultPossibleDownloads() throws MalformedURLException { return doCollectPossibleDownloads(m_configuration.getDefaultRepositories()); } private Set<DownloadableArtifact> doCollectPossibleDownloads(final List<MavenRepositoryURL> repositories) throws MalformedURLException { final Set<DownloadableArtifact> downloadables = new TreeSet<DownloadableArtifact>(new DownloadComparator()); // find artifact type final boolean isLatest = m_parser.getVersion().contains("LATEST"); final boolean isSnapshot = m_parser.getVersion().endsWith("SNAPSHOT"); VersionRange versionRange = null; if (!isLatest && !isSnapshot) { try { versionRange = new VersionRange(m_parser.getVersion()); } catch (Exception ignore) { // well, we do not have a range of versions } } final boolean isVersionRange = versionRange != null; final boolean isExactVersion = !(isLatest || isSnapshot || isVersionRange); int priority = 0; for (MavenRepositoryURL repositoryURL : repositories) { LOG.debug("Collecting versions from repository [" + repositoryURL + "]"); priority++; try { if (isExactVersion) { downloadables.add(resolveExactVersion(repositoryURL, priority)); } else if (isSnapshot) { final DownloadableArtifact snapshot = resolveSnapshotVersion(repositoryURL, priority, m_parser.getVersion()); downloadables.add(snapshot); // if we have a local built snapshot we skip the rest of repositories if (snapshot.isLocalSnapshotBuild()) { break; } } else { final Document metadata = getMetadata(repositoryURL.getURL(), new String[] { m_parser.getArtifactLocalMetdataPath(), m_parser.getArtifactMetdataPath() }); if (isLatest) { downloadables.add(resolveLatestVersion(metadata, repositoryURL, priority)); } else { downloadables.addAll(resolveRangeVersions(metadata, repositoryURL, priority, versionRange)); } } } catch (IOException ignore) { // if metadata cannot be found we go on with the next repository. Maybe we have better luck. LOG.debug(Ix2 + "Skipping repository [" + repositoryURL + "], reason: " + ignore.getMessage()); } } return downloadables; } /** * Returns maven metadata by looking first for a local metatdata xml file and then for a remote one. * If no metadata file is found or cannot be used an IOException is thrown. * * @param repositoryURL url of the repository from where the metadata should be parsed * @param metadataLocations array of location paths to try as metadata * * @return parsed xml document for the metadata file * * @throws java.io.IOException if: * metadata file cannot be located */ private Document getMetadata(final URL repositoryURL, final String[] metadataLocations) throws IOException { LOG.debug(Ix2 + "Resolving metadata"); InputStream inputStream = null; String foundLocation = null; for (String location : metadataLocations) { try { // first try to get the artifact local metadata inputStream = prepareInputStream(repositoryURL, location); // get out at first found location foundLocation = location; LOG.trace(Ix4 + "Metadata found: [" + location + "]"); break; } catch (IOException ignore) { LOG.trace(Ix4 + "Metadata not found: [" + location + "]"); } } if (inputStream == null) { throw new IOException("Metadata not found in repository [" + repositoryURL + "]"); } try { return XmlUtils.parseDoc(inputStream); } catch (ParserConfigurationException e) { throw initIOException("Metadata [" + foundLocation + "] could not be parsed.", e); } catch (SAXException e) { throw initIOException("Metadata [" + foundLocation + "] could not be parsed.", e); } } /** * Returns a downloadable artifact where the version is fully specified. * * @param repositoryURL the url of the repository to download from * @param priority repository priority * * @return a downloadable artifact * * @throws IOException re-thrown */ private DownloadableArtifact resolveExactVersion(final MavenRepositoryURL repositoryURL, final int priority) throws IOException { if (!repositoryURL.isReleasesEnabled()) { throw new IOException("Releases not enabled"); } LOG.debug(Ix2 + "Resolving exact version"); return new DownloadableArtifact(m_parser.getVersion(), priority, repositoryURL.getURL(), m_parser.getArtifactPath(), false, // no local built snapshot m_configuration.getCertificateCheck()); } /** * Resolves the latest version of the artifact. * * @param metadata parsed metadata xml * @param repositoryURL the url of the repository to download from * @param priority repository priority * * @return a downloadable artifact or throw an IOException if latest version cannot be determined. * * @throws IOException if the artifact could not be resolved */ private DownloadableArtifact resolveLatestVersion(final Document metadata, final MavenRepositoryURL repositoryURL, final int priority) throws IOException { LOG.debug(Ix2 + "Resolving latest version"); final String version = XmlUtils.getTextContentOfElement(metadata, "versioning/versions/version[last]"); if (version != null) { if (version.endsWith("SNAPSHOT")) { return resolveSnapshotVersion(repositoryURL, priority, version); } else { return new DownloadableArtifact(version, priority, repositoryURL.getURL(), m_parser.getArtifactPath(version), false, // no local built snapshot m_configuration.getCertificateCheck()); } } throw new IOException("LATEST version could not be resolved."); } /** * Resolves snapshot version of the artifact. * Snapshot versions are resolved by parsing the metadata within the directory that contains the version as: * 1. if the metadata containes entries like "versioning/snapshot/timestamp (most likely on remote repos) it will * use the timestamp and buildnumber to point the real version * 2. if the metatdata does not contain the above (most likely a local repo) it will use as version the * versioning/lastUpdated * * @param repositoryURL the url of the repository to download from * @param priority repository priority * @param version snapshot version to resolve * * @return an input stream to the artifact * * @throws IOException if the artifact could not be resolved */ private DownloadableArtifact resolveSnapshotVersion(final MavenRepositoryURL repositoryURL, final int priority, final String version) throws IOException { if (!repositoryURL.isSnapshotsEnabled()) { throw new IOException("Snapshots not enabled"); } LOG.debug(Ix2 + "Resolving snapshot version [" + version + "]"); try { final Document snapshotMetadata = getMetadata(repositoryURL.getURL(), new String[] { m_parser.getVersionLocalMetadataPath(version), m_parser.getVersionMetadataPath(version) }); final String timestamp = XmlUtils.getTextContentOfElement(snapshotMetadata, "versioning/snapshot/timestamp"); final String buildNumber = XmlUtils.getTextContentOfElement(snapshotMetadata, "versioning/snapshot/buildNumber"); final String localSnapshot = XmlUtils.getTextContentOfElement(snapshotMetadata, "versioning/snapshot/localCopy"); if (timestamp != null && buildNumber != null) { return new DownloadableArtifact(m_parser.getSnapshotVersion(version, timestamp, buildNumber), priority, repositoryURL.getURL(), m_parser.getSnapshotPath(version, timestamp, buildNumber), localSnapshot != null, m_configuration.getCertificateCheck()); } else { String lastUpdated = XmlUtils.getTextContentOfElement(snapshotMetadata, "versioning/lastUpdated"); if (lastUpdated != null) { // last updated should contain in the first 8 chars the date and then the time, // fact that is not compatible with timeStamp from remote repos which has a "." after date if (lastUpdated.length() > 8) { lastUpdated = lastUpdated.substring(0, 8) + "." + lastUpdated.substring(8); return new DownloadableArtifact(m_parser.getSnapshotVersion(version, lastUpdated, "0"), priority, repositoryURL.getURL(), m_parser.getArtifactPath(version), localSnapshot != null, m_configuration.getCertificateCheck()); } } } } catch (IOException ignore) { // in this case we could not find any metadata so try to get the *-SNAPSHOT file directly } return new DownloadableArtifact(m_parser.getVersion(), priority, repositoryURL.getURL(), m_parser.getArtifactPath(), false, // no local built snapshot m_configuration.getCertificateCheck()); } /** * Resolves all versions that fits the provided range. * * @param metadata parsed metadata xml * @param repositoryURL the url of the repository to download from * @param priority repository priority * @param versionRange version range to fulfill * * @return list of downloadable artifacts that match the range * * @throws IOException re-thrown */ private List<DownloadableArtifact> resolveRangeVersions(final Document metadata, final MavenRepositoryURL repositoryURL, final int priority, final VersionRange versionRange) throws IOException { LOG.debug(Ix2 + "Resolving versions in range [" + versionRange + "]"); final List<DownloadableArtifact> downladables = new ArrayList<DownloadableArtifact>(); final List<Element> elements = XmlUtils.getElements(metadata, "versioning/versions/version"); if (elements != null && elements.size() > 0) { for (Element element : elements) { final String versionString = XmlUtils.getTextContent(element); if (versionString != null) { final Version version = new Version(versionString); if (versionRange.includes(version)) { if (versionString.endsWith("SNAPSHOT")) { downladables.add(resolveSnapshotVersion(repositoryURL, priority, versionString)); } else { downladables.add(new DownloadableArtifact(versionString, priority, repositoryURL.getURL(), m_parser.getArtifactPath(versionString), false, // no local built snapshot m_configuration.getCertificateCheck())); } } } } } return downladables; } /** * @param repositoryURL url to reporsitory * @param path a path to the artifact jar file * * @return prepared input stream * * @throws IOException re-thrown * @see org.ops4j.net.URLUtils#prepareInputStream(java.net.URL,boolean) */ private InputStream prepareInputStream(URL repositoryURL, final String path) throws IOException { String repository = repositoryURL.toExternalForm(); if (!repository.endsWith(Parser.FILE_SEPARATOR)) { repository = repository + Parser.FILE_SEPARATOR; } m_configuration.enableProxy(repositoryURL); final URL url = new URL(repository + path); LOG.trace("Reading " + url.toExternalForm()); return URLUtils.prepareInputStream(url, !m_configuration.getCertificateCheck()); } /** * Creates an IOException with a message and a cause. * * @param message exception message * @param cause exception cause * * @return the created IO Exception */ private IOException initIOException(final String message, final Exception cause) { IOException exception = new IOException(message); exception.initCause(cause); return exception; } /** * Sorting comparator for downladable artifacts. * The sorting is done by: * 1. descending version * 2. ascending priority. */ private static class DownloadComparator implements Comparator<DownloadableArtifact> { public int compare(final DownloadableArtifact first, final DownloadableArtifact second) { // first descending by version int result = -1 * first.getVersion().compareTo(second.getVersion()); if (result == 0) { // then ascending by priority if (first.getPriority() < second.getPriority()) { result = -1; } else if (first.getPriority() > second.getPriority()) { result = 1; } } return result; } } }