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