View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.maven;
25  
26  import org.modeshape.common.component.ClassLoaderFactory;
27  import org.modeshape.common.util.CheckArg;
28  import org.modeshape.common.util.Logger;
29  import org.modeshape.common.xml.SimpleNamespaceContext;
30  import org.modeshape.maven.spi.JcrMavenUrlProvider;
31  import org.modeshape.maven.spi.MavenUrlProvider;
32  import org.w3c.dom.Document;
33  import org.w3c.dom.NodeList;
34  import org.xml.sax.SAXException;
35  
36  import javax.jcr.Repository;
37  import javax.xml.parsers.DocumentBuilder;
38  import javax.xml.parsers.DocumentBuilderFactory;
39  import javax.xml.parsers.ParserConfigurationException;
40  import javax.xml.xpath.XPath;
41  import javax.xml.xpath.XPathConstants;
42  import javax.xml.xpath.XPathExpression;
43  import javax.xml.xpath.XPathExpressionException;
44  import javax.xml.xpath.XPathFactory;
45  import java.io.IOException;
46  import java.io.InputStream;
47  import java.net.MalformedURLException;
48  import java.net.URL;
49  import java.net.URLConnection;
50  import java.util.ArrayList;
51  import java.util.Collections;
52  import java.util.EnumSet;
53  import java.util.HashSet;
54  import java.util.Iterator;
55  import java.util.List;
56  import java.util.Set;
57  
58  /**
59   * A Maven 2 repository that can be used to store and access artifacts like JARs and source archives within a running application.
60   * This class understands Maven 2 Project Object Model (POM) files, and thus is able to analyze dependencies and provide a
61   * {@link ClassLoader class loader} that accesses libraries using these transitive dependencies.
62   * <p>
63   * Instances are initialized with an authenticated {@link MavenUrlProvider Maven URL provider}, which is typically a
64   * {@link JcrMavenUrlProvider} instance configured with a {@link Repository JCR Repository} and path to the root of the repository
65   * subtree in that workspace. The repository can either already exist and contain the required artifacts, or it will be created as
66   * artifacts are loaded. Then to use libraries that are in the repository, simply obtain the
67   * {@link #getClassLoader(ClassLoader,MavenId...) class loader} by specifying the {@link MavenId artifact identifiers} for the
68   * libraries used directly by your code. This class loader will add any libraries that are required by those you supply.
69   * </p>
70   */
71  public class MavenRepository implements ClassLoaderFactory {
72  
73      private final MavenUrlProvider urlProvider;
74      private final MavenClassLoaders classLoaders;
75      private static final Logger LOGGER = Logger.getLogger(MavenRepository.class);
76  
77      public MavenRepository( final MavenUrlProvider urlProvider ) {
78          CheckArg.isNotNull(urlProvider, "urlProvider");
79          this.urlProvider = urlProvider;
80          this.classLoaders = new MavenClassLoaders(this);
81          assert LOGGER != null;
82          assert this.urlProvider != null;
83      }
84  
85      /**
86       * Get a class loader that has as its classpath the JARs for the libraries identified by the supplied IDs. This method always
87       * returns a class loader, even when none of the specified libraries {@link #exists(MavenId) exist} in this repository.
88       * 
89       * @param parent the parent class loader that will be consulted before any project class loaders; may be null if the
90       *        {@link Thread#getContextClassLoader() current thread's context class loader} or the class loader that loaded this
91       *        class should be used
92       * @param mavenIds the IDs of the libraries in this Maven repository
93       * @return the class loader
94       * @see #exists(MavenId)
95       * @see #exists(MavenId,MavenId...)
96       * @throws IllegalArgumentException if no Maven IDs are passed in or if any of the IDs are null
97       */
98      public ClassLoader getClassLoader( ClassLoader parent,
99                                         MavenId... mavenIds ) {
100         CheckArg.isNotEmpty(mavenIds, "mavenIds");
101         CheckArg.containsNoNulls(mavenIds, "mavenIds");
102         return this.classLoaders.getClassLoader(parent, mavenIds);
103     }
104 
105     /**
106      * Get a class loader that has as its classpath the JARs for the libraries identified by the supplied IDs. This method always
107      * returns a class loader, even when none of the specified libraries {@link #exists(MavenId) exist} in this repository.
108      * 
109      * @param coordinates the IDs of the libraries in this Maven repository
110      * @return the class loader
111      * @throws IllegalArgumentException if no coordinates are passed in or if any of the coordinate references is null
112      */
113     public ClassLoader getClassLoader( String... coordinates ) {
114         return getClassLoader(null, coordinates);
115     }
116 
117     /**
118      * Get a class loader that has as its classpath the JARs for the libraries identified by the supplied IDs. This method always
119      * returns a class loader, even when none of the specified libraries {@link #exists(MavenId) exist} in this repository.
120      * 
121      * @param parent the parent class loader that will be consulted before any project class loaders; may be null if the
122      *        {@link Thread#getContextClassLoader() current thread's context class loader} or the class loader that loaded this
123      *        class should be used
124      * @param coordinates the IDs of the libraries in this Maven repository
125      * @return the class loader
126      * @throws IllegalArgumentException if no coordinates are passed in or if any of the coordinate references is null
127      */
128     public ClassLoader getClassLoader( ClassLoader parent,
129                                        String... coordinates ) {
130         CheckArg.isNotEmpty(coordinates, "coordinates");
131         CheckArg.containsNoNulls(coordinates, "coordinates");
132         MavenId[] mavenIds = new MavenId[coordinates.length];
133         for (int i = 0; i < coordinates.length; i++) {
134             String coordinate = coordinates[i];
135             mavenIds[i] = new MavenId(coordinate);
136         }
137         return getClassLoader(parent, mavenIds); // parent may be null
138     }
139 
140     /**
141      * Determine whether the identified library exists in this Maven repository.
142      * 
143      * @param mavenId the ID of the library
144      * @return true if this repository contains the library, or false if it does not exist (or the ID is null)
145      * @throws MavenRepositoryException if there is a problem connecting to or using the Maven repository, as configured
146      * @see #exists(MavenId,MavenId...)
147      */
148     public boolean exists( MavenId mavenId ) throws MavenRepositoryException {
149         if (mavenId == null) return false;
150         Set<MavenId> existing = exists(mavenId, (MavenId)null);
151         return existing.contains(mavenId);
152     }
153 
154     /**
155      * Determine which of the identified libraries exist in this Maven repository.
156      * 
157      * @param firstId the first ID of the library to check
158      * @param mavenIds the IDs of the libraries; any null IDs will be ignored
159      * @return the set of IDs for libraries that do exist in this repository; never null
160      * @throws MavenRepositoryException if there is a problem connecting to or using the Maven repository, as configured
161      * @see #exists(MavenId)
162      */
163     public Set<MavenId> exists( MavenId firstId,
164                                 MavenId... mavenIds ) throws MavenRepositoryException {
165         if (mavenIds == null || mavenIds.length == 0) return Collections.emptySet();
166 
167         // Find the set of MavenIds that are not null ...
168         Set<MavenId> nonNullIds = new HashSet<MavenId>();
169         if (firstId != null) nonNullIds.add(firstId);
170         for (MavenId mavenId : mavenIds) {
171             if (mavenId != null) nonNullIds.add(mavenId);
172         }
173         if (nonNullIds.isEmpty()) return nonNullIds;
174 
175         MavenId lastMavenId = null;
176         try {
177             for (Iterator<MavenId> iter = nonNullIds.iterator(); iter.hasNext();) {
178                 lastMavenId = iter.next();
179                 URL urlToMavenId = this.urlProvider.getUrl(lastMavenId, null, null, false);
180                 boolean exists = urlToMavenId != null;
181                 if (!exists) iter.remove();
182             }
183         } catch (MalformedURLException err) {
184             throw new MavenRepositoryException(MavenI18n.errorCreatingUrlForMavenId.text(lastMavenId, err.getMessage()));
185         }
186         return nonNullIds;
187     }
188 
189     /**
190      * Get the dependencies for the Maven project with the specified ID.
191      * <p>
192      * This implementation downloads the POM file for the specified project to extract the dependencies and exclusions.
193      * </p>
194      * 
195      * @param mavenId the ID of the project; may not be null
196      * @return the list of dependencies
197      * @throws IllegalArgumentException if the MavenId reference is null
198      * @throws MavenRepositoryException if there is a problem finding or reading the POM file given the MavenId
199      */
200     public List<MavenDependency> getDependencies( MavenId mavenId ) {
201         URL pomUrl = null;
202         try {
203             pomUrl = getUrl(mavenId, ArtifactType.POM, null);
204             return getDependencies(mavenId, pomUrl.openStream());
205         } catch (IOException e) {
206             throw new MavenRepositoryException(MavenI18n.errorGettingPomFileForMavenIdAtUrl.text(mavenId, pomUrl), e);
207         }
208     }
209 
210     /**
211      * Get the dependencies for the Maven project with the specified ID.
212      * <p>
213      * This implementation downloads the POM file for the specified project to extract the dependencies and exclusions.
214      * </p>
215      * 
216      * @param mavenId the ID of the Maven project for which the dependencies are to be obtained
217      * @param pomStream the stream to the POM file
218      * @param allowedScopes the set of scopes that are to be allowed in the dependency list; if null, the default scopes of
219      *        {@link MavenDependency.Scope#getRuntimeScopes()} are used
220      * @return the list of dependencies; never null
221      * @throws IllegalArgumentException if the MavenId or InputStream references are null
222      * @throws IOException if an error occurs reading the stream
223      * @throws MavenRepositoryException if there is a problem reading the POM file given the supplied stream and MavenId
224      */
225     protected List<MavenDependency> getDependencies( MavenId mavenId,
226                                                      InputStream pomStream,
227                                                      MavenDependency.Scope... allowedScopes ) throws IOException {
228         CheckArg.isNotNull(mavenId, "mavenId");
229         CheckArg.isNotNull(pomStream, "pomStream");
230         EnumSet<MavenDependency.Scope> includedScopes = MavenDependency.Scope.getRuntimeScopes();
231         if (allowedScopes != null && allowedScopes.length > 0) includedScopes = EnumSet.of(allowedScopes[0], allowedScopes);
232         List<MavenDependency> results = new ArrayList<MavenDependency>();
233 
234         try {
235             // Use JAXP to load the XML document ...
236             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
237             factory.setNamespaceAware(true); // never forget this!
238             DocumentBuilder builder = factory.newDocumentBuilder();
239             Document doc = builder.parse(pomStream);
240 
241             // Create an XPath object ...
242             XPathFactory xpathFactory = XPathFactory.newInstance();
243             XPath xpath = xpathFactory.newXPath();
244             xpath.setNamespaceContext(new SimpleNamespaceContext().setNamespace("m", "http://maven.apache.org/POM/4.0.0"));
245 
246             // Set up some XPath expressions ...
247             XPathExpression projectExpression = xpath.compile("//m:project");
248             XPathExpression dependencyExpression = xpath.compile("//m:project/m:dependencies/m:dependency");
249             XPathExpression groupIdExpression = xpath.compile("./m:groupId/text()");
250             XPathExpression artifactIdExpression = xpath.compile("./m:artifactId/text()");
251             XPathExpression versionExpression = xpath.compile("./m:version/text()");
252             XPathExpression classifierExpression = xpath.compile("./m:classifier/text()");
253             XPathExpression scopeExpression = xpath.compile("./m:scope/text()");
254             XPathExpression typeExpression = xpath.compile("./m:type/text()");
255             XPathExpression exclusionExpression = xpath.compile("./m:exclusions/m:exclusion");
256 
257             // Extract the Maven ID for this POM file ...
258             org.w3c.dom.Node projectNode = (org.w3c.dom.Node)projectExpression.evaluate(doc, XPathConstants.NODE);
259             String groupId = (String)groupIdExpression.evaluate(projectNode, XPathConstants.STRING);
260             String artifactId = (String)artifactIdExpression.evaluate(projectNode, XPathConstants.STRING);
261             String version = (String)versionExpression.evaluate(projectNode, XPathConstants.STRING);
262             String classifier = (String)classifierExpression.evaluate(projectNode, XPathConstants.STRING);
263             if (groupId == null || artifactId == null || version == null) {
264                 throw new IllegalArgumentException(MavenI18n.pomFileIsInvalid.text(mavenId));
265             }
266             MavenId actualMavenId = new MavenId(groupId, artifactId, version, classifier);
267             if (!mavenId.equals(actualMavenId)) {
268                 throw new IllegalArgumentException(MavenI18n.pomFileContainsUnexpectedId.text(actualMavenId, mavenId));
269             }
270 
271             // Evaluate the XPath expression and iterate over the "dependency" nodes ...
272             NodeList nodes = (NodeList)dependencyExpression.evaluate(doc, XPathConstants.NODESET);
273             for (int i = 0; i < nodes.getLength(); ++i) {
274                 org.w3c.dom.Node dependencyNode = nodes.item(i);
275                 assert dependencyNode != null;
276                 String depGroupId = (String)groupIdExpression.evaluate(dependencyNode, XPathConstants.STRING);
277                 String depArtifactId = (String)artifactIdExpression.evaluate(dependencyNode, XPathConstants.STRING);
278                 String depVersion = (String)versionExpression.evaluate(dependencyNode, XPathConstants.STRING);
279                 String depClassifier = (String)classifierExpression.evaluate(dependencyNode, XPathConstants.STRING);
280                 String scopeText = (String)scopeExpression.evaluate(dependencyNode, XPathConstants.STRING);
281                 String depType = (String)typeExpression.evaluate(dependencyNode, XPathConstants.STRING);
282 
283                 // Extract the Maven dependency ...
284                 if (depGroupId == null || depArtifactId == null || depVersion == null) {
285                     LOGGER.trace("Skipping dependency of {1} due to missing groupId, artifactId or version: {2}",
286                                       mavenId,
287                                       dependencyNode);
288                     continue; // not enough information, so skip
289                 }
290                 MavenDependency dependency = new MavenDependency(depGroupId, depArtifactId, depVersion, depClassifier);
291                 dependency.setType(depType);
292 
293                 // If the scope is "compile" (default) or "runtime", then we need to process the dependency ...
294                 dependency.setScope(scopeText);
295                 if (!includedScopes.contains(dependency.getScope())) continue;
296 
297                 // Find any exclusions ...
298                 NodeList exclusionNodes = (NodeList)exclusionExpression.evaluate(dependencyNode, XPathConstants.NODESET);
299                 for (int j = 0; j < exclusionNodes.getLength(); ++j) {
300                     org.w3c.dom.Node exclusionNode = exclusionNodes.item(j);
301                     assert exclusionNode != null;
302                     String excludedGroupId = (String)groupIdExpression.evaluate(exclusionNode, XPathConstants.STRING);
303                     String excludedArtifactId = (String)artifactIdExpression.evaluate(exclusionNode, XPathConstants.STRING);
304 
305                     if (excludedGroupId == null || excludedArtifactId == null) {
306                         LOGGER.trace("Skipping exclusion in dependency of {1} due to missing exclusion groupId or artifactId: {2} ",
307                                           mavenId,
308                                           exclusionNode);
309                         continue; // not enough information, so skip
310                     }
311                     MavenId excludedId = new MavenId(excludedGroupId, excludedArtifactId);
312                     dependency.getExclusions().add(excludedId);
313                 }
314 
315                 results.add(dependency);
316             }
317         } catch (XPathExpressionException err) {
318             throw new MavenRepositoryException(MavenI18n.errorCreatingXpathStatementsToEvaluatePom.text(mavenId), err);
319         } catch (ParserConfigurationException err) {
320             throw new MavenRepositoryException(MavenI18n.errorCreatingXpathParserToEvaluatePom.text(mavenId), err);
321         } catch (SAXException err) {
322             throw new MavenRepositoryException(MavenI18n.errorReadingXmlDocumentToEvaluatePom.text(mavenId), err);
323         } finally {
324             try {
325                 pomStream.close();
326             } catch (IOException e) {
327                 LOGGER.error(e, MavenI18n.errorClosingUrlStreamToPom, mavenId);
328             }
329         }
330         return results;
331     }
332 
333     /**
334      * Get the URL for the artifact with the specified type in the given Maven project. The resulting URL can be used to
335      * {@link URL#openConnection() connect} to the repository to {@link URLConnection#getInputStream() read} or
336      * {@link URLConnection#getOutputStream() write} the artifact's content.
337      * 
338      * @param mavenId the ID of the Maven project; may not be null
339      * @param artifactType the type of artifact; may be null, but the URL will not be able to be read or written to
340      * @param signatureType the type of signature; may be null if the signature file is not desired
341      * @return the URL to this artifact; never null
342      * @throws MalformedURLException if the supplied information cannot be turned into a valid URL
343      */
344     public URL getUrl( MavenId mavenId,
345                        ArtifactType artifactType,
346                        SignatureType signatureType ) throws MalformedURLException {
347         return this.urlProvider.getUrl(mavenId, artifactType, signatureType, false);
348     }
349 
350     /**
351      * Get the URL for the artifact with the specified type in the given Maven project. The resulting URL can be used to
352      * {@link URL#openConnection() connect} to the repository to {@link URLConnection#getInputStream() read} or
353      * {@link URLConnection#getOutputStream() write} the artifact's content.
354      * 
355      * @param mavenId the ID of the Maven project; may not be null
356      * @param artifactType the type of artifact; may be null, but the URL will not be able to be read or written to
357      * @param signatureType the type of signature; may be null if the signature file is not desired
358      * @param createIfRequired true if the node structure should be created if any part of it does not exist; this always expects
359      *        that the path to the top of the repository tree exists.
360      * @return the URL to this artifact; never null
361      * @throws MalformedURLException if the supplied information cannot be turned into a valid URL
362      */
363     public URL getUrl( MavenId mavenId,
364                        ArtifactType artifactType,
365                        SignatureType signatureType,
366                        boolean createIfRequired ) throws MalformedURLException {
367         return this.urlProvider.getUrl(mavenId, artifactType, signatureType, createIfRequired);
368     }
369 
370     /**
371      * This method is called to signal this repository that the POM file for a project has been updated. This method notifies the
372      * associated class loader of the change, which will adapt appropriately.
373      * 
374      * @param mavenId
375      */
376     protected void notifyUpdatedPom( MavenId mavenId ) {
377         this.classLoaders.notifyChangeInDependencies(mavenId);
378     }
379 }