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