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