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