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.io.InputStream;
23 import java.io.UncheckedIOException;
24 import java.lang.module.ModuleDescriptor;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.jar.Attributes;
32 import java.util.jar.JarFile;
33 import java.util.jar.Manifest;
34 import java.util.stream.Stream;
35 import java.util.zip.ZipEntry;
36
37 import org.apache.maven.api.JavaPathType;
38 import org.apache.maven.api.annotations.Nonnull;
39
40 /**
41 * Information about the modules contained in a path element.
42 * The path element may be a JAR file or a directory. Directories may use either package hierarchy
43 * or module hierarchy, but not module source hierarchy. The latter is excluded because this class
44 * is for path elements of compiled codes.
45 */
46 class PathModularization {
47 /**
48 * A unique constant for all non-modular dependencies.
49 */
50 public static final PathModularization NONE = new PathModularization();
51
52 /**
53 * Name of the file to use as a sentinel value for deciding if a directory or a JAR is modular.
54 */
55 private static final String MODULE_INFO = "module-info.class";
56
57 /**
58 * The attribute for automatic module name in {@code META-INF/MANIFEST.MF} files.
59 */
60 private static final Attributes.Name AUTO_MODULE_NAME = new Attributes.Name("Automatic-Module-Name");
61
62 /**
63 * Filename of the path specified at construction time.
64 */
65 private final String filename;
66
67 /**
68 * Module information for the path specified at construction time.
69 * This map is usually either empty if no module was found, or a singleton map.
70 * It may however contain more than one entry if module hierarchy was detected,
71 * in which case there is one key per sub-directory.
72 *
73 * <p>This map may contain null values if the constructor was invoked with {@code resolve}
74 * parameter set to false. This is more efficient when only the module existence needs to
75 * be tested, and module descriptors are not needed.</p>
76 *
77 * @see #getModuleNames()
78 */
79 private final Map<Path, String> descriptors;
80
81 /**
82 * Whether module hierarchy was detected. If false, then package hierarchy is assumed.
83 * In a package hierarchy, the {@linkplain #descriptors} map has either zero or one entry.
84 * In a module hierarchy, the descriptors map may have an arbitrary number of entries,
85 * including one (so the map size cannot be used as a criterion).
86 *
87 * @see #isModuleHierarchy()
88 */
89 private final boolean isModuleHierarchy;
90
91 /**
92 * Constructs an empty instance for non-modular dependencies.
93 *
94 * @see #NONE
95 */
96 private PathModularization() {
97 filename = "(none)";
98 descriptors = Collections.emptyMap();
99 isModuleHierarchy = false;
100 }
101
102 /**
103 * Finds module information in the given JAR file, output directory, or test output directory.
104 * If no module is found, or if module information cannot be extracted, then this constructor
105 * builds an empty map.
106 *
107 * <p>If the {@code resolve} parameter value is {@code false}, then some or all map values may
108 * be null instead of the actual module name. This option can avoid the cost of reading module
109 * descriptors when only the modules existence needs to be verified.</p>
110 *
111 * <p><b>Algorithm:</b>
112 * If the given path is a directory, then there is a choice:
113 * </p>
114 * <ul>
115 * <li><b>Package hierarchy:</b> if a {@code module-info.class} file is found at the root,
116 * then builds a singleton map with the module name declared in that descriptor.</li>
117 * <li><b>Module hierarchy:</b> if {@code module-info.class} files are found in sub-directories,
118 * at a deep intentionally restricted to one level, then builds a map of module names found
119 * in the descriptor of each sub-directory.</li>
120 * </ul>
121 *
122 * Otherwise if the given path is a JAR file, then there is a choice:
123 * <ul>
124 * <li>If a {@code module-info.class} file is found in the root directory or in a
125 * {@code "META-INF/versions/{n}/"} subdirectory, builds a singleton map with
126 * the module name declared in that descriptor.</li>
127 * <li>Otherwise if an {@code "Automatic-Module-Name"} attribute is declared in the
128 * {@code META-INF/MANIFEST.MF} file, builds a singleton map with the value of that attribute.</li>
129 * </ul>
130 *
131 * Otherwise builds an empty map.
132 *
133 * @param path directory or JAR file to test
134 * @param resolve whether the module names are requested. If false, null values may be used instead
135 * @throws IOException if an error occurred while reading the JAR file or the module descriptor
136 */
137 PathModularization(Path path, boolean resolve) throws IOException {
138 filename = path.getFileName().toString();
139 if (Files.isDirectory(path)) {
140 /*
141 * Package hierarchy: only one module with descriptor at the root.
142 * This is the layout of output directories in projects using the
143 * classical (Java 8 and before) way to organize source files.
144 */
145 Path file = path.resolve(MODULE_INFO);
146 if (Files.isRegularFile(file)) {
147 String name = null;
148 if (resolve) {
149 try (InputStream in = Files.newInputStream(file)) {
150 name = getModuleName(in);
151 }
152 }
153 descriptors = Collections.singletonMap(file, name);
154 isModuleHierarchy = false;
155 return;
156 }
157 /*
158 * Module hierarchy: many modules, one per directory, with descriptor at the root of the sub-directory.
159 * This is the layout of output directories in projects using the new (Java 9 and later) way to organize
160 * source files.
161 */
162 if (Files.isDirectory(file)) {
163 Map<Path, String> names = new HashMap<>();
164 try (Stream<Path> subdirs = Files.list(file)) {
165 subdirs.filter(Files::isDirectory).forEach((subdir) -> {
166 Path mf = subdir.resolve(MODULE_INFO);
167 if (Files.isRegularFile(mf)) {
168 String name = null;
169 if (resolve) {
170 try (InputStream in = Files.newInputStream(mf)) {
171 name = getModuleName(in);
172 } catch (IOException e) {
173 throw new UncheckedIOException(e);
174 }
175 }
176 names.put(mf, name);
177 }
178 });
179 } catch (UncheckedIOException e) {
180 throw e.getCause();
181 }
182 if (!names.isEmpty()) {
183 descriptors = Collections.unmodifiableMap(names);
184 isModuleHierarchy = true;
185 return;
186 }
187 }
188 } else if (Files.isRegularFile(path)) {
189 /*
190 * JAR file: can contain only one module, with descriptor at the root.
191 * If no descriptor, the "Automatic-Module-Name" manifest attribute is
192 * taken as a fallback.
193 */
194 try (JarFile jar = new JarFile(path.toFile())) {
195 ZipEntry entry = jar.getEntry(MODULE_INFO);
196 if (entry != null) {
197 String name = null;
198 if (resolve) {
199 try (InputStream in = jar.getInputStream(entry)) {
200 name = getModuleName(in);
201 }
202 }
203 descriptors = Collections.singletonMap(path, name);
204 isModuleHierarchy = false;
205 return;
206 }
207 // No module descriptor, check manifest file.
208 Manifest mf = jar.getManifest();
209 if (mf != null) {
210 Object name = mf.getMainAttributes().get(AUTO_MODULE_NAME);
211 if (name instanceof String) {
212 descriptors = Collections.singletonMap(path, (String) name);
213 isModuleHierarchy = false;
214 return;
215 }
216 }
217 }
218 }
219 descriptors = Collections.emptyMap();
220 isModuleHierarchy = false;
221 }
222
223 /**
224 * {@return the module name declared in the given {@code module-info} descriptor}.
225 * The input stream may be for a file or for an entry in a JAR file.
226 */
227 @Nonnull
228 private static String getModuleName(InputStream in) throws IOException {
229 return ModuleDescriptor.read(in).name();
230 }
231
232 /**
233 * {@return the type of path detected}. The return value is {@link JavaPathType#MODULES}
234 * if the dependency is a modular JAR file or a directory containing module descriptor(s),
235 * or {@link JavaPathType#CLASSES} otherwise. A JAR file without module descriptor but with
236 * an "Automatic-Module-Name" manifest attribute is considered modular.
237 */
238 public JavaPathType getPathType() {
239 return descriptors.isEmpty() ? JavaPathType.CLASSES : JavaPathType.MODULES;
240 }
241
242 /**
243 * If the module has no name, adds the filename of the JAR file in the given collection.
244 * This method should be invoked for dependencies placed on {@link JavaPathType#MODULES}
245 * for preparing a warning asking to not deploy the build artifact on a public repository.
246 * If the module has an explicit name either with a {@code module-info.class} file or with
247 * an {@code "Automatic-Module-Name"} attribute in the {@code META-INF/MANIFEST.MF} file,
248 * then this method does nothing.
249 */
250 public void addIfFilenameBasedAutomodules(Collection<String> automodulesDetected) {
251 if (descriptors.isEmpty()) {
252 automodulesDetected.add(filename);
253 }
254 }
255
256 /**
257 * {@return whether module hierarchy was detected}. If {@code false}, then package hierarchy is assumed.
258 * In a package hierarchy, the {@linkplain #getModuleNames()} map of modules has either zero or one entry.
259 * In a module hierarchy, the descriptors map may have an arbitrary number of entries,
260 * including one (so the map size cannot be used as a criterion).
261 */
262 public boolean isModuleHierarchy() {
263 return isModuleHierarchy;
264 }
265
266 /**
267 * {@return the module names for the path specified at construction time}.
268 * This map is usually either empty if no module was found, or a singleton map.
269 * It may however contain more than one entry if module hierarchy was detected,
270 * in which case there is one key per sub-directory.
271 *
272 * <p>This map may contain null values if the constructor was invoked with {@code resolve}
273 * parameter set to false. This is more efficient when only the module existence needs to
274 * be tested, and module descriptors are not needed.</p>
275 */
276 @Nonnull
277 public Map<Path, String> getModuleNames() {
278 return descriptors;
279 }
280
281 /**
282 * {@return whether the dependency contains a module of the given name}.
283 */
284 public boolean containsModule(String name) {
285 return descriptors.containsValue(name);
286 }
287
288 /**
289 * {@return a string representation of this object for debugging purposes}.
290 * This string representation may change in any future version.
291 */
292 @Override
293 public String toString() {
294 return getClass().getCanonicalName() + '[' + filename + ']';
295 }
296 }