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