001 /*
002 * JBoss, Home of Professional Open Source.
003 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004 * as indicated by the @author tags. See the copyright.txt file in the
005 * distribution for a full listing of individual contributors.
006 *
007 * This is free software; you can redistribute it and/or modify it
008 * under the terms of the GNU Lesser General Public License as
009 * published by the Free Software Foundation; either version 2.1 of
010 * the License, or (at your option) any later version.
011 *
012 * This software is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this software; if not, write to the Free
019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021 */
022 package org.jboss.dna.maven;
023
024 import java.io.IOException;
025 import java.io.InputStream;
026 import java.net.MalformedURLException;
027 import java.net.URL;
028 import java.net.URLConnection;
029 import java.util.ArrayList;
030 import java.util.Collections;
031 import java.util.EnumSet;
032 import java.util.HashSet;
033 import java.util.Iterator;
034 import java.util.List;
035 import java.util.Set;
036 import javax.jcr.Repository;
037 import javax.xml.parsers.DocumentBuilder;
038 import javax.xml.parsers.DocumentBuilderFactory;
039 import javax.xml.parsers.ParserConfigurationException;
040 import javax.xml.xpath.XPath;
041 import javax.xml.xpath.XPathConstants;
042 import javax.xml.xpath.XPathExpression;
043 import javax.xml.xpath.XPathExpressionException;
044 import javax.xml.xpath.XPathFactory;
045 import org.jboss.dna.common.component.ClassLoaderFactory;
046 import org.jboss.dna.common.util.CheckArg;
047 import org.jboss.dna.common.util.Logger;
048 import org.jboss.dna.common.xml.SimpleNamespaceContext;
049 import org.jboss.dna.maven.spi.JcrMavenUrlProvider;
050 import org.jboss.dna.maven.spi.MavenUrlProvider;
051 import org.w3c.dom.Document;
052 import org.w3c.dom.NodeList;
053 import org.xml.sax.SAXException;
054
055 /**
056 * A Maven 2 repository that can be used to store and access artifacts like JARs and source archives within a running application.
057 * This class understands Maven 2 Project Object Model (POM) files, and thus is able to analyze dependencies and provide a
058 * {@link ClassLoader class loader} that accesses libraries using these transitive dependencies.
059 * <p>
060 * Instances are initialized with an authenticated {@link MavenUrlProvider Maven URL provider}, which is typically a
061 * {@link JcrMavenUrlProvider} instance configured with a {@link Repository JCR Repository} and path to the root of the repository
062 * subtree in that workspace. The repository can either already exist and contain the required artifacts, or it will be created as
063 * artifacts are loaded. Then to use libraries that are in the repository, simply obtain the
064 * {@link #getClassLoader(ClassLoader,MavenId...) class loader} by specifying the {@link MavenId artifact identifiers} for the
065 * libraries used directly by your code. This class loader will add any libraries that are required by those you supply.
066 * </p>
067 * @author Randall Hauch
068 */
069 public class MavenRepository implements ClassLoaderFactory {
070
071 private final MavenUrlProvider urlProvider;
072 private final MavenClassLoaders classLoaders;
073 private final Logger logger;
074
075 public MavenRepository( final MavenUrlProvider urlProvider ) {
076 CheckArg.isNotNull(urlProvider, "urlProvider");
077 this.urlProvider = urlProvider;
078 this.classLoaders = new MavenClassLoaders(this);
079 this.logger = Logger.getLogger(this.getClass());
080 assert this.logger != null;
081 assert this.urlProvider != null;
082 }
083
084 /**
085 * Get a class loader that has as its classpath the JARs for the libraries identified by the supplied IDs. This method always
086 * returns a class loader, even when none of the specified libraries {@link #exists(MavenId) exist} in this repository.
087 * @param parent the parent class loader that will be consulted before any project class loaders; may be null if the
088 * {@link Thread#getContextClassLoader() current thread's context class loader} or the class loader that loaded this class
089 * should be used
090 * @param mavenIds the IDs of the libraries in this Maven repository
091 * @return the class loader
092 * @see #exists(MavenId)
093 * @see #exists(MavenId,MavenId...)
094 * @throws IllegalArgumentException if no Maven IDs are passed in or if any of the IDs are null
095 */
096 public ClassLoader getClassLoader( ClassLoader parent, MavenId... mavenIds ) {
097 CheckArg.isNotEmpty(mavenIds, "mavenIds");
098 CheckArg.containsNoNulls(mavenIds, "mavenIds");
099 return this.classLoaders.getClassLoader(parent, mavenIds);
100 }
101
102 /**
103 * Get a class loader that has as its classpath the JARs for the libraries identified by the supplied IDs. This method always
104 * returns a class loader, even when none of the specified libraries {@link #exists(MavenId) exist} in this repository.
105 * @param coordinates the IDs of the libraries in this Maven repository
106 * @return the class loader
107 * @throws IllegalArgumentException if no coordinates are passed in or if any of the coordinate references is null
108 */
109 public ClassLoader getClassLoader( String... coordinates ) {
110 return getClassLoader(null, coordinates);
111 }
112
113 /**
114 * Get a class loader that has as its classpath the JARs for the libraries identified by the supplied IDs. This method always
115 * returns a class loader, even when none of the specified libraries {@link #exists(MavenId) exist} in this repository.
116 * @param parent the parent class loader that will be consulted before any project class loaders; may be null if the
117 * {@link Thread#getContextClassLoader() current thread's context class loader} or the class loader that loaded this class
118 * should be used
119 * @param coordinates the IDs of the libraries in this Maven repository
120 * @return the class loader
121 * @throws IllegalArgumentException if no coordinates are passed in or if any of the coordinate references is null
122 */
123 public ClassLoader getClassLoader( ClassLoader parent, String... coordinates ) {
124 CheckArg.isNotEmpty(coordinates, "coordinates");
125 CheckArg.containsNoNulls(coordinates, "coordinates");
126 MavenId[] mavenIds = new MavenId[coordinates.length];
127 for (int i = 0; i < coordinates.length; i++) {
128 String coordinate = coordinates[i];
129 mavenIds[i] = new MavenId(coordinate);
130 }
131 return getClassLoader(parent, mavenIds); // parent may be null
132 }
133
134 /**
135 * Determine whether the identified library exists in this Maven repository.
136 * @param mavenId the ID of the library
137 * @return true if this repository contains the library, or false if it does not exist (or the ID is null)
138 * @throws MavenRepositoryException if there is a problem connecting to or using the Maven repository, as configured
139 * @see #exists(MavenId,MavenId...)
140 */
141 public boolean exists( MavenId mavenId ) throws MavenRepositoryException {
142 if (mavenId == null) return false;
143 Set<MavenId> existing = exists(mavenId, (MavenId)null);
144 return existing.contains(mavenId);
145 }
146
147 /**
148 * Determine which of the identified libraries exist in this Maven repository.
149 * @param firstId the first ID of the library to check
150 * @param mavenIds the IDs of the libraries; any null IDs will be ignored
151 * @return the set of IDs for libraries that do exist in this repository; never null
152 * @throws MavenRepositoryException if there is a problem connecting to or using the Maven repository, as configured
153 * @see #exists(MavenId)
154 */
155 public Set<MavenId> exists( MavenId firstId, MavenId... mavenIds ) throws MavenRepositoryException {
156 if (mavenIds == null || mavenIds.length == 0) return Collections.emptySet();
157
158 // Find the set of MavenIds that are not null ...
159 Set<MavenId> nonNullIds = new HashSet<MavenId>();
160 if (firstId != null) nonNullIds.add(firstId);
161 for (MavenId mavenId : mavenIds) {
162 if (mavenId != null) nonNullIds.add(mavenId);
163 }
164 if (nonNullIds.isEmpty()) return nonNullIds;
165
166 MavenId lastMavenId = null;
167 try {
168 for (Iterator<MavenId> iter = nonNullIds.iterator(); iter.hasNext();) {
169 lastMavenId = iter.next();
170 URL urlToMavenId = this.urlProvider.getUrl(lastMavenId, null, null, false);
171 boolean exists = urlToMavenId != null;
172 if (!exists) iter.remove();
173 }
174 } catch (MalformedURLException err) {
175 throw new MavenRepositoryException(MavenI18n.errorCreatingUrlForMavenId.text(lastMavenId, err.getMessage()));
176 }
177 return nonNullIds;
178 }
179
180 /**
181 * Get the dependencies for the Maven project with the specified ID.
182 * <p>
183 * This implementation downloads the POM file for the specified project to extract the dependencies and exclusions.
184 * </p>
185 * @param mavenId the ID of the project; may not be null
186 * @return the list of dependencies
187 * @throws IllegalArgumentException if the MavenId reference is null
188 * @throws MavenRepositoryException if there is a problem finding or reading the POM file given the MavenId
189 */
190 public List<MavenDependency> getDependencies( MavenId mavenId ) {
191 URL pomUrl = null;
192 try {
193 pomUrl = getUrl(mavenId, ArtifactType.POM, null);
194 return getDependencies(mavenId, pomUrl.openStream());
195 } catch (IOException e) {
196 throw new MavenRepositoryException(MavenI18n.errorGettingPomFileForMavenIdAtUrl.text(mavenId, pomUrl), e);
197 }
198 }
199
200 /**
201 * Get the dependencies for the Maven project with the specified ID.
202 * <p>
203 * This implementation downloads the POM file for the specified project to extract the dependencies and exclusions.
204 * </p>
205 * @param mavenId the ID of the Maven project for which the dependencies are to be obtained
206 * @param pomStream the stream to the POM file
207 * @param allowedScopes the set of scopes that are to be allowed in the dependency list; if null, the default scopes of
208 * {@link MavenDependency.Scope#getRuntimeScopes()} are used
209 * @return the list of dependencies; never null
210 * @throws IllegalArgumentException if the MavenId or InputStream references are null
211 * @throws IOException if an error occurs reading the stream
212 * @throws MavenRepositoryException if there is a problem reading the POM file given the supplied stream and MavenId
213 */
214 protected List<MavenDependency> getDependencies( MavenId mavenId, InputStream pomStream, MavenDependency.Scope... allowedScopes ) throws IOException {
215 CheckArg.isNotNull(mavenId, "mavenId");
216 CheckArg.isNotNull(pomStream, "pomStream");
217 EnumSet<MavenDependency.Scope> includedScopes = MavenDependency.Scope.getRuntimeScopes();
218 if (allowedScopes != null && allowedScopes.length > 0) includedScopes = EnumSet.of(allowedScopes[0], allowedScopes);
219 List<MavenDependency> results = new ArrayList<MavenDependency>();
220
221 try {
222 // Use JAXP to load the XML document ...
223 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
224 factory.setNamespaceAware(true); // never forget this!
225 DocumentBuilder builder = factory.newDocumentBuilder();
226 Document doc = builder.parse(pomStream);
227
228 // Create an XPath object ...
229 XPathFactory xpathFactory = XPathFactory.newInstance();
230 XPath xpath = xpathFactory.newXPath();
231 xpath.setNamespaceContext(new SimpleNamespaceContext().setNamespace("m", "http://maven.apache.org/POM/4.0.0"));
232
233 // Set up some XPath expressions ...
234 XPathExpression projectExpression = xpath.compile("//m:project");
235 XPathExpression dependencyExpression = xpath.compile("//m:project/m:dependencies/m:dependency");
236 XPathExpression groupIdExpression = xpath.compile("./m:groupId/text()");
237 XPathExpression artifactIdExpression = xpath.compile("./m:artifactId/text()");
238 XPathExpression versionExpression = xpath.compile("./m:version/text()");
239 XPathExpression classifierExpression = xpath.compile("./m:classifier/text()");
240 XPathExpression scopeExpression = xpath.compile("./m:scope/text()");
241 XPathExpression typeExpression = xpath.compile("./m:type/text()");
242 XPathExpression exclusionExpression = xpath.compile("./m:exclusions/m:exclusion");
243
244 // Extract the Maven ID for this POM file ...
245 org.w3c.dom.Node projectNode = (org.w3c.dom.Node)projectExpression.evaluate(doc, XPathConstants.NODE);
246 String groupId = (String)groupIdExpression.evaluate(projectNode, XPathConstants.STRING);
247 String artifactId = (String)artifactIdExpression.evaluate(projectNode, XPathConstants.STRING);
248 String version = (String)versionExpression.evaluate(projectNode, XPathConstants.STRING);
249 String classifier = (String)classifierExpression.evaluate(projectNode, XPathConstants.STRING);
250 if (groupId == null || artifactId == null || version == null) {
251 throw new IllegalArgumentException(MavenI18n.pomFileIsInvalid.text(mavenId));
252 }
253 MavenId actualMavenId = new MavenId(groupId, artifactId, version, classifier);
254 if (!mavenId.equals(actualMavenId)) {
255 throw new IllegalArgumentException(MavenI18n.pomFileContainsUnexpectedId.text(actualMavenId, mavenId));
256 }
257
258 // Evaluate the XPath expression and iterate over the "dependency" nodes ...
259 NodeList nodes = (NodeList)dependencyExpression.evaluate(doc, XPathConstants.NODESET);
260 for (int i = 0; i < nodes.getLength(); ++i) {
261 org.w3c.dom.Node dependencyNode = nodes.item(i);
262 assert dependencyNode != null;
263 String depGroupId = (String)groupIdExpression.evaluate(dependencyNode, XPathConstants.STRING);
264 String depArtifactId = (String)artifactIdExpression.evaluate(dependencyNode, XPathConstants.STRING);
265 String depVersion = (String)versionExpression.evaluate(dependencyNode, XPathConstants.STRING);
266 String depClassifier = (String)classifierExpression.evaluate(dependencyNode, XPathConstants.STRING);
267 String scopeText = (String)scopeExpression.evaluate(dependencyNode, XPathConstants.STRING);
268 String depType = (String)typeExpression.evaluate(dependencyNode, XPathConstants.STRING);
269
270 // Extract the Maven dependency ...
271 if (depGroupId == null || depArtifactId == null || depVersion == null) {
272 this.logger.trace("Skipping dependency of {1} due to missing groupId, artifactId or version: {2}", mavenId, dependencyNode);
273 continue; // not enough information, so skip
274 }
275 MavenDependency dependency = new MavenDependency(depGroupId, depArtifactId, depVersion, depClassifier);
276 dependency.setType(depType);
277
278 // If the scope is "compile" (default) or "runtime", then we need to process the dependency ...
279 dependency.setScope(scopeText);
280 if (!includedScopes.contains(dependency.getScope())) continue;
281
282 // Find any exclusions ...
283 NodeList exclusionNodes = (NodeList)exclusionExpression.evaluate(dependencyNode, XPathConstants.NODESET);
284 for (int j = 0; j < exclusionNodes.getLength(); ++j) {
285 org.w3c.dom.Node exclusionNode = exclusionNodes.item(j);
286 assert exclusionNode != null;
287 String excludedGroupId = (String)groupIdExpression.evaluate(exclusionNode, XPathConstants.STRING);
288 String excludedArtifactId = (String)artifactIdExpression.evaluate(exclusionNode, XPathConstants.STRING);
289
290 if (excludedGroupId == null || excludedArtifactId == null) {
291 this.logger.trace("Skipping exclusion in dependency of {1} due to missing exclusion groupId or artifactId: {2} ", mavenId, exclusionNode);
292 continue; // not enough information, so skip
293 }
294 MavenId excludedId = new MavenId(excludedGroupId, excludedArtifactId);
295 dependency.getExclusions().add(excludedId);
296 }
297
298 results.add(dependency);
299 }
300 } catch (XPathExpressionException err) {
301 throw new MavenRepositoryException(MavenI18n.errorCreatingXpathStatementsToEvaluatePom.text(mavenId), err);
302 } catch (ParserConfigurationException err) {
303 throw new MavenRepositoryException(MavenI18n.errorCreatingXpathParserToEvaluatePom.text(mavenId), err);
304 } catch (SAXException err) {
305 throw new MavenRepositoryException(MavenI18n.errorReadingXmlDocumentToEvaluatePom.text(mavenId), err);
306 } finally {
307 try {
308 pomStream.close();
309 } catch (IOException e) {
310 this.logger.error(e, MavenI18n.errorClosingUrlStreamToPom, mavenId);
311 }
312 }
313 return results;
314 }
315
316 /**
317 * Get the URL for the artifact with the specified type in the given Maven project. The resulting URL can be used to
318 * {@link URL#openConnection() connect} to the repository to {@link URLConnection#getInputStream() read} or
319 * {@link URLConnection#getOutputStream() write} the artifact's content.
320 * @param mavenId the ID of the Maven project; may not be null
321 * @param artifactType the type of artifact; may be null, but the URL will not be able to be read or written to
322 * @param signatureType the type of signature; may be null if the signature file is not desired
323 * @return the URL to this artifact; never null
324 * @throws MalformedURLException if the supplied information cannot be turned into a valid URL
325 */
326 public URL getUrl( MavenId mavenId, ArtifactType artifactType, SignatureType signatureType ) throws MalformedURLException {
327 return this.urlProvider.getUrl(mavenId, artifactType, signatureType, false);
328 }
329
330 /**
331 * Get the URL for the artifact with the specified type in the given Maven project. The resulting URL can be used to
332 * {@link URL#openConnection() connect} to the repository to {@link URLConnection#getInputStream() read} or
333 * {@link URLConnection#getOutputStream() write} the artifact's content.
334 * @param mavenId the ID of the Maven project; may not be null
335 * @param artifactType the type of artifact; may be null, but the URL will not be able to be read or written to
336 * @param signatureType the type of signature; may be null if the signature file is not desired
337 * @param createIfRequired true if the node structure should be created if any part of it does not exist; this always expects
338 * that the path to the top of the repository tree exists.
339 * @return the URL to this artifact; never null
340 * @throws MalformedURLException if the supplied information cannot be turned into a valid URL
341 */
342 public URL getUrl( MavenId mavenId, ArtifactType artifactType, SignatureType signatureType, boolean createIfRequired ) throws MalformedURLException {
343 return this.urlProvider.getUrl(mavenId, artifactType, signatureType, createIfRequired);
344 }
345
346 /**
347 * This method is called to signal this repository that the POM file for a project has been updated. This method notifies the
348 * associated class loader of the change, which will adapt appropriately.
349 * @param mavenId
350 */
351 protected void notifyUpdatedPom( MavenId mavenId ) {
352 this.classLoaders.notifyChangeInDependencies(mavenId);
353 }
354 }