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