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.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 }