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