View Javadoc
1   package org.codehaus.plexus.languages.java.jpms;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.LinkedHashMap;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.Set;
37  
38  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.JavaProvides;
39  
40  /**
41   * Maps artifacts to modules and analyzes the type of required modules
42   *
43   * @author Robert Scholte
44   * @since 1.0.0
45   */
46  @Named
47  @Singleton
48  public class LocationManager {
49      private SourceModuleInfoParser sourceParser;
50  
51      private ManifestModuleNameExtractor manifestModuleNameExtractor;
52  
53      public LocationManager() {
54          this.sourceParser = new SourceModuleInfoParser();
55          this.manifestModuleNameExtractor = new ManifestModuleNameExtractor();
56      }
57  
58      LocationManager(SourceModuleInfoParser sourceParser) {
59          this.sourceParser = sourceParser;
60          this.manifestModuleNameExtractor = new ManifestModuleNameExtractor();
61      }
62  
63      /**
64       * @param descriptorPath never {@code null}
65       * @return the parsed module descriptor
66       * @throws IOException when descriptorPath could not be read
67       */
68      public ResolvePathResult parseModuleDescriptor(Path descriptorPath) throws IOException {
69          JavaModuleDescriptor moduleDescriptor;
70          if (descriptorPath.endsWith("module-info.java")) {
71              moduleDescriptor = sourceParser.fromSourcePath(descriptorPath);
72          } else {
73              throw new IOException("Invalid path to module descriptor: " + descriptorPath);
74          }
75          return new ResolvePathResult()
76                  .setModuleDescriptor(moduleDescriptor)
77                  .setModuleNameSource(ModuleNameSource.MODULEDESCRIPTOR);
78      }
79  
80      /**
81       * @param descriptorPath never {@code null}
82       * @return the parsed module descriptor
83       * @throws IOException when descriptorPath could not be read
84       */
85      public ResolvePathResult parseModuleDescriptor(File descriptorPath) throws IOException {
86          return parseModuleDescriptor(descriptorPath.toPath());
87      }
88  
89      /**
90       * @param descriptorPath never {@code null}
91       * @return the parsed module descriptor
92       * @throws IOException when descriptorPath could not be read
93       */
94      public ResolvePathResult parseModuleDescriptor(String descriptorPath) throws IOException {
95          return parseModuleDescriptor(Paths.get(descriptorPath));
96      }
97  
98      /**
99       * Resolve a single jar
100      *
101      * @param request the request
102      * @return the {@link ResolvePathResult}, containing the name and optional module descriptor
103      * @throws IOException if any occurs
104      */
105     public <T> ResolvePathResult resolvePath(final ResolvePathRequest<T> request) throws IOException {
106         ModuleNameExtractor filenameExtractor = new ModuleNameExtractor() {
107             MainClassModuleNameExtractor extractor = new MainClassModuleNameExtractor(request.getJdkHome());
108 
109             @Override
110             public String extract(Path file) throws IOException {
111                 if (request.getJdkHome() != null) {
112                     return extractor
113                             .extract(Collections.singletonMap(file, file))
114                             .get(file);
115                 } else {
116                     return CmdModuleNameExtractor.getModuleName(file);
117                 }
118             }
119         };
120 
121         return resolvePath(
122                 request.toPath(request.getPathElement()),
123                 filenameExtractor,
124                 getBinaryModuleInfoParser(request.getJdkHome()));
125     }
126 
127     /**
128      * Decide for every {@code request.getPathElements()} if it belongs to the modulePath or classPath, based on the
129      * {@code request.getMainModuleDescriptor()}.
130      *
131      * @param request the paths to resolve
132      * @return the result of the resolution
133      * @throws IOException if a critical IOException occurs
134      */
135     public <T> ResolvePathsResult<T> resolvePaths(final ResolvePathsRequest<T> request) throws IOException {
136         final ResolvePathsResult<T> result = request.createResult();
137 
138         Map<T, JavaModuleDescriptor> pathElements =
139                 new LinkedHashMap<>(request.getPathElements().size());
140 
141         final ModuleInfoParser binaryParser = getBinaryModuleInfoParser(request.getJdkHome());
142 
143         JavaModuleDescriptor mainModuleDescriptor = getMainModuleDescriptor(request, binaryParser);
144 
145         result.setMainModuleDescriptor(mainModuleDescriptor);
146 
147         // key = service, value = names of modules that provide this service
148         Map<String, Set<String>> availableProviders = new HashMap<>();
149 
150         if (mainModuleDescriptor != null && request.isIncludeAllProviders()) {
151             collectProviders(mainModuleDescriptor, availableProviders);
152         }
153 
154         Map<String, JavaModuleDescriptor> availableNamedModules = new HashMap<>();
155 
156         Map<String, ModuleNameSource> moduleNameSources = new HashMap<>();
157 
158         final Map<T, Path> filenameAutoModules = new HashMap<>();
159 
160         // collect all modules from path
161         for (final T t : request.getPathElements()) {
162             JavaModuleDescriptor moduleDescriptor;
163             ModuleNameSource source;
164 
165             ModuleNameExtractor nameExtractor = new ModuleNameExtractor() {
166                 @Override
167                 public String extract(Path path) throws IOException {
168                     if (request.getJdkHome() != null) {
169                         filenameAutoModules.put(t, path);
170                     } else {
171                         return CmdModuleNameExtractor.getModuleName(path);
172                     }
173                     return null;
174                 }
175             };
176 
177             try {
178                 ResolvePathResult resolvedPath = resolvePath(request.toPath(t), nameExtractor, binaryParser);
179 
180                 moduleDescriptor = resolvedPath.getModuleDescriptor();
181 
182                 source = resolvedPath.getModuleNameSource();
183             } catch (Exception e) {
184                 result.getPathExceptions().put(t, e);
185 
186                 pathElements.put(t, null);
187 
188                 continue;
189             }
190 
191             // in case of identical module names, first one wins
192             if (moduleDescriptor != null && moduleNameSources.putIfAbsent(moduleDescriptor.name(), source) == null) {
193                 availableNamedModules.put(moduleDescriptor.name(), moduleDescriptor);
194 
195                 if (request.isIncludeAllProviders()) {
196                     collectProviders(moduleDescriptor, availableProviders);
197                 }
198             }
199 
200             pathElements.put(t, moduleDescriptor);
201         }
202         result.setPathElements(pathElements);
203 
204         if (!filenameAutoModules.isEmpty()) {
205             MainClassModuleNameExtractor extractor = new MainClassModuleNameExtractor(request.getJdkHome());
206 
207             Map<T, String> automodules = extractor.extract(filenameAutoModules);
208 
209             for (Map.Entry<T, String> entry : automodules.entrySet()) {
210                 String moduleName = entry.getValue();
211 
212                 if (moduleName != null) {
213                     JavaModuleDescriptor moduleDescriptor =
214                             JavaModuleDescriptor.newAutomaticModule(moduleName).build();
215 
216                     moduleNameSources.put(moduleDescriptor.name(), ModuleNameSource.FILENAME);
217 
218                     availableNamedModules.put(moduleDescriptor.name(), moduleDescriptor);
219 
220                     pathElements.put(entry.getKey(), moduleDescriptor);
221                 }
222             }
223         }
224 
225         Set<String> requiredNamedModules = new HashSet<>();
226 
227         if (mainModuleDescriptor != null) {
228             requiredNamedModules.add(mainModuleDescriptor.name());
229 
230             selectRequires(
231                     mainModuleDescriptor,
232                     Collections.unmodifiableMap(availableNamedModules),
233                     Collections.unmodifiableMap(availableProviders),
234                     requiredNamedModules,
235                     true,
236                     true,
237                     request.isIncludeStatic());
238         }
239 
240         for (String additionalModule : request.getAdditionalModules()) {
241             selectModule(
242                     additionalModule,
243                     Collections.unmodifiableMap(availableNamedModules),
244                     Collections.unmodifiableMap(availableProviders),
245                     requiredNamedModules,
246                     true,
247                     true,
248                     request.isIncludeStatic());
249         }
250 
251         Set<String> collectedModules = new HashSet<>(requiredNamedModules.size());
252 
253         for (Entry<T, JavaModuleDescriptor> entry : pathElements.entrySet()) {
254             if (entry.getValue() != null
255                     && requiredNamedModules.contains(entry.getValue().name())) {
256                 // Consider strategies how to handle duplicate modules by name
257                 // For now only add first on modulePath, just ignore others,
258                 //   This has effectively the same result as putting it on the modulePath, but might better help
259                 // analyzing issues.
260                 if (collectedModules.add(entry.getValue().name())) {
261                     result.getModulepathElements()
262                             .put(
263                                     entry.getKey(),
264                                     moduleNameSources.get(entry.getValue().name()));
265                 } else {
266                     result.getPathExceptions()
267                             .put(
268                                     entry.getKey(),
269                                     new IllegalStateException(
270                                             "Module '" + entry.getValue().name() + "' is already on the module path!"));
271                 }
272             } else {
273                 result.getClasspathElements().add(entry.getKey());
274             }
275         }
276 
277         return result;
278     }
279 
280     /**
281      * If the jdkHome is specified, its version it considered higher than the runtime java version.
282      * In that case ASM must be used to read the module descriptor
283      *
284      * @param jdkHome
285      * @return
286      */
287     ModuleInfoParser getBinaryModuleInfoParser(final Path jdkHome) {
288         final ModuleInfoParser binaryParser;
289         if (jdkHome == null) {
290             binaryParser = new BinaryModuleInfoParser();
291         } else {
292             binaryParser = new AsmModuleInfoParser();
293         }
294         return binaryParser;
295     }
296 
297     private <T> JavaModuleDescriptor getMainModuleDescriptor(
298             final ResolvePathsRequest<T> request, ModuleInfoParser binaryParser) throws IOException {
299         JavaModuleDescriptor mainModuleDescriptor;
300 
301         Path descriptorPath = request.getMainModuleDescriptor();
302 
303         if (descriptorPath != null) {
304             if (descriptorPath.endsWith("module-info.java")) {
305                 mainModuleDescriptor = sourceParser.fromSourcePath(descriptorPath);
306             } else if (descriptorPath.endsWith("module-info.class")) {
307                 mainModuleDescriptor = binaryParser.getModuleDescriptor(descriptorPath.getParent());
308             } else {
309                 throw new IOException("Invalid path to module descriptor: " + descriptorPath);
310             }
311         } else {
312             mainModuleDescriptor = request.getModuleDescriptor();
313         }
314         return mainModuleDescriptor;
315     }
316 
317     private ResolvePathResult resolvePath(
318             Path path, ModuleNameExtractor fileModulenameExtractor, ModuleInfoParser binaryParser) throws IOException {
319         ResolvePathResult result = new ResolvePathResult();
320 
321         JavaModuleDescriptor moduleDescriptor = null;
322 
323         // either jar or outputDirectory
324         if (Files.isRegularFile(path) && !path.getFileName().toString().endsWith(".jar")) {
325             throw new IllegalArgumentException(
326                     "'" + path.toString() + "' not allowed on the path, only outputDirectories and jars are accepted");
327         }
328 
329         if (Files.isRegularFile(path) || Files.exists(path.resolve("module-info.class"))) {
330             moduleDescriptor = binaryParser.getModuleDescriptor(path);
331         }
332 
333         if (moduleDescriptor != null) {
334             result.setModuleNameSource(ModuleNameSource.MODULEDESCRIPTOR);
335         } else {
336             String moduleName = manifestModuleNameExtractor.extract(path);
337 
338             if (moduleName != null) {
339                 result.setModuleNameSource(ModuleNameSource.MANIFEST);
340             } else {
341                 moduleName = fileModulenameExtractor.extract(path);
342 
343                 if (moduleName != null) {
344                     result.setModuleNameSource(ModuleNameSource.FILENAME);
345                 }
346             }
347 
348             if (moduleName != null) {
349                 moduleDescriptor =
350                         JavaModuleDescriptor.newAutomaticModule(moduleName).build();
351             }
352         }
353         result.setModuleDescriptor(moduleDescriptor);
354 
355         return result;
356     }
357 
358     private void selectRequires(
359             JavaModuleDescriptor module,
360             Map<String, JavaModuleDescriptor> availableModules,
361             Map<String, Set<String>> availableProviders,
362             Set<String> namedModules,
363             boolean isRootModule,
364             boolean includeAsTransitive,
365             boolean includeStatic) {
366         for (JavaModuleDescriptor.JavaRequires requires : module.requires()) {
367             // includeTransitive is one level deeper compared to includeStatic
368             if (isRootModule
369                     || includeStatic
370                     || includeAsTransitive
371                     || !requires.modifiers().contains(JavaModuleDescriptor.JavaRequires.JavaModifier.STATIC)
372                     || requires.modifiers().contains(JavaModuleDescriptor.JavaRequires.JavaModifier.TRANSITIVE)) {
373                 selectModule(
374                         requires.name(),
375                         availableModules,
376                         availableProviders,
377                         namedModules,
378                         false,
379                         includeStatic,
380                         includeStatic);
381             }
382         }
383 
384         for (String uses : module.uses()) {
385             if (availableProviders.containsKey(uses)) {
386                 for (String providerModule : availableProviders.get(uses)) {
387                     JavaModuleDescriptor requiredModule = availableModules.get(providerModule);
388 
389                     if (requiredModule != null && namedModules.add(providerModule)) {
390                         selectRequires(
391                                 requiredModule,
392                                 availableModules,
393                                 availableProviders,
394                                 namedModules,
395                                 false,
396                                 includeAsTransitive,
397                                 includeStatic);
398                     }
399                 }
400             }
401         }
402     }
403 
404     private void selectModule(
405             String module,
406             Map<String, JavaModuleDescriptor> availableModules,
407             Map<String, Set<String>> availableProviders,
408             Set<String> namedModules,
409             boolean isRootModule,
410             boolean includeTransitive,
411             boolean includeStatic) {
412         JavaModuleDescriptor requiredModule = availableModules.get(module);
413 
414         if (requiredModule != null && namedModules.add(module)) {
415             selectRequires(
416                     requiredModule,
417                     availableModules,
418                     availableProviders,
419                     namedModules,
420                     false,
421                     includeTransitive,
422                     includeStatic);
423         }
424     }
425 
426     private void collectProviders(JavaModuleDescriptor moduleDescriptor, Map<String, Set<String>> availableProviders) {
427         for (JavaProvides provides : moduleDescriptor.provides()) {
428             // module-info.class uses FQN, i.e. $-separator for subclasses
429             final String serviceClassName = provides.service().replace('$', '.');
430 
431             Set<String> providingModules = availableProviders.get(serviceClassName);
432 
433             if (providingModules == null) {
434                 providingModules = new HashSet<>();
435 
436                 availableProviders.put(serviceClassName, providingModules);
437             }
438             providingModules.add(moduleDescriptor.name());
439         }
440     }
441 }