001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.maven.cdi; 018 019import java.io.File; 020 021import java.net.MalformedURLException; 022import java.net.URI; 023import java.net.URL; 024import java.net.URLClassLoader; 025import java.net.URLStreamHandler; // for javadoc only 026import java.net.URLStreamHandlerFactory; 027 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Objects; 036import java.util.Set; 037 038import java.util.stream.Collectors; 039 040import org.eclipse.aether.artifact.ArtifactProperties; 041 042import org.eclipse.aether.RepositorySystem; 043import org.eclipse.aether.RepositorySystemSession; 044 045import org.eclipse.aether.artifact.Artifact; 046import org.eclipse.aether.artifact.DefaultArtifact; 047 048import org.eclipse.aether.collection.CollectRequest; 049 050import org.eclipse.aether.graph.Dependency; 051 052import org.eclipse.aether.repository.RemoteRepository; 053 054import org.eclipse.aether.resolution.ArtifactResult; 055import org.eclipse.aether.resolution.DependencyRequest; 056import org.eclipse.aether.resolution.DependencyResolutionException; 057import org.eclipse.aether.resolution.DependencyResult; 058 059import org.eclipse.aether.util.artifact.JavaScopes; 060 061import org.eclipse.aether.util.filter.DependencyFilterUtils; 062 063/** 064 * A {@link URLClassLoader} that uses the <a 065 * href="https://maven.apache.org/resolver/apidocs/index.html?org/eclipse/aether/util/filter/DependencyFilterUtils.html">Maven 066 * Resolver API</a> to resolve artifacts from Maven repositories. 067 * 068 * @author <a href="https://about.me/lairdnelson" 069 * target="_parent">Laird Nelson</a> 070 * 071 * @see #MavenArtifactClassLoader(RepositorySystem, 072 * RepositorySystemSession, DependencyRequest, ClassLoader, 073 * URLStreamHandlerFactory) 074 */ 075public class MavenArtifactClassLoader extends URLClassLoader { 076 077 078 /* 079 * Static fields. 080 */ 081 082 083 /** 084 * The {@link String} used to separate classpath entries. 085 * 086 * <p>This field is never {@code null}.</p> 087 */ 088 private static final String classpathSeparator = System.getProperty("path.separator", ":"); 089 090 091 /* 092 * Constructors. 093 */ 094 095 /** 096 * Creates a new {@link MavenArtifactClassLoader}. 097 * 098 * @param repositorySystem the {@link RepositorySystem} responsible 099 * for resolving artifacts; must not be {@code null} 100 * 101 * @param session the {@link RepositorySystemSession} governing 102 * certain aspects of the resolution process; must not be {@code 103 * null} 104 * 105 * @param dependencyRequest the {@link DependencyRequest} that 106 * describes what artifacts should be resolved; must not be {@code 107 * null} 108 * 109 * @exception NullPointerException if a parameter that must not be 110 * {@code null} is discovered to be {@code null} 111 * 112 * @exception DependencyResolutionException if there was a problem 113 * resolving dependencies 114 * 115 * @see #getUrls(RepositorySystem, RepositorySystemSession, 116 * DependencyRequest) 117 * 118 * @see #getDependencyRequest(String, List) 119 * 120 * @see URLClassLoader#URLClassLoader(URL[]) 121 */ 122 public MavenArtifactClassLoader(final RepositorySystem repositorySystem, 123 final RepositorySystemSession session, 124 final DependencyRequest dependencyRequest) 125 throws DependencyResolutionException { 126 super(getUrlArray(getUrls(repositorySystem, session, dependencyRequest))); 127 } 128 129 /** 130 * Creates a new {@link MavenArtifactClassLoader}. 131 * 132 * @param repositorySystem the {@link RepositorySystem} responsible 133 * for resolving artifacts; must not be {@code null} 134 * 135 * @param session the {@link RepositorySystemSession} governing 136 * certain aspects of the resolution process; must not be {@code 137 * null} 138 * 139 * @param dependencyRequest the {@link DependencyRequest} that 140 * describes what artifacts should be resolved; must not be {@code 141 * null} 142 * 143 * @param parentClassLoader the {@link ClassLoader} that will 144 * {@linkplain ClassLoader#getParent() parent} this one; may be 145 * {@code null} 146 * 147 * @exception NullPointerException if a parameter that must not be 148 * {@code null} is discovered to be {@code null} 149 * 150 * @exception DependencyResolutionException if there was a problem 151 * resolving dependencies 152 * 153 * @see #getUrls(RepositorySystem, RepositorySystemSession, 154 * DependencyRequest) 155 * 156 * @see #getDependencyRequest(String, List) 157 * 158 * @see URLClassLoader#URLClassLoader(URL[], ClassLoader) 159 */ 160 public MavenArtifactClassLoader(final RepositorySystem repositorySystem, 161 final RepositorySystemSession session, 162 final DependencyRequest dependencyRequest, 163 final ClassLoader parentClassLoader) 164 throws DependencyResolutionException { 165 super(getUrlArray(getUrls(repositorySystem, session, dependencyRequest)), 166 parentClassLoader); 167 } 168 169 /** 170 * Creates a new {@link MavenArtifactClassLoader}. 171 * 172 * @param repositorySystem the {@link RepositorySystem} responsible 173 * for resolving artifacts; must not be {@code null} 174 * 175 * @param session the {@link RepositorySystemSession} governing 176 * certain aspects of the resolution process; must not be {@code 177 * null} 178 * 179 * @param dependencyRequest the {@link DependencyRequest} that 180 * describes what artifacts should be resolved; must not be {@code 181 * null} 182 * 183 * @param parentClassLoader the {@link ClassLoader} that will 184 * {@linkplain ClassLoader#getParent() parent} this one; may be 185 * {@code null} 186 * 187 * @param urlStreamHandlerFactory the {@link 188 * URLStreamHandlerFactory} to create {@link URLStreamHandler}s for 189 * the {@link URL}s {@linkplain #getURLs() <code>URL</code>s that 190 * constitute this <code>MavenArtifactClassLoader</code>'s 191 * classpath}; {@link URLClassLoader} does not define whether this 192 * can be {@code null} or not 193 * 194 * @exception NullPointerException if a parameter that must not be 195 * {@code null} is discovered to be {@code null} 196 * 197 * @exception DependencyResolutionException if there was a problem 198 * resolving dependencies 199 * 200 * @see #getUrls(RepositorySystem, RepositorySystemSession, 201 * DependencyRequest) 202 * 203 * @see #getDependencyRequest(String, List) 204 * 205 * @see URLClassLoader#URLClassLoader(URL[], ClassLoader, 206 * URLStreamHandlerFactory) 207 */ 208 public MavenArtifactClassLoader(final RepositorySystem repositorySystem, 209 final RepositorySystemSession session, 210 final DependencyRequest dependencyRequest, 211 final ClassLoader parentClassLoader, 212 final URLStreamHandlerFactory urlStreamHandlerFactory) 213 throws DependencyResolutionException { 214 super(getUrlArray(getUrls(repositorySystem, session, dependencyRequest)), 215 parentClassLoader, 216 urlStreamHandlerFactory); 217 } 218 219 220 /* 221 * Instance methods. 222 */ 223 224 225 /** 226 * Returns a non-{@code null} {@link String} with a classpath-like 227 * format that can represent this {@linkplain #getURLs() 228 * <code>MavenArtifactClassLoader</code>'s <code>URL</code>s}. 229 * 230 * <p>The format of the {@link String} that is returned is exactly 231 * like a classpath string appropriate for the current platform with 232 * the exception that the platform-specific classpath separator is 233 * doubled if any of the {@link URL}s {@linkplain #getURLs() 234 * belonging to this <code>MavenArtifactClassLoader</code>} is not a 235 * {@code file} URL.</p> 236 * 237 * <p>For any given {@link URL}, the returned {@link String} will 238 * represent it as its {@linkplain URL#toExternalForm() 239 * <code>String</code> form}, unless it has a {@linkplain 240 * URL#getProtocol() protocol} equal to {@code file}, in which case 241 * will represent it as its {@linkplain URL#getPath() path}.</p> 242 * 243 * @return a non-{@code null} classpath-like {@link String} (with 244 * doubled classpath separators) consisting of this {@linkplain 245 * #getURLs() <code>MavenArtifactClassLoader</code>'s 246 * <code>URL</code>s} represented as described above 247 */ 248 public String toClasspath() { 249 final String returnValue; 250 final URL[] urls = this.getURLs(); 251 if (urls != null && urls.length > 0) { 252 final StringBuilder sb = new StringBuilder(); 253 boolean allFileProtocols = true; 254 final String separator = classpathSeparator + classpathSeparator; 255 for (int i = 0; i < urls.length; i++) { 256 final URL url = urls[i]; 257 if (url != null) { 258 if ("file".equals(url.getProtocol())) { 259 sb.append(url.getPath()); 260 } else { 261 allFileProtocols = false; 262 sb.append(url.toString()); 263 } 264 if (i + 1 < urls.length) { 265 sb.append(separator); 266 } 267 } 268 } 269 if (allFileProtocols) { 270 returnValue = sb.toString().replace(separator, classpathSeparator); 271 } else { 272 returnValue = sb.toString(); 273 } 274 } else { 275 returnValue = ""; 276 } 277 return returnValue; 278 } 279 280 281 /* 282 * Static methods. 283 */ 284 285 286 /** 287 * Returns a non-{@code null} {@link DependencyRequest} by parsing 288 * the supplied classpath-like {@link String} for Maven artifact 289 * coordinates, and then invoking and returning the result of the 290 * {@link #getDependencyRequest(Set, List)} method. 291 * 292 * <p>This method never returns {@code null}.</p> 293 * 294 * <p>Elements within the supplied {@code classpathLikeString} are 295 * separated by double occurrences of the platform-specific 296 * classpath separator. For example, on Unix and Unix-derived 297 * systems, a classpath-like string of the form {@code 298 * com.foo:bar:1.0::com.fizz:buzz:1.0} will yield two elements: 299 * {@code com.foo:bar:1.0} and {@code com.fizz:buzz:1.0}. On 300 * Windows systems, a classpath-like string of the form {@code 301 * com.foo:bar:1.0;;com.fizz:buzz:1.0} will yield two elements: 302 * {@code com.foo:bar:1.0} and {@code com.fizz:buzz:1.0}.</p> 303 * 304 * @param classpathLikeString a classpath-like {@link String} 305 * formatted as described above; may be {@code null} 306 * 307 * @param remoteRepositories a {@link List} of {@link 308 * RemoteRepository} instances that will be forwarded on to the 309 * {@link #getDependencyRequest(Set, List)} method; must not be 310 * {@code null} 311 * 312 * @return the return value that results from invoking the {@link 313 * #getDependencyRequest(Set, List)} method 314 * 315 * @exception NullPointerException if {@code remoteRepositories} is 316 * {@code null} 317 */ 318 public static final DependencyRequest getDependencyRequest(final String classpathLikeString, 319 final List<RemoteRepository> remoteRepositories) { 320 final Set<Artifact> artifacts; 321 if (classpathLikeString == null) { 322 artifacts = Collections.emptySet(); 323 } else { 324 final String separator = "\\s*" + classpathSeparator + classpathSeparator + "\\s*"; 325 final String[] artifactIdentifiers = classpathLikeString.split(separator); 326 assert artifactIdentifiers != null; 327 artifacts = new LinkedHashSet<>(); 328 for (final String artifactIdentifier : artifactIdentifiers) { 329 if (artifactIdentifier != null && !artifactIdentifier.isEmpty()) { 330 final Artifact artifact = new DefaultArtifact(artifactIdentifier); 331 final String extension = artifact.getExtension(); 332 if ("war".equals(extension) || "ear".equals(extension)) { 333 final Map<String, String> originalProperties = artifact.getProperties(); 334 final Map<String, String> newProperties; 335 if (originalProperties == null) { 336 newProperties = new HashMap<>(); 337 } else { 338 newProperties = new HashMap<>(originalProperties); 339 } 340 newProperties.put(ArtifactProperties.INCLUDES_DEPENDENCIES, "true"); 341 artifacts.add(artifact.setProperties(newProperties)); 342 } else { 343 artifacts.add(artifact); 344 } 345 } 346 } 347 } 348 return getDependencyRequest(artifacts, remoteRepositories); 349 } 350 351 /** 352 * Given a {@link Set} of {@link Artifact}s and a {@link List} of 353 * {@link RemoteRepository} instances representing repositories from 354 * which they might be resolved, creates and returns a {@link 355 * DependencyRequest} for their resolution, using {@linkplain 356 * JavaScopes#RUNTIME runtime scope}. 357 * 358 * <p>This method never returns {@code null}.</p> 359 * 360 * @param artifacts a {@link Set} of {@link Artifact}s to resolve; 361 * may be {@code null} 362 * 363 * @param remoteRepositories a {@link List} of {@link 364 * RemoteRepository} instances representing Maven repositories from 365 * which the supplied {@link Artifact} instances may be resolved; 366 * must not be {@code null} 367 * 368 * @return a non-{@code null} {@link DependencyRequest} 369 * 370 * @exception NullPointerException if {@code remoteRepositories} is 371 * {@code null} 372 * 373 * @see #getDependencyRequest(Set, String, List) 374 */ 375 public static final DependencyRequest getDependencyRequest(final Set<? extends Artifact> artifacts, 376 final List<RemoteRepository> remoteRepositories) { 377 return getDependencyRequest(artifacts, JavaScopes.RUNTIME, remoteRepositories); 378 } 379 380 /** 381 * Given a {@link Set} of {@link Artifact}s and a {@link List} of 382 * {@link RemoteRepository} instances representing repositories from 383 * which they might be resolved, creates and returns a {@link 384 * DependencyRequest} for their resolution in the supplied scope. 385 * 386 * <p>This method never returns {@code null}.</p> 387 * 388 * @param artifacts a {@link Set} of {@link Artifact}s to resolve; 389 * may be {@code null} 390 * 391 * @param scope the scope in which resolution should take place; may 392 * be {@code null} in which case {@link JavaScopes#RUNTIME} will be 393 * used instead; see {@link JavaScopes} for commonly-used scopes 394 * 395 * @param remoteRepositories a {@link List} of {@link 396 * RemoteRepository} instances representing Maven repositories from 397 * which the supplied {@link Artifact} instances may be resolved; 398 * must not be {@code null} 399 * 400 * @return a non-{@code null} {@link DependencyRequest} 401 * 402 * @exception NullPointerException if {@code remoteRepositories} is 403 * {@code null} 404 * 405 * @see JavaScopes 406 * 407 * @see #getDependencyRequest(CollectRequest) 408 */ 409 public static final DependencyRequest getDependencyRequest(final Set<? extends Artifact> artifacts, 410 String scope, 411 final List<RemoteRepository> remoteRepositories) { 412 if (scope == null) { 413 scope = JavaScopes.RUNTIME; 414 } 415 CollectRequest collectRequest = new CollectRequest() 416 .setRoot(null) 417 .setRepositories(remoteRepositories); 418 if (artifacts != null && !artifacts.isEmpty()) { 419 if (artifacts.size() == 1) { 420 final Artifact root = artifacts.iterator().next(); 421 if (root != null) { 422 collectRequest = collectRequest.setRoot(new Dependency(root, scope)); 423 } 424 } else { 425 for (final Artifact artifact : artifacts) { 426 if (artifact != null) { 427 collectRequest = collectRequest.addDependency(new Dependency(artifact, scope)); 428 } 429 } 430 } 431 } 432 return getDependencyRequest(collectRequest); 433 } 434 435 /** 436 * Returns a non-{@code null} {@link DependencyRequest} suitable for 437 * the supplied {@link CollectRequest}. 438 * 439 * <p>This method never returns {@code null}.</p> 440 * 441 * @param collectRequest a {@link CollectRequest} describing 442 * dependency collection; must not be {@code null} 443 * 444 * @return a new {@link DependencyRequest}; never {@code null} 445 * 446 * @exception NullPointerException if {@code collectRequest} is 447 * {@code null} 448 * 449 * @see CollectRequest 450 * 451 * @see DependencyRequest 452 * 453 * @see #MavenArtifactClassLoader(RepositorySystem, 454 * RepositorySystemSession, DependencyRequest, ClassLoader, 455 * URLStreamHandlerFactory) 456 */ 457 public static final DependencyRequest getDependencyRequest(final CollectRequest collectRequest) { 458 Objects.requireNonNull(collectRequest); 459 final Dependency root = collectRequest.getRoot(); 460 final String scope; 461 if (root != null) { 462 scope = root.getScope(); 463 } else { 464 scope = JavaScopes.RUNTIME; 465 } 466 return new DependencyRequest(collectRequest, DependencyFilterUtils.classpathFilter(scope)); 467 } 468 469 private static final URL[] getUrlArray(final Collection<? extends URL> urls) { 470 if (urls == null || urls.isEmpty()) { 471 return new URL[0]; 472 } 473 return urls.toArray(new URL[urls.size()]); 474 } 475 476 /** 477 * Returns a {@link Collection} of ({@code file}) {@link URL}s that 478 * results from resolution of the dependencies described by the 479 * supplied {@link DependencyRequest}. 480 * 481 * <p>This method never returns {@code null}.</p> 482 * 483 * @param repositorySystem the {@link RepositorySystem} responsible 484 * for resolving artifacts; must not be {@code null} 485 * 486 * @param session the {@link RepositorySystemSession} governing 487 * certain aspects of the resolution process; must not be {@code 488 * null} 489 * 490 * @param dependencyRequest the {@link DependencyRequest} that 491 * describes what artifacts should be resolved; must not be {@code 492 * null} 493 * 494 * @return a non-{@code null} {@link Collection} of distinct {@link 495 * URL}s; a {@link Set} is not part of the contract of this method 496 * only because the {@link URL#equals(Object)} method involves DNS 497 * lookups 498 * 499 * @exception NullPointerException if any parameter is {@code null} 500 * 501 * @exception DependencyResolutionException if there was a problem 502 * with dependency resolution 503 */ 504 public static final Collection<? extends URL> getUrls(final RepositorySystem repositorySystem, 505 final RepositorySystemSession session, 506 final DependencyRequest dependencyRequest) 507 throws DependencyResolutionException { 508 Objects.requireNonNull(repositorySystem); 509 Objects.requireNonNull(session); 510 Objects.requireNonNull(dependencyRequest); 511 final DependencyResult dependencyResult = repositorySystem.resolveDependencies(session, dependencyRequest); 512 assert dependencyResult != null; 513 final List<ArtifactResult> artifactResults = dependencyResult.getArtifactResults(); 514 assert artifactResults != null; 515 516 // We use an intermediate Set of URIs, not URLs, because the 517 // URL#equals(Object) method will trigger DNS lookups otherwise 518 // (!). 519 Set<URI> uris = new LinkedHashSet<>(); 520 if (!artifactResults.isEmpty()) { 521 for (final ArtifactResult artifactResult : artifactResults) { 522 if (artifactResult != null && artifactResult.isResolved()) { 523 final Artifact resolvedArtifact = artifactResult.getArtifact(); 524 if (resolvedArtifact != null) { 525 final File f = resolvedArtifact.getFile(); 526 assert f != null; // guaranteed by isResolved() contract 527 assert f.isFile(); 528 assert f.canRead(); 529 uris.add(f.toURI()); 530 } 531 } 532 } 533 } 534 final Collection<? extends URL> returnValue; 535 if (uris.isEmpty()) { 536 returnValue = Collections.emptySet(); 537 } else { 538 returnValue = Collections.unmodifiableCollection(uris.stream() 539 .map(uri -> { 540 try { 541 return uri.toURL(); 542 } catch (final MalformedURLException malformedUrlException) { 543 throw new IllegalArgumentException(malformedUrlException.getMessage(), 544 malformedUrlException); 545 } 546 }) 547 .collect(Collectors.toCollection(ArrayList::new))); 548 } 549 return returnValue; 550 } 551 552}