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 }