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 }