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}