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