View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.impl;
20  
21  import java.io.IOException;
22  import java.lang.module.ModuleDescriptor;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.Optional;
32  import java.util.Set;
33  import java.util.function.Predicate;
34  
35  import org.apache.maven.api.Dependency;
36  import org.apache.maven.api.JavaPathType;
37  import org.apache.maven.api.Node;
38  import org.apache.maven.api.PathType;
39  import org.apache.maven.api.Type;
40  import org.apache.maven.api.services.DependencyResolverException;
41  import org.apache.maven.api.services.DependencyResolverRequest;
42  import org.apache.maven.api.services.DependencyResolverResult;
43  import org.apache.maven.impl.resolver.artifact.MavenArtifactProperties;
44  
45  /**
46   * The result of collecting dependencies with a dependency resolver.
47   * New instances are initially empty. Callers must populate with calls
48   * to the following methods, in that order:
49   *
50   * <ul>
51   *   <li>{@link #addOutputDirectory(Path, Path)} (optional)</li>
52   *   <li>{@link #addDependency(Node, Dependency, Predicate, Path)}</li>
53   * </ul>
54   *
55   * @see DefaultDependencyResolver#resolve(DependencyResolverRequest)
56   */
57  public class DefaultDependencyResolverResult implements DependencyResolverResult {
58      /**
59       * The corresponding request.
60       */
61      private final DependencyResolverRequest request;
62  
63      /**
64       * The exceptions that occurred while building the dependency graph.
65       */
66      private final List<Exception> exceptions;
67  
68      /**
69       * The root node of the dependency graph.
70       */
71      private final Node root;
72  
73      /**
74       * The ordered list of the flattened dependency nodes.
75       */
76      private final List<Node> nodes;
77  
78      /**
79       * The file paths of all dependencies, regardless on which Java tool option those paths should be placed.
80       */
81      private final List<Path> paths;
82  
83      /**
84       * The file paths of all dependencies, dispatched according the Java options where to place them.
85       */
86      private final Map<PathType, List<Path>> dispatchedPaths;
87  
88      /**
89       * The dependencies together with the path to each dependency.
90       */
91      private final Map<Dependency, Path> dependencies;
92  
93      /**
94       * Information about modules in the main output. This field is initially null and is set to a non-null
95       * value when the output directories have been set, or when it is too late for setting them.
96       */
97      private PathModularization outputModules;
98  
99      /**
100      * Cache of module information about each dependency.
101      */
102     private final PathModularizationCache cache;
103 
104     /**
105      * Creates an initially empty result with a temporary cache.
106      * Callers should add path elements by calls to {@link #addDependency(Node, Dependency, Predicate, Path)}.
107      *
108      * <p><b>WARNING: this constructor may be removed in a future Maven release.</b>
109      * The reason is because {@code DefaultDependencyResolverResult} needs a cache, which should
110      * preferably be session-wide. How to manage such caches has not yet been clarified.</p>
111      *
112      * @param request the corresponding request
113      * @param exceptions the exceptions that occurred while building the dependency graph
114      * @param root the root node of the dependency graph
115      * @param count estimated number of dependencies
116      */
117     public DefaultDependencyResolverResult(
118             DependencyResolverRequest request, List<Exception> exceptions, Node root, int count) {
119         this(
120                 request,
121                 new PathModularizationCache(DefaultDependencyResolver.getTargetVersion(request)),
122                 exceptions,
123                 root,
124                 count);
125     }
126 
127     /**
128      * Creates an initially empty result. Callers should add path elements by calls
129      * to {@link #addDependency(Node, Dependency, Predicate, Path)}.
130      *
131      * @param request the corresponding request
132      * @param cache cache of module information about each dependency
133      * @param exceptions the exceptions that occurred while building the dependency graph
134      * @param root the root node of the dependency graph
135      * @param count estimated number of dependencies
136      */
137     DefaultDependencyResolverResult(
138             DependencyResolverRequest request,
139             PathModularizationCache cache,
140             List<Exception> exceptions,
141             Node root,
142             int count) {
143         this.request = request;
144         this.cache = Objects.requireNonNull(cache);
145         this.exceptions = exceptions;
146         this.root = root;
147         nodes = new ArrayList<>(count);
148         paths = new ArrayList<>(count);
149         dispatchedPaths = new LinkedHashMap<>();
150         dependencies = new LinkedHashMap<>(count + count / 3);
151     }
152 
153     /**
154      * Adds the given path element to the specified type of path.
155      *
156      * @param type the type of path (class-path, module-path, …)
157      * @param path the path element to add
158      */
159     private void addPathElement(PathType type, Path path) {
160         dispatchedPaths.computeIfAbsent(type, (t) -> new ArrayList<>()).add(path);
161     }
162 
163     /**
164      * Adds main and test output directories to the result. This method adds the main output directory
165      * to the module path if it contains a {@code module-info.class}, or to the class path otherwise.
166      * For the test output directory, the rules are more complex and are governed by the fact that
167      * Java does not accept the placement of two modules of the same name on the module path.
168      * So the modular test output directory usually needs to be placed in a {@code --path-module} option.
169      *
170      * <ul>
171      *   <li>If the test output directory is modular, then:
172      *     <ul>
173      *       <li>If a test module name is identical to a main module name,
174      *           place the test directory in a {@code --patch-module} option.</li>
175      *       <li>Otherwise, place the test directory on the module path. However, this case
176      *           (a module existing only in test output, not in main output) should be uncommon.</li>
177      *     </ul>
178      *   </li>
179      *   <li>Otherwise (test output contains no module information), then:
180      *     <ul>
181      *       <li>If the main output is on the module path, place the test output
182      *           on a {@code --patch-module} option.</li>
183      *       <li>Otherwise (main output on the class path), place the test output on the class path too.</li>
184      *     </ul>
185      *   </li>
186      * </ul>
187      *
188      * This method must be invoked before {@link #addDependency(Node, Dependency, Predicate, Path)}
189      * if output directories are desired on the class path or module path.
190      * This method can be invoked at most once.
191      *
192      * @param main the main output directory, or {@code null} if none
193      * @param test the test output directory, or {@code null} if none
194      * @throws IOException if an error occurred while reading module information
195      *
196      * TODO: this is currently not called. This is intended for use by Surefire and may move there.
197      */
198     void addOutputDirectory(Path main, Path test) throws IOException {
199         if (outputModules != null) {
200             throw new IllegalStateException("Output directories must be set first and only once.");
201         }
202         if (main != null) {
203             outputModules = cache.getModuleInfo(main);
204             addPathElement(outputModules.getPathType(), main);
205         } else {
206             outputModules = PathModularization.NONE;
207         }
208         if (test != null) {
209             boolean addToClasspath = true;
210             PathModularization testModules = cache.getModuleInfo(test);
211             boolean isModuleHierarchy = outputModules.isModuleHierarchy || testModules.isModuleHierarchy;
212             for (Object value : outputModules.descriptors.values()) {
213                 String moduleName = name(value);
214                 Path subdir = test;
215                 if (isModuleHierarchy) {
216                     // If module hierarchy is used, the directory names shall be the module names.
217                     Path path = test.resolve(moduleName);
218                     if (!Files.isDirectory(path)) {
219                         // Main module without tests. It is okay.
220                         continue;
221                     }
222                     subdir = path;
223                 }
224                 // When the same module is found in main and test output, the latter is patching the former.
225                 addPathElement(JavaPathType.patchModule(moduleName), subdir);
226                 addToClasspath = false;
227             }
228             /*
229              * If the test output directory provides some modules of its own, add them.
230              * Except for this unusual case, tests should never be added to the module-path.
231              */
232             for (Map.Entry<Path, Object> entry : testModules.descriptors.entrySet()) {
233                 if (!outputModules.containsModule(name(entry.getValue()))) {
234                     addPathElement(JavaPathType.MODULES, entry.getKey());
235                     addToClasspath = false;
236                 }
237             }
238             if (addToClasspath) {
239                 addPathElement(JavaPathType.CLASSES, test);
240             }
241         }
242     }
243 
244     /**
245      * Adds a dependency node to the result.
246      *
247      * @param node the dependency node
248      */
249     void addNode(Node node) {
250         nodes.add(node);
251     }
252 
253     /**
254      * Adds a dependency to the result. This method populates the {@link #nodes}, {@link #paths},
255      * {@link #dispatchedPaths} and {@link #dependencies} collections with the given arguments.
256      *
257      * @param node the dependency node
258      * @param dep the dependency for the given node, or {@code null} if none
259      * @param filter filter the paths accepted by the tool which will consume the path.
260      * @param path the path to the dependency, or {@code null} if the dependency was null
261      * @throws IOException if an error occurred while reading module information
262      */
263     void addDependency(Node node, Dependency dep, Predicate<PathType> filter, Path path) throws IOException {
264         nodes.add(node);
265         if (dep == null) {
266             return;
267         }
268         if (dependencies.put(dep, path) != null) {
269             throw new IllegalStateException("Duplicated key: " + dep);
270         }
271         if (path == null) {
272             return;
273         }
274         paths.add(path);
275         /*
276          * Dispatch the dependency to class path, module path, patch-module path, etc.
277          * according the dependency properties. We need to process patch-module first,
278          * because this type depends on whether a module of the same name has already
279          * been added on the module-type.
280          */
281         // DependencyProperties properties = dep.getDependencyProperties();
282         Set<PathType> pathTypes = dep.getType().getPathTypes();
283         if (containsPatches(pathTypes)) {
284             if (outputModules == null) {
285                 // For telling users that it is too late for setting the output directory.
286                 outputModules = PathModularization.NONE;
287             }
288             PathType type = null;
289             for (Map.Entry<Path, Object> info :
290                     cache.getModuleInfo(path).descriptors.entrySet()) {
291                 String moduleName = name(info.getValue());
292                 type = JavaPathType.patchModule(moduleName);
293                 if (!containsModule(moduleName)) {
294                     /*
295                      * Not patching an existing module. This case should be unusual. If it nevertheless
296                      * happens, add to class path or module path if allowed, or keep patching otherwise.
297                      * The latter case (keep patching) is okay if the main module will be defined later.
298                      */
299                     type = cache.selectPathType(pathTypes, filter, path).orElse(type);
300                 }
301                 addPathElement(type, info.getKey());
302                 // There is usually no more than one element, but nevertheless allow multi-modules.
303             }
304             /*
305              * If the dependency has no module information, search for an artifact of the same groupId
306              * and artifactId. If one is found, we are patching that module. If none is found, add the
307              * dependency as a normal dependency.
308              */
309             if (type == null) {
310                 Path main = findArtifactPath(dep.getGroupId(), dep.getArtifactId());
311                 if (main != null) {
312                     for (Map.Entry<Path, Object> info :
313                             cache.getModuleInfo(main).descriptors.entrySet()) {
314                         type = JavaPathType.patchModule(name(info.getValue()));
315                         addPathElement(type, info.getKey());
316                         // There is usually no more than one element, but nevertheless allow multi-modules.
317                     }
318                 }
319             }
320             if (type != null) {
321                 return; // Dependency added, we are done.
322             }
323         }
324         addPathElement(cache.selectPathType(pathTypes, filter, path).orElse(PathType.UNRESOLVED), path);
325         // If the artifact is also needed on a processor path (because it's a transitive dep
326         // of a processor AND a direct dep with a different type), add it to the processor path too.
327         addProcessorPathIfNeeded(node, filter, path);
328     }
329 
330     /**
331      * Checks if the artifact has a {@link MavenArtifactProperties#PROCESSOR_TYPE} property
332      * and, if so, also adds it to the corresponding processor path. This handles the case
333      * where an artifact is both a regular dependency (e.g., modular-jar on --module-path)
334      * and a transitive dependency of a processor (needs --processor-module-path).
335      */
336     private void addProcessorPathIfNeeded(Node node, Predicate<PathType> filter, Path path) throws IOException {
337         if (!(node instanceof AbstractNode abstractNode)) {
338             return;
339         }
340         org.eclipse.aether.artifact.Artifact aetherArtifact =
341                 abstractNode.getDependencyNode().getArtifact();
342         if (aetherArtifact == null) {
343             return;
344         }
345         String processorType = aetherArtifact.getProperty(MavenArtifactProperties.PROCESSOR_TYPE, null);
346         if (processorType == null) {
347             return;
348         }
349         Set<PathType> processorPathTypes = processorPathTypesFor(processorType);
350         if (processorPathTypes != null) {
351             cache.selectPathType(processorPathTypes, filter, path).ifPresent(pt -> addPathElement(pt, path));
352         }
353     }
354 
355     // Path type sets for processor types — must stay in sync with DefaultTypeProvider
356     private static final Set<PathType> PROCESSOR_PATH_TYPES =
357             Set.of(JavaPathType.PROCESSOR_CLASSES, JavaPathType.PROCESSOR_MODULES);
358     private static final Set<PathType> CLASSPATH_PROCESSOR_PATH_TYPES = Set.of(JavaPathType.PROCESSOR_CLASSES);
359     private static final Set<PathType> MODULAR_PROCESSOR_PATH_TYPES = Set.of(JavaPathType.PROCESSOR_MODULES);
360 
361     /**
362      * Maps a processor type ID to its corresponding path types.
363      */
364     private static Set<PathType> processorPathTypesFor(String processorType) {
365         return switch (processorType) {
366             case Type.PROCESSOR -> PROCESSOR_PATH_TYPES;
367             case Type.CLASSPATH_PROCESSOR -> CLASSPATH_PROCESSOR_PATH_TYPES;
368             case Type.MODULAR_PROCESSOR -> MODULAR_PROCESSOR_PATH_TYPES;
369             default -> null;
370         };
371     }
372 
373     /**
374      * Returns whether the given set of path types contains at least one patch for a module.
375      */
376     private boolean containsPatches(Set<PathType> types) {
377         for (PathType type : types) {
378             if (type instanceof JavaPathType.Modular modular) {
379                 type = modular.rawType();
380             }
381             if (JavaPathType.PATCH_MODULE.equals(type)) {
382                 return true;
383             }
384         }
385         return false;
386     }
387 
388     /**
389      * Returns whether at least one previously added modular dependency contains a module of the given name.
390      *
391      * @param moduleName name of the module to search
392      */
393     private boolean containsModule(String moduleName) throws IOException {
394         for (Path path : dispatchedPaths.getOrDefault(JavaPathType.MODULES, Collections.emptyList())) {
395             if (cache.getModuleInfo(path).containsModule(moduleName)) {
396                 return true;
397             }
398         }
399         return false;
400     }
401 
402     /**
403      * Searches an artifact of the given group and artifact identifiers, and returns its path
404      *
405      * @param group the group identifier to search
406      * @param artifact the artifact identifier to search
407      * @return path to the desired artifact, or {@code null} if not found
408      */
409     private Path findArtifactPath(String group, String artifact) throws IOException {
410         for (Map.Entry<Dependency, Path> entry : dependencies.entrySet()) {
411             Dependency dep = entry.getKey();
412             if (group.equals(dep.getGroupId()) && artifact.equals(dep.getArtifactId())) {
413                 return entry.getValue();
414             }
415         }
416         return null;
417     }
418 
419     @Override
420     public DependencyResolverRequest getRequest() {
421         return request;
422     }
423 
424     @Override
425     public List<Exception> getExceptions() {
426         return Collections.unmodifiableList(exceptions);
427     }
428 
429     @Override
430     public Node getRoot() {
431         return root;
432     }
433 
434     @Override
435     public List<Node> getNodes() {
436         return Collections.unmodifiableList(nodes);
437     }
438 
439     @Override
440     public List<Path> getPaths() {
441         return Collections.unmodifiableList(paths);
442     }
443 
444     @Override
445     public Map<PathType, List<Path>> getDispatchedPaths() {
446         return Collections.unmodifiableMap(dispatchedPaths);
447     }
448 
449     @Override
450     public Map<Dependency, Path> getDependencies() {
451         return Collections.unmodifiableMap(dependencies);
452     }
453 
454     @Override
455     public Optional<ModuleDescriptor> getModuleDescriptor(Path dependency) throws IOException {
456         Object value = cache.getModuleInfo(dependency).descriptors.get(dependency);
457         return (value instanceof ModuleDescriptor moduleDescriptor) ? Optional.of(moduleDescriptor) : Optional.empty();
458     }
459 
460     @Override
461     public Optional<String> getModuleName(Path dependency) throws IOException {
462         return Optional.ofNullable(
463                 name(cache.getModuleInfo(dependency).descriptors.get(dependency)));
464     }
465 
466     /**
467      * Returns the module name for the given value of the {@link PathModularization#descriptors} map.
468      */
469     private static String name(final Object value) {
470         if (value instanceof String string) {
471             return string;
472         } else if (value instanceof ModuleDescriptor moduleDescriptor) {
473             return moduleDescriptor.name();
474         } else {
475             return null;
476         }
477     }
478 
479     @Override
480     public Optional<String> warningForFilenameBasedAutomodules() {
481         try {
482             return cache.warningForFilenameBasedAutomodules(dispatchedPaths.get(JavaPathType.MODULES));
483         } catch (IOException e) {
484             throw new DependencyResolverException("Cannot read module information.", e);
485         }
486     }
487 }