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.Objects;
31 import java.util.Optional;
32 import java.util.Set;
33 import java.util.function.Predicate;
34
35 import org.apache.maven.api.Dependency;
36 import org.apache.maven.api.JavaPathType;
37 import org.apache.maven.api.Node;
38 import org.apache.maven.api.PathType;
39 import org.apache.maven.api.Type;
40 import org.apache.maven.api.services.DependencyResolverException;
41 import org.apache.maven.api.services.DependencyResolverRequest;
42 import org.apache.maven.api.services.DependencyResolverResult;
43 import org.apache.maven.impl.resolver.artifact.MavenArtifactProperties;
44
45 /**
46 * The result of collecting dependencies with a dependency resolver.
47 * New instances are initially empty. Callers must populate with calls
48 * to the following methods, in that order:
49 *
50 * <ul>
51 * <li>{@link #addOutputDirectory(Path, Path)} (optional)</li>
52 * <li>{@link #addDependency(Node, Dependency, Predicate, Path)}</li>
53 * </ul>
54 *
55 * @see DefaultDependencyResolver#resolve(DependencyResolverRequest)
56 */
57 public class DefaultDependencyResolverResult implements DependencyResolverResult {
58 /**
59 * The corresponding request.
60 */
61 private final DependencyResolverRequest request;
62
63 /**
64 * The exceptions that occurred while building the dependency graph.
65 */
66 private final List<Exception> exceptions;
67
68 /**
69 * The root node of the dependency graph.
70 */
71 private final Node root;
72
73 /**
74 * The ordered list of the flattened dependency nodes.
75 */
76 private final List<Node> nodes;
77
78 /**
79 * The file paths of all dependencies, regardless on which Java tool option those paths should be placed.
80 */
81 private final List<Path> paths;
82
83 /**
84 * The file paths of all dependencies, dispatched according the Java options where to place them.
85 */
86 private final Map<PathType, List<Path>> dispatchedPaths;
87
88 /**
89 * The dependencies together with the path to each dependency.
90 */
91 private final Map<Dependency, Path> dependencies;
92
93 /**
94 * Information about modules in the main output. This field is initially null and is set to a non-null
95 * value when the output directories have been set, or when it is too late for setting them.
96 */
97 private PathModularization outputModules;
98
99 /**
100 * Cache of module information about each dependency.
101 */
102 private final PathModularizationCache cache;
103
104 /**
105 * Creates an initially empty result with a temporary cache.
106 * Callers should add path elements by calls to {@link #addDependency(Node, Dependency, Predicate, Path)}.
107 *
108 * <p><b>WARNING: this constructor may be removed in a future Maven release.</b>
109 * The reason is because {@code DefaultDependencyResolverResult} needs a cache, which should
110 * preferably be session-wide. How to manage such caches has not yet been clarified.</p>
111 *
112 * @param request the corresponding request
113 * @param exceptions the exceptions that occurred while building the dependency graph
114 * @param root the root node of the dependency graph
115 * @param count estimated number of dependencies
116 */
117 public DefaultDependencyResolverResult(
118 DependencyResolverRequest request, List<Exception> exceptions, Node root, int count) {
119 this(
120 request,
121 new PathModularizationCache(DefaultDependencyResolver.getTargetVersion(request)),
122 exceptions,
123 root,
124 count);
125 }
126
127 /**
128 * Creates an initially empty result. Callers should add path elements by calls
129 * to {@link #addDependency(Node, Dependency, Predicate, Path)}.
130 *
131 * @param request the corresponding request
132 * @param cache cache of module information about each dependency
133 * @param exceptions the exceptions that occurred while building the dependency graph
134 * @param root the root node of the dependency graph
135 * @param count estimated number of dependencies
136 */
137 DefaultDependencyResolverResult(
138 DependencyResolverRequest request,
139 PathModularizationCache cache,
140 List<Exception> exceptions,
141 Node root,
142 int count) {
143 this.request = request;
144 this.cache = Objects.requireNonNull(cache);
145 this.exceptions = exceptions;
146 this.root = root;
147 nodes = new ArrayList<>(count);
148 paths = new ArrayList<>(count);
149 dispatchedPaths = new LinkedHashMap<>();
150 dependencies = new LinkedHashMap<>(count + count / 3);
151 }
152
153 /**
154 * Adds the given path element to the specified type of path.
155 *
156 * @param type the type of path (class-path, module-path, …)
157 * @param path the path element to add
158 */
159 private void addPathElement(PathType type, Path path) {
160 dispatchedPaths.computeIfAbsent(type, (t) -> new ArrayList<>()).add(path);
161 }
162
163 /**
164 * Adds main and test output directories to the result. This method adds the main output directory
165 * to the module path if it contains a {@code module-info.class}, or to the class path otherwise.
166 * For the test output directory, the rules are more complex and are governed by the fact that
167 * Java does not accept the placement of two modules of the same name on the module path.
168 * So the modular test output directory usually needs to be placed in a {@code --path-module} option.
169 *
170 * <ul>
171 * <li>If the test output directory is modular, then:
172 * <ul>
173 * <li>If a test module name is identical to a main module name,
174 * place the test directory in a {@code --patch-module} option.</li>
175 * <li>Otherwise, place the test directory on the module path. However, this case
176 * (a module existing only in test output, not in main output) should be uncommon.</li>
177 * </ul>
178 * </li>
179 * <li>Otherwise (test output contains no module information), then:
180 * <ul>
181 * <li>If the main output is on the module path, place the test output
182 * on a {@code --patch-module} option.</li>
183 * <li>Otherwise (main output on the class path), place the test output on the class path too.</li>
184 * </ul>
185 * </li>
186 * </ul>
187 *
188 * This method must be invoked before {@link #addDependency(Node, Dependency, Predicate, Path)}
189 * if output directories are desired on the class path or module path.
190 * This method can be invoked at most once.
191 *
192 * @param main the main output directory, or {@code null} if none
193 * @param test the test output directory, or {@code null} if none
194 * @throws IOException if an error occurred while reading module information
195 *
196 * TODO: this is currently not called. This is intended for use by Surefire and may move there.
197 */
198 void addOutputDirectory(Path main, Path test) throws IOException {
199 if (outputModules != null) {
200 throw new IllegalStateException("Output directories must be set first and only once.");
201 }
202 if (main != null) {
203 outputModules = cache.getModuleInfo(main);
204 addPathElement(outputModules.getPathType(), main);
205 } else {
206 outputModules = PathModularization.NONE;
207 }
208 if (test != null) {
209 boolean addToClasspath = true;
210 PathModularization testModules = cache.getModuleInfo(test);
211 boolean isModuleHierarchy = outputModules.isModuleHierarchy || testModules.isModuleHierarchy;
212 for (Object value : outputModules.descriptors.values()) {
213 String moduleName = name(value);
214 Path subdir = test;
215 if (isModuleHierarchy) {
216 // If module hierarchy is used, the directory names shall be the module names.
217 Path path = test.resolve(moduleName);
218 if (!Files.isDirectory(path)) {
219 // Main module without tests. It is okay.
220 continue;
221 }
222 subdir = path;
223 }
224 // When the same module is found in main and test output, the latter is patching the former.
225 addPathElement(JavaPathType.patchModule(moduleName), subdir);
226 addToClasspath = false;
227 }
228 /*
229 * If the test output directory provides some modules of its own, add them.
230 * Except for this unusual case, tests should never be added to the module-path.
231 */
232 for (Map.Entry<Path, Object> entry : testModules.descriptors.entrySet()) {
233 if (!outputModules.containsModule(name(entry.getValue()))) {
234 addPathElement(JavaPathType.MODULES, entry.getKey());
235 addToClasspath = false;
236 }
237 }
238 if (addToClasspath) {
239 addPathElement(JavaPathType.CLASSES, test);
240 }
241 }
242 }
243
244 /**
245 * Adds a dependency node to the result.
246 *
247 * @param node the dependency node
248 */
249 void addNode(Node node) {
250 nodes.add(node);
251 }
252
253 /**
254 * Adds a dependency to the result. This method populates the {@link #nodes}, {@link #paths},
255 * {@link #dispatchedPaths} and {@link #dependencies} collections with the given arguments.
256 *
257 * @param node the dependency node
258 * @param dep the dependency for the given node, or {@code null} if none
259 * @param filter filter the paths accepted by the tool which will consume the path.
260 * @param path the path to the dependency, or {@code null} if the dependency was null
261 * @throws IOException if an error occurred while reading module information
262 */
263 void addDependency(Node node, Dependency dep, Predicate<PathType> filter, Path path) throws IOException {
264 nodes.add(node);
265 if (dep == null) {
266 return;
267 }
268 if (dependencies.put(dep, path) != null) {
269 throw new IllegalStateException("Duplicated key: " + dep);
270 }
271 if (path == null) {
272 return;
273 }
274 paths.add(path);
275 /*
276 * Dispatch the dependency to class path, module path, patch-module path, etc.
277 * according the dependency properties. We need to process patch-module first,
278 * because this type depends on whether a module of the same name has already
279 * been added on the module-type.
280 */
281 // DependencyProperties properties = dep.getDependencyProperties();
282 Set<PathType> pathTypes = dep.getType().getPathTypes();
283 if (containsPatches(pathTypes)) {
284 if (outputModules == null) {
285 // For telling users that it is too late for setting the output directory.
286 outputModules = PathModularization.NONE;
287 }
288 PathType type = null;
289 for (Map.Entry<Path, Object> info :
290 cache.getModuleInfo(path).descriptors.entrySet()) {
291 String moduleName = name(info.getValue());
292 type = JavaPathType.patchModule(moduleName);
293 if (!containsModule(moduleName)) {
294 /*
295 * Not patching an existing module. This case should be unusual. If it nevertheless
296 * happens, add to class path or module path if allowed, or keep patching otherwise.
297 * The latter case (keep patching) is okay if the main module will be defined later.
298 */
299 type = cache.selectPathType(pathTypes, filter, path).orElse(type);
300 }
301 addPathElement(type, info.getKey());
302 // There is usually no more than one element, but nevertheless allow multi-modules.
303 }
304 /*
305 * If the dependency has no module information, search for an artifact of the same groupId
306 * and artifactId. If one is found, we are patching that module. If none is found, add the
307 * dependency as a normal dependency.
308 */
309 if (type == null) {
310 Path main = findArtifactPath(dep.getGroupId(), dep.getArtifactId());
311 if (main != null) {
312 for (Map.Entry<Path, Object> info :
313 cache.getModuleInfo(main).descriptors.entrySet()) {
314 type = JavaPathType.patchModule(name(info.getValue()));
315 addPathElement(type, info.getKey());
316 // There is usually no more than one element, but nevertheless allow multi-modules.
317 }
318 }
319 }
320 if (type != null) {
321 return; // Dependency added, we are done.
322 }
323 }
324 addPathElement(cache.selectPathType(pathTypes, filter, path).orElse(PathType.UNRESOLVED), path);
325 // If the artifact is also needed on a processor path (because it's a transitive dep
326 // of a processor AND a direct dep with a different type), add it to the processor path too.
327 addProcessorPathIfNeeded(node, filter, path);
328 }
329
330 /**
331 * Checks if the artifact has a {@link MavenArtifactProperties#PROCESSOR_TYPE} property
332 * and, if so, also adds it to the corresponding processor path. This handles the case
333 * where an artifact is both a regular dependency (e.g., modular-jar on --module-path)
334 * and a transitive dependency of a processor (needs --processor-module-path).
335 */
336 private void addProcessorPathIfNeeded(Node node, Predicate<PathType> filter, Path path) throws IOException {
337 if (!(node instanceof AbstractNode abstractNode)) {
338 return;
339 }
340 org.eclipse.aether.artifact.Artifact aetherArtifact =
341 abstractNode.getDependencyNode().getArtifact();
342 if (aetherArtifact == null) {
343 return;
344 }
345 String processorType = aetherArtifact.getProperty(MavenArtifactProperties.PROCESSOR_TYPE, null);
346 if (processorType == null) {
347 return;
348 }
349 Set<PathType> processorPathTypes = processorPathTypesFor(processorType);
350 if (processorPathTypes != null) {
351 cache.selectPathType(processorPathTypes, filter, path).ifPresent(pt -> addPathElement(pt, path));
352 }
353 }
354
355 // Path type sets for processor types — must stay in sync with DefaultTypeProvider
356 private static final Set<PathType> PROCESSOR_PATH_TYPES =
357 Set.of(JavaPathType.PROCESSOR_CLASSES, JavaPathType.PROCESSOR_MODULES);
358 private static final Set<PathType> CLASSPATH_PROCESSOR_PATH_TYPES = Set.of(JavaPathType.PROCESSOR_CLASSES);
359 private static final Set<PathType> MODULAR_PROCESSOR_PATH_TYPES = Set.of(JavaPathType.PROCESSOR_MODULES);
360
361 /**
362 * Maps a processor type ID to its corresponding path types.
363 */
364 private static Set<PathType> processorPathTypesFor(String processorType) {
365 return switch (processorType) {
366 case Type.PROCESSOR -> PROCESSOR_PATH_TYPES;
367 case Type.CLASSPATH_PROCESSOR -> CLASSPATH_PROCESSOR_PATH_TYPES;
368 case Type.MODULAR_PROCESSOR -> MODULAR_PROCESSOR_PATH_TYPES;
369 default -> null;
370 };
371 }
372
373 /**
374 * Returns whether the given set of path types contains at least one patch for a module.
375 */
376 private boolean containsPatches(Set<PathType> types) {
377 for (PathType type : types) {
378 if (type instanceof JavaPathType.Modular modular) {
379 type = modular.rawType();
380 }
381 if (JavaPathType.PATCH_MODULE.equals(type)) {
382 return true;
383 }
384 }
385 return false;
386 }
387
388 /**
389 * Returns whether at least one previously added modular dependency contains a module of the given name.
390 *
391 * @param moduleName name of the module to search
392 */
393 private boolean containsModule(String moduleName) throws IOException {
394 for (Path path : dispatchedPaths.getOrDefault(JavaPathType.MODULES, Collections.emptyList())) {
395 if (cache.getModuleInfo(path).containsModule(moduleName)) {
396 return true;
397 }
398 }
399 return false;
400 }
401
402 /**
403 * Searches an artifact of the given group and artifact identifiers, and returns its path
404 *
405 * @param group the group identifier to search
406 * @param artifact the artifact identifier to search
407 * @return path to the desired artifact, or {@code null} if not found
408 */
409 private Path findArtifactPath(String group, String artifact) throws IOException {
410 for (Map.Entry<Dependency, Path> entry : dependencies.entrySet()) {
411 Dependency dep = entry.getKey();
412 if (group.equals(dep.getGroupId()) && artifact.equals(dep.getArtifactId())) {
413 return entry.getValue();
414 }
415 }
416 return null;
417 }
418
419 @Override
420 public DependencyResolverRequest getRequest() {
421 return request;
422 }
423
424 @Override
425 public List<Exception> getExceptions() {
426 return Collections.unmodifiableList(exceptions);
427 }
428
429 @Override
430 public Node getRoot() {
431 return root;
432 }
433
434 @Override
435 public List<Node> getNodes() {
436 return Collections.unmodifiableList(nodes);
437 }
438
439 @Override
440 public List<Path> getPaths() {
441 return Collections.unmodifiableList(paths);
442 }
443
444 @Override
445 public Map<PathType, List<Path>> getDispatchedPaths() {
446 return Collections.unmodifiableMap(dispatchedPaths);
447 }
448
449 @Override
450 public Map<Dependency, Path> getDependencies() {
451 return Collections.unmodifiableMap(dependencies);
452 }
453
454 @Override
455 public Optional<ModuleDescriptor> getModuleDescriptor(Path dependency) throws IOException {
456 Object value = cache.getModuleInfo(dependency).descriptors.get(dependency);
457 return (value instanceof ModuleDescriptor moduleDescriptor) ? Optional.of(moduleDescriptor) : Optional.empty();
458 }
459
460 @Override
461 public Optional<String> getModuleName(Path dependency) throws IOException {
462 return Optional.ofNullable(
463 name(cache.getModuleInfo(dependency).descriptors.get(dependency)));
464 }
465
466 /**
467 * Returns the module name for the given value of the {@link PathModularization#descriptors} map.
468 */
469 private static String name(final Object value) {
470 if (value instanceof String string) {
471 return string;
472 } else if (value instanceof ModuleDescriptor moduleDescriptor) {
473 return moduleDescriptor.name();
474 } else {
475 return null;
476 }
477 }
478
479 @Override
480 public Optional<String> warningForFilenameBasedAutomodules() {
481 try {
482 return cache.warningForFilenameBasedAutomodules(dispatchedPaths.get(JavaPathType.MODULES));
483 } catch (IOException e) {
484 throw new DependencyResolverException("Cannot read module information.", e);
485 }
486 }
487 }