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 }