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.services.DependencyResolverException;
40  import org.apache.maven.api.services.DependencyResolverRequest;
41  import org.apache.maven.api.services.DependencyResolverResult;
42  
43  /**
44   * The result of collecting dependencies with a dependency resolver.
45   * New instances are initially empty. Callers must populate with calls
46   * to the following methods, in that order:
47   *
48   * <ul>
49   *   <li>{@link #addOutputDirectory(Path, Path)} (optional)</li>
50   *   <li>{@link #addDependency(Node, Dependency, Predicate, Path)}</li>
51   * </ul>
52   *
53   * @see DefaultDependencyResolver#resolve(DependencyResolverRequest)
54   */
55  public class DefaultDependencyResolverResult implements DependencyResolverResult {
56      /**
57       * The corresponding request.
58       */
59      private final DependencyResolverRequest request;
60  
61      /**
62       * The exceptions that occurred while building the dependency graph.
63       */
64      private final List<Exception> exceptions;
65  
66      /**
67       * The root node of the dependency graph.
68       */
69      private final Node root;
70  
71      /**
72       * The ordered list of the flattened dependency nodes.
73       */
74      private final List<Node> nodes;
75  
76      /**
77       * The file paths of all dependencies, regardless on which Java tool option those paths should be placed.
78       */
79      private final List<Path> paths;
80  
81      /**
82       * The file paths of all dependencies, dispatched according the Java options where to place them.
83       */
84      private final Map<PathType, List<Path>> dispatchedPaths;
85  
86      /**
87       * The dependencies together with the path to each dependency.
88       */
89      private final Map<Dependency, Path> dependencies;
90  
91      /**
92       * Information about modules in the main output. This field is initially null and is set to a non-null
93       * value when the output directories have been set, or when it is too late for setting them.
94       */
95      private PathModularization outputModules;
96  
97      /**
98       * Cache of module information about each dependency.
99       */
100     private final PathModularizationCache cache;
101 
102     /**
103      * Creates an initially empty result with a temporary cache.
104      * Callers should add path elements by calls to {@link #addDependency(Node, Dependency, Predicate, Path)}.
105      *
106      * <p><b>WARNING: this constructor may be removed in a future Maven release.</b>
107      * The reason is because {@code DefaultDependencyResolverResult} needs a cache, which should
108      * preferably be session-wide. How to manage such caches has not yet been clarified.</p>
109      *
110      * @param request the corresponding request
111      * @param exceptions the exceptions that occurred while building the dependency graph
112      * @param root the root node of the dependency graph
113      * @param count estimated number of dependencies
114      */
115     public DefaultDependencyResolverResult(
116             DependencyResolverRequest request, List<Exception> exceptions, Node root, int count) {
117         this(
118                 request,
119                 new PathModularizationCache(DefaultDependencyResolver.getTargetVersion(request)),
120                 exceptions,
121                 root,
122                 count);
123     }
124 
125     /**
126      * Creates an initially empty result. Callers should add path elements by calls
127      * to {@link #addDependency(Node, Dependency, Predicate, Path)}.
128      *
129      * @param request the corresponding request
130      * @param cache cache of module information about each dependency
131      * @param exceptions the exceptions that occurred while building the dependency graph
132      * @param root the root node of the dependency graph
133      * @param count estimated number of dependencies
134      */
135     DefaultDependencyResolverResult(
136             DependencyResolverRequest request,
137             PathModularizationCache cache,
138             List<Exception> exceptions,
139             Node root,
140             int count) {
141         this.request = request;
142         this.cache = Objects.requireNonNull(cache);
143         this.exceptions = exceptions;
144         this.root = root;
145         nodes = new ArrayList<>(count);
146         paths = new ArrayList<>(count);
147         dispatchedPaths = new LinkedHashMap<>();
148         dependencies = new LinkedHashMap<>(count + count / 3);
149     }
150 
151     /**
152      * Adds the given path element to the specified type of path.
153      *
154      * @param type the type of path (class-path, module-path, …)
155      * @param path the path element to add
156      */
157     private void addPathElement(PathType type, Path path) {
158         dispatchedPaths.computeIfAbsent(type, (t) -> new ArrayList<>()).add(path);
159     }
160 
161     /**
162      * Adds main and test output directories to the result. This method adds the main output directory
163      * to the module path if it contains a {@code module-info.class}, or to the class path otherwise.
164      * For the test output directory, the rules are more complex and are governed by the fact that
165      * Java does not accept the placement of two modules of the same name on the module path.
166      * So the modular test output directory usually needs to be placed in a {@code --path-module} option.
167      *
168      * <ul>
169      *   <li>If the test output directory is modular, then:
170      *     <ul>
171      *       <li>If a test module name is identical to a main module name,
172      *           place the test directory in a {@code --patch-module} option.</li>
173      *       <li>Otherwise, place the test directory on the module path. However, this case
174      *           (a module existing only in test output, not in main output) should be uncommon.</li>
175      *     </ul>
176      *   </li>
177      *   <li>Otherwise (test output contains no module information), then:
178      *     <ul>
179      *       <li>If the main output is on the module path, place the test output
180      *           on a {@code --patch-module} option.</li>
181      *       <li>Otherwise (main output on the class path), place the test output on the class path too.</li>
182      *     </ul>
183      *   </li>
184      * </ul>
185      *
186      * This method must be invoked before {@link #addDependency(Node, Dependency, Predicate, Path)}
187      * if output directories are desired on the class path or module path.
188      * This method can be invoked at most once.
189      *
190      * @param main the main output directory, or {@code null} if none
191      * @param test the test output directory, or {@code null} if none
192      * @throws IOException if an error occurred while reading module information
193      *
194      * TODO: this is currently not called. This is intended for use by Surefire and may move there.
195      */
196     void addOutputDirectory(Path main, Path test) throws IOException {
197         if (outputModules != null) {
198             throw new IllegalStateException("Output directories must be set first and only once.");
199         }
200         if (main != null) {
201             outputModules = cache.getModuleInfo(main);
202             addPathElement(outputModules.getPathType(), main);
203         } else {
204             outputModules = PathModularization.NONE;
205         }
206         if (test != null) {
207             boolean addToClasspath = true;
208             PathModularization testModules = cache.getModuleInfo(test);
209             boolean isModuleHierarchy = outputModules.isModuleHierarchy || testModules.isModuleHierarchy;
210             for (Object value : outputModules.descriptors.values()) {
211                 String moduleName = name(value);
212                 Path subdir = test;
213                 if (isModuleHierarchy) {
214                     // If module hierarchy is used, the directory names shall be the module names.
215                     Path path = test.resolve(moduleName);
216                     if (!Files.isDirectory(path)) {
217                         // Main module without tests. It is okay.
218                         continue;
219                     }
220                     subdir = path;
221                 }
222                 // When the same module is found in main and test output, the latter is patching the former.
223                 addPathElement(JavaPathType.patchModule(moduleName), subdir);
224                 addToClasspath = false;
225             }
226             /*
227              * If the test output directory provides some modules of its own, add them.
228              * Except for this unusual case, tests should never be added to the module-path.
229              */
230             for (Map.Entry<Path, Object> entry : testModules.descriptors.entrySet()) {
231                 if (!outputModules.containsModule(name(entry.getValue()))) {
232                     addPathElement(JavaPathType.MODULES, entry.getKey());
233                     addToClasspath = false;
234                 }
235             }
236             if (addToClasspath) {
237                 addPathElement(JavaPathType.CLASSES, test);
238             }
239         }
240     }
241 
242     /**
243      * Adds a dependency node to the result.
244      *
245      * @param node the dependency node
246      */
247     void addNode(Node node) {
248         nodes.add(node);
249     }
250 
251     /**
252      * Adds a dependency to the result. This method populates the {@link #nodes}, {@link #paths},
253      * {@link #dispatchedPaths} and {@link #dependencies} collections with the given arguments.
254      *
255      * @param node the dependency node
256      * @param dep the dependency for the given node, or {@code null} if none
257      * @param filter filter the paths accepted by the tool which will consume the path.
258      * @param path the path to the dependency, or {@code null} if the dependency was null
259      * @throws IOException if an error occurred while reading module information
260      */
261     void addDependency(Node node, Dependency dep, Predicate<PathType> filter, Path path) throws IOException {
262         nodes.add(node);
263         if (dep == null) {
264             return;
265         }
266         if (dependencies.put(dep, path) != null) {
267             throw new IllegalStateException("Duplicated key: " + dep);
268         }
269         if (path == null) {
270             return;
271         }
272         paths.add(path);
273         /*
274          * Dispatch the dependency to class path, module path, patch-module path, etc.
275          * according the dependency properties. We need to process patch-module first,
276          * because this type depends on whether a module of the same name has already
277          * been added on the module-type.
278          */
279         // DependencyProperties properties = dep.getDependencyProperties();
280         Set<PathType> pathTypes = dep.getType().getPathTypes();
281         if (containsPatches(pathTypes)) {
282             if (outputModules == null) {
283                 // For telling users that it is too late for setting the output directory.
284                 outputModules = PathModularization.NONE;
285             }
286             PathType type = null;
287             for (Map.Entry<Path, Object> info :
288                     cache.getModuleInfo(path).descriptors.entrySet()) {
289                 String moduleName = name(info.getValue());
290                 type = JavaPathType.patchModule(moduleName);
291                 if (!containsModule(moduleName)) {
292                     /*
293                      * Not patching an existing module. This case should be unusual. If it nevertheless
294                      * happens, add to class path or module path if allowed, or keep patching otherwise.
295                      * The latter case (keep patching) is okay if the main module will be defined later.
296                      */
297                     type = cache.selectPathType(pathTypes, filter, path).orElse(type);
298                 }
299                 addPathElement(type, info.getKey());
300                 // There is usually no more than one element, but nevertheless allow multi-modules.
301             }
302             /*
303              * If the dependency has no module information, search for an artifact of the same groupId
304              * and artifactId. If one is found, we are patching that module. If none is found, add the
305              * dependency as a normal dependency.
306              */
307             if (type == null) {
308                 Path main = findArtifactPath(dep.getGroupId(), dep.getArtifactId());
309                 if (main != null) {
310                     for (Map.Entry<Path, Object> info :
311                             cache.getModuleInfo(main).descriptors.entrySet()) {
312                         type = JavaPathType.patchModule(name(info.getValue()));
313                         addPathElement(type, info.getKey());
314                         // There is usually no more than one element, but nevertheless allow multi-modules.
315                     }
316                 }
317             }
318             if (type != null) {
319                 return; // Dependency added, we are done.
320             }
321         }
322         addPathElement(cache.selectPathType(pathTypes, filter, path).orElse(PathType.UNRESOLVED), path);
323     }
324 
325     /**
326      * Returns whether the given set of path types contains at least one patch for a module.
327      */
328     private boolean containsPatches(Set<PathType> types) {
329         for (PathType type : types) {
330             if (type instanceof JavaPathType.Modular modular) {
331                 type = modular.rawType();
332             }
333             if (JavaPathType.PATCH_MODULE.equals(type)) {
334                 return true;
335             }
336         }
337         return false;
338     }
339 
340     /**
341      * Returns whether at least one previously added modular dependency contains a module of the given name.
342      *
343      * @param moduleName name of the module to search
344      */
345     private boolean containsModule(String moduleName) throws IOException {
346         for (Path path : dispatchedPaths.getOrDefault(JavaPathType.MODULES, Collections.emptyList())) {
347             if (cache.getModuleInfo(path).containsModule(moduleName)) {
348                 return true;
349             }
350         }
351         return false;
352     }
353 
354     /**
355      * Searches an artifact of the given group and artifact identifiers, and returns its path
356      *
357      * @param group the group identifier to search
358      * @param artifact the artifact identifier to search
359      * @return path to the desired artifact, or {@code null} if not found
360      */
361     private Path findArtifactPath(String group, String artifact) throws IOException {
362         for (Map.Entry<Dependency, Path> entry : dependencies.entrySet()) {
363             Dependency dep = entry.getKey();
364             if (group.equals(dep.getGroupId()) && artifact.equals(dep.getArtifactId())) {
365                 return entry.getValue();
366             }
367         }
368         return null;
369     }
370 
371     @Override
372     public DependencyResolverRequest getRequest() {
373         return request;
374     }
375 
376     @Override
377     public List<Exception> getExceptions() {
378         return Collections.unmodifiableList(exceptions);
379     }
380 
381     @Override
382     public Node getRoot() {
383         return root;
384     }
385 
386     @Override
387     public List<Node> getNodes() {
388         return Collections.unmodifiableList(nodes);
389     }
390 
391     @Override
392     public List<Path> getPaths() {
393         return Collections.unmodifiableList(paths);
394     }
395 
396     @Override
397     public Map<PathType, List<Path>> getDispatchedPaths() {
398         return Collections.unmodifiableMap(dispatchedPaths);
399     }
400 
401     @Override
402     public Map<Dependency, Path> getDependencies() {
403         return Collections.unmodifiableMap(dependencies);
404     }
405 
406     @Override
407     public Optional<ModuleDescriptor> getModuleDescriptor(Path dependency) throws IOException {
408         Object value = cache.getModuleInfo(dependency).descriptors.get(dependency);
409         return (value instanceof ModuleDescriptor moduleDescriptor) ? Optional.of(moduleDescriptor) : Optional.empty();
410     }
411 
412     @Override
413     public Optional<String> getModuleName(Path dependency) throws IOException {
414         return Optional.ofNullable(
415                 name(cache.getModuleInfo(dependency).descriptors.get(dependency)));
416     }
417 
418     /**
419      * Returns the module name for the given value of the {@link PathModularization#descriptors} map.
420      */
421     private static String name(final Object value) {
422         if (value instanceof String string) {
423             return string;
424         } else if (value instanceof ModuleDescriptor moduleDescriptor) {
425             return moduleDescriptor.name();
426         } else {
427             return null;
428         }
429     }
430 
431     @Override
432     public Optional<String> warningForFilenameBasedAutomodules() {
433         try {
434             return cache.warningForFilenameBasedAutomodules(dispatchedPaths.get(JavaPathType.MODULES));
435         } catch (IOException e) {
436             throw new DependencyResolverException("Cannot read module information.", e);
437         }
438     }
439 }