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