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.nio.file.Path;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Optional;
29 import java.util.Set;
30 import java.util.StringJoiner;
31 import java.util.function.Predicate;
32
33 import org.apache.maven.api.JavaPathType;
34 import org.apache.maven.api.PathType;
35
36 /**
37 * Cache of {@link PathModularization} instances computed for given {@link Path} elements.
38 * The cache is used for avoiding the need to reopen the same files many times when the
39 * same dependency is used for different scope. For example a path used for compilation
40 * is typically also used for tests.
41 */
42 final class PathModularizationCache {
43 /**
44 * Module information for each JAR file or output directories.
45 * Cached when first requested to avoid decoding the module descriptors multiple times.
46 *
47 * @see #getModuleInfo(Path)
48 */
49 private final Map<Path, PathModularization> moduleInfo;
50
51 /**
52 * Whether JAR files are modular. This map is redundant with {@link #moduleInfo},
53 * but cheaper to compute when the module names are not needed.
54 *
55 * @see #getPathType(Path)
56 */
57 private final Map<Path, PathType> pathTypes;
58
59 /**
60 * The target Java version for which the project is built.
61 * If unknown, it should be {@link Runtime#version()}.
62 */
63 private final Runtime.Version targetVersion;
64
65 /**
66 * Creates an initially empty cache.
67 *
68 * @param target the target Java release for which the project is built
69 */
70 PathModularizationCache(Runtime.Version target) {
71 moduleInfo = new HashMap<>();
72 pathTypes = new HashMap<>();
73 targetVersion = Objects.requireNonNull(target);
74 }
75
76 /**
77 * Gets module information for the given JAR file or output directory.
78 * Module descriptors are read when first requested, then cached.
79 */
80 PathModularization getModuleInfo(Path path) throws IOException {
81 PathModularization info = moduleInfo.get(path);
82 if (info == null) {
83 info = new PathModularization(path, targetVersion, true);
84 moduleInfo.put(path, info);
85 pathTypes.put(path, info.getPathType());
86 }
87 return info;
88 }
89
90 /**
91 * Returns {@link JavaPathType#MODULES} if the given JAR file or output directory is modular.
92 * This is used in heuristic rules for deciding whether to place a dependency on the class-path
93 * or on the module-path when the {@code "jar"} artifact type is used.
94 */
95 private PathType getPathType(Path path) throws IOException {
96 PathType type = pathTypes.get(path);
97 if (type == null) {
98 type = new PathModularization(path, targetVersion, false).getPathType();
99 pathTypes.put(path, type);
100 }
101 return type;
102 }
103
104 /**
105 * Selects the type of path where to place the given dependency.
106 * This method returns one of the values specified in the given collection.
107 * This method does not handle the patch-module paths, because the patches
108 * depend on which modules have been previously added on the module-paths.
109 *
110 * <p>If the dependency can be a constituent of both the class-path and the module-path,
111 * then the path type is determined by checking if the dependency is modular.</p>
112 *
113 * @param types types of path where a dependency can be placed
114 * @param filter filter the paths accepted by the tool which will consume the path
115 * @param path path to the JAR file or output directory of the dependency
116 * @return where to place the dependency, or an empty value if the placement cannot be determined
117 * @throws IOException if an error occurred while reading module information
118 */
119 Optional<PathType> selectPathType(Set<PathType> types, Predicate<PathType> filter, Path path) throws IOException {
120 PathType selected = null;
121 boolean classes = false;
122 boolean modules = false;
123 boolean unknown = false;
124 boolean processorClasses = false;
125 boolean processorModules = false;
126 for (PathType type : types) {
127 if (filter.test(type)) {
128 if (JavaPathType.CLASSES.equals(type)) {
129 classes = true;
130 } else if (JavaPathType.MODULES.equals(type)) {
131 modules = true;
132 } else if (JavaPathType.PROCESSOR_CLASSES.equals(type)) {
133 processorClasses = true;
134 } else if (JavaPathType.PROCESSOR_MODULES.equals(type)) {
135 processorModules = true;
136 } else {
137 unknown = true;
138 }
139 if (selected == null) {
140 selected = type;
141 } else if (unknown) {
142 // More than one filtered value, and we don't know how to handle at least one of them.
143 // TODO: add a plugin mechanism for allowing plugin to specify their selection algorithm.
144 return Optional.empty();
145 }
146 }
147 }
148 /*
149 * If the dependency can be both on the class-path and the module-path, we need to chose one of these.
150 * The choice done below will overwrite the current `selected` value because the latter is only the
151 * first value encountered in iteration order, which may be random.
152 */
153 if (classes | modules) {
154 if (classes & modules) {
155 selected = getPathType(path);
156 } else if (classes) {
157 selected = JavaPathType.CLASSES;
158 } else {
159 selected = JavaPathType.MODULES;
160 }
161 } else if (processorClasses & processorModules) {
162 selected = getPathType(path);
163 if (JavaPathType.CLASSES.equals(selected)) {
164 selected = JavaPathType.PROCESSOR_CLASSES;
165 } else if (JavaPathType.MODULES.equals(selected)) {
166 selected = JavaPathType.PROCESSOR_MODULES;
167 }
168 }
169 return Optional.ofNullable(selected);
170 }
171
172 /**
173 * If the module-path contains a filename-based auto-module, prepares a warning message.
174 * It is caller's responsibility to send the message to a logger.
175 *
176 * @param modulePaths content of the module path, or {@code null} if none
177 * @return warning message if at least one filename-based auto-module was found
178 * @throws IOException if an error occurred while reading module information
179 */
180 Optional<String> warningForFilenameBasedAutomodules(Collection<Path> modulePaths) throws IOException {
181 if (modulePaths == null) {
182 return Optional.empty();
183 }
184 var automodulesDetected = new ArrayList<String>();
185 for (Path p : modulePaths) {
186 getModuleInfo(p).addIfFilenameBasedAutomodules(automodulesDetected);
187 }
188 if (automodulesDetected.isEmpty()) {
189 return Optional.empty();
190 }
191 String lineSeparator = System.lineSeparator();
192 var joiner = new StringJoiner(
193 lineSeparator + " - ",
194 "Filename-based automodules detected on the module path: " + lineSeparator + " - ",
195 lineSeparator + "Please don't publish this project to a public artifact repository.");
196 automodulesDetected.forEach(joiner::add);
197 return Optional.of(joiner.toString());
198 }
199 }