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 }