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