View Javadoc
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.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  final 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>Values are instances of either {@link ModuleDescriptor} or {@link String}.
74       * The latter case happens when a JAR file has no {@code module-info.class} entry
75       * but has an automatic name declared in {@code META-INF/MANIFEST.MF}.</p>
76       *
77       * <p>This map may contain null values if the constructor was invoked with {@code resolve}
78       * parameter set to false. This is more efficient when only the module existence needs to
79       * be tested, and module descriptors are not needed.</p>
80       */
81      @Nonnull
82      final Map<Path, Object> descriptors;
83  
84      /**
85       * Whether module hierarchy was detected. If false, then package hierarchy is assumed.
86       * In a package hierarchy, the {@linkplain #descriptors} map has either zero or one entry.
87       * In a module hierarchy, the descriptors map may have an arbitrary number of entries,
88       * including one (so the map size cannot be used as a criterion).
89       */
90      final boolean isModuleHierarchy;
91  
92      /**
93       * Constructs an empty instance for non-modular dependencies.
94       *
95       * @see #NONE
96       */
97      private PathModularization() {
98          filename = "(none)";
99          descriptors = Collections.emptyMap();
100         isModuleHierarchy = false;
101     }
102 
103     /**
104      * Finds module information in the given JAR file, output directory, or test output directory.
105      * If no module is found, or if module information cannot be extracted, then this constructor
106      * builds an empty map.
107      *
108      * <p>If the {@code resolve} parameter value is {@code false}, then some or all map values may
109      * be null instead of the actual module name. This option can avoid the cost of reading module
110      * descriptors when only the modules existence needs to be verified.</p>
111      *
112      * <p><b>Algorithm:</b>
113      * If the given path is a directory, then there is a choice:
114      * </p>
115      * <ul>
116      *   <li><b>Package hierarchy:</b> if a {@code module-info.class} file is found at the root,
117      *       then builds a singleton map with the module name declared in that descriptor.</li>
118      *   <li><b>Module hierarchy:</b> if {@code module-info.class} files are found in sub-directories,
119      *       at a deep intentionally restricted to one level, then builds a map of module names found
120      *       in the descriptor of each sub-directory.</li>
121      * </ul>
122      *
123      * Otherwise if the given path is a JAR file, then there is a choice:
124      * <ul>
125      *   <li>If a {@code module-info.class} file is found in the root directory or in a
126      *       {@code "META-INF/versions/{n}/"} subdirectory, builds a singleton map with
127      *       the module name declared in that descriptor.</li>
128      *   <li>Otherwise if an {@code "Automatic-Module-Name"} attribute is declared in the
129      *       {@code META-INF/MANIFEST.MF} file, builds a singleton map with the value of that attribute.</li>
130      * </ul>
131      *
132      * Otherwise builds an empty map.
133      *
134      * @param path directory or JAR file to test
135      * @param target the target Java release for which the project is built
136      * @param resolve whether the module names are requested. If false, null values may be used instead
137      * @throws IOException if an error occurred while reading the JAR file or the module descriptor
138      */
139     PathModularization(Path path, Runtime.Version target, boolean resolve) throws IOException {
140         filename = path.getFileName().toString();
141         if (Files.isDirectory(path)) {
142             /*
143              * Package hierarchy: only one module with descriptor at the root.
144              * This is the layout of output directories in projects using the
145              * classical (Java 8 and before) way to organize source files.
146              */
147             Path file = path.resolve(MODULE_INFO);
148             if (Files.isRegularFile(file)) {
149                 ModuleDescriptor descriptor = null;
150                 if (resolve) {
151                     try (InputStream in = Files.newInputStream(file)) {
152                         descriptor = ModuleDescriptor.read(in);
153                     }
154                 }
155                 descriptors = Collections.singletonMap(file, descriptor);
156                 isModuleHierarchy = false;
157                 return;
158             }
159             /*
160              * Module hierarchy: many modules, one per directory, with descriptor at the root of the sub-directory.
161              * This is the layout of output directories in projects using the new (Java 9 and later) way to organize
162              * source files.
163              */
164             if (Files.isDirectory(file)) {
165                 var multi = new HashMap<Path, ModuleDescriptor>();
166                 try (Stream<Path> subdirs = Files.list(file)) {
167                     subdirs.filter(Files::isDirectory).forEach((subdir) -> {
168                         Path mf = subdir.resolve(MODULE_INFO);
169                         if (Files.isRegularFile(mf)) {
170                             ModuleDescriptor descriptor = null;
171                             if (resolve) {
172                                 try (InputStream in = Files.newInputStream(mf)) {
173                                     descriptor = ModuleDescriptor.read(in);
174                                 } catch (IOException e) {
175                                     throw new UncheckedIOException(e);
176                                 }
177                             }
178                             multi.put(mf, descriptor);
179                         }
180                     });
181                 } catch (UncheckedIOException e) {
182                     throw e.getCause();
183                 }
184                 if (!multi.isEmpty()) {
185                     descriptors = Collections.unmodifiableMap(multi);
186                     isModuleHierarchy = true;
187                     return;
188                 }
189             }
190         } else if (Files.isRegularFile(path)) {
191             /*
192              * JAR file: can contain only one module, with descriptor at the root.
193              * If no descriptor, the "Automatic-Module-Name" manifest attribute is
194              * taken as a fallback.
195              */
196             try (JarFile jar = new JarFile(path.toFile(), false, JarFile.OPEN_READ, target)) {
197                 ZipEntry entry = jar.getEntry(MODULE_INFO);
198                 if (entry != null) {
199                     ModuleDescriptor descriptor = null;
200                     if (resolve) {
201                         try (InputStream in = jar.getInputStream(entry)) {
202                             descriptor = ModuleDescriptor.read(in);
203                         }
204                     }
205                     descriptors = Collections.singletonMap(path, descriptor);
206                     isModuleHierarchy = false;
207                     return;
208                 }
209                 // No module descriptor, check manifest file.
210                 Manifest mf = jar.getManifest();
211                 if (mf != null) {
212                     Object name = mf.getMainAttributes().get(AUTO_MODULE_NAME);
213                     if (name instanceof String) {
214                         descriptors = Collections.singletonMap(path, name);
215                         isModuleHierarchy = false;
216                         return;
217                     }
218                 }
219             }
220         }
221         descriptors = Collections.emptyMap();
222         isModuleHierarchy = false;
223     }
224 
225     /**
226      * {@return the type of path detected}
227      * The return value is {@link JavaPathType#MODULES}
228      * if the dependency is a modular JAR file or a directory containing module descriptor(s),
229      * or {@link JavaPathType#CLASSES} otherwise. A JAR file without module descriptor but with
230      * an "Automatic-Module-Name" manifest attribute is considered modular.
231      */
232     public JavaPathType getPathType() {
233         return descriptors.isEmpty() ? JavaPathType.CLASSES : JavaPathType.MODULES;
234     }
235 
236     /**
237      * If the module has no name, adds the filename of the JAR file in the given collection.
238      * This method should be invoked for dependencies placed on {@link JavaPathType#MODULES}
239      * for preparing a warning asking to not deploy the build artifact on a public repository.
240      * If the module has an explicit name either with a {@code module-info.class} file or with
241      * an {@code "Automatic-Module-Name"} attribute in the {@code META-INF/MANIFEST.MF} file,
242      * then this method does nothing.
243      */
244     public void addIfFilenameBasedAutomodules(Collection<String> automodulesDetected) {
245         if (descriptors.isEmpty()) {
246             automodulesDetected.add(filename);
247         }
248     }
249 
250     /**
251      * {@return whether the dependency contains a module of the given name}
252      */
253     public boolean containsModule(String name) {
254         return descriptors.containsValue(name);
255     }
256 
257     /**
258      * {@return a string representation of this object for debugging purposes}
259      * This string representation may change in any future version.
260      */
261     @Override
262     public String toString() {
263         return getClass().getCanonicalName() + '[' + filename + ']';
264     }
265 }