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 }