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