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 = path -> {
166                 if (request.getJdkHome() != null) {
167                     filenameAutoModules.put(t, path);
168                 } else {
169                     return CmdModuleNameExtractor.getModuleName(path);
170                 }
171                 return null;
172             };
173 
174             try {
175                 ResolvePathResult resolvedPath = resolvePath(request.toPath(t), nameExtractor, binaryParser);
176 
177                 moduleDescriptor = resolvedPath.getModuleDescriptor();
178 
179                 source = resolvedPath.getModuleNameSource();
180             } catch (Exception e) {
181                 result.getPathExceptions().put(t, e);
182 
183                 pathElements.put(t, null);
184 
185                 continue;
186             }
187 
188             // in case of identical module names, first one wins
189             if (moduleDescriptor != null && moduleNameSources.putIfAbsent(moduleDescriptor.name(), source) == null) {
190                 availableNamedModules.put(moduleDescriptor.name(), moduleDescriptor);
191 
192                 if (request.isIncludeAllProviders()) {
193                     collectProviders(moduleDescriptor, availableProviders);
194                 }
195             }
196 
197             pathElements.put(t, moduleDescriptor);
198         }
199         result.setPathElements(pathElements);
200 
201         if (!filenameAutoModules.isEmpty()) {
202             MainClassModuleNameExtractor extractor = new MainClassModuleNameExtractor(request.getJdkHome());
203 
204             Map<T, String> automodules = extractor.extract(filenameAutoModules);
205 
206             for (Map.Entry<T, String> entry : automodules.entrySet()) {
207                 String moduleName = entry.getValue();
208 
209                 if (moduleName != null) {
210                     JavaModuleDescriptor moduleDescriptor =
211                             JavaModuleDescriptor.newAutomaticModule(moduleName).build();
212 
213                     moduleNameSources.put(moduleDescriptor.name(), ModuleNameSource.FILENAME);
214 
215                     availableNamedModules.put(moduleDescriptor.name(), moduleDescriptor);
216 
217                     pathElements.put(entry.getKey(), moduleDescriptor);
218                 }
219             }
220         }
221 
222         Set<String> requiredNamedModules = new HashSet<>();
223 
224         if (mainModuleDescriptor != null) {
225             requiredNamedModules.add(mainModuleDescriptor.name());
226 
227             selectRequires(
228                     mainModuleDescriptor,
229                     Collections.unmodifiableMap(availableNamedModules),
230                     Collections.unmodifiableMap(availableProviders),
231                     requiredNamedModules,
232                     true,
233                     true,
234                     request.isIncludeStatic());
235         }
236 
237         for (String additionalModule : request.getAdditionalModules()) {
238             selectModule(
239                     additionalModule,
240                     Collections.unmodifiableMap(availableNamedModules),
241                     Collections.unmodifiableMap(availableProviders),
242                     requiredNamedModules,
243                     true,
244                     true,
245                     request.isIncludeStatic());
246         }
247 
248         Set<String> collectedModules = new HashSet<>(requiredNamedModules.size());
249 
250         for (Entry<T, JavaModuleDescriptor> entry : pathElements.entrySet()) {
251             if (entry.getValue() != null
252                     && requiredNamedModules.contains(entry.getValue().name())) {
253                 // Consider strategies how to handle duplicate modules by name
254                 // For now only add first on modulePath, just ignore others,
255                 //   This has effectively the same result as putting it on the modulePath, but might better help
256                 // analyzing issues.
257                 if (collectedModules.add(entry.getValue().name())) {
258                     result.getModulepathElements()
259                             .put(
260                                     entry.getKey(),
261                                     moduleNameSources.get(entry.getValue().name()));
262                 } else {
263                     result.getPathExceptions()
264                             .put(
265                                     entry.getKey(),
266                                     new IllegalStateException(
267                                             "Module '" + entry.getValue().name() + "' is already on the module path!"));
268                 }
269             } else {
270                 result.getClasspathElements().add(entry.getKey());
271             }
272         }
273 
274         return result;
275     }
276 
277     /**
278      * If the jdkHome is specified, its version it considered higher than the runtime java version.
279      * In that case ASM must be used to read the module descriptor
280      *
281      * @param jdkHome
282      * @return
283      */
284     ModuleInfoParser getBinaryModuleInfoParser(final Path jdkHome) {
285         final ModuleInfoParser binaryParser;
286         if (jdkHome == null) {
287             binaryParser = new BinaryModuleInfoParser();
288         } else {
289             binaryParser = new AsmModuleInfoParser();
290         }
291         return binaryParser;
292     }
293 
294     private <T> JavaModuleDescriptor getMainModuleDescriptor(
295             final ResolvePathsRequest<T> request, ModuleInfoParser binaryParser) throws IOException {
296         JavaModuleDescriptor mainModuleDescriptor;
297 
298         Path descriptorPath = request.getMainModuleDescriptor();
299 
300         if (descriptorPath != null) {
301             if (descriptorPath.endsWith("module-info.java")) {
302                 mainModuleDescriptor = sourceParser.fromSourcePath(descriptorPath);
303             } else if (descriptorPath.endsWith("module-info.class")) {
304                 mainModuleDescriptor = binaryParser.getModuleDescriptor(descriptorPath.getParent());
305             } else {
306                 throw new IOException("Invalid path to module descriptor: " + descriptorPath);
307             }
308         } else {
309             mainModuleDescriptor = request.getModuleDescriptor();
310         }
311         return mainModuleDescriptor;
312     }
313 
314     private ResolvePathResult resolvePath(
315             Path path, ModuleNameExtractor fileModulenameExtractor, ModuleInfoParser binaryParser) throws IOException {
316         ResolvePathResult result = new ResolvePathResult();
317 
318         JavaModuleDescriptor moduleDescriptor = null;
319 
320         // either jar or outputDirectory
321         if (Files.isRegularFile(path) && !path.getFileName().toString().endsWith(".jar")) {
322             throw new IllegalArgumentException(
323                     "'" + path + "' not allowed on the path, only outputDirectories and jars are accepted");
324         }
325 
326         if (Files.isRegularFile(path) || Files.exists(path.resolve("module-info.class"))) {
327             moduleDescriptor = binaryParser.getModuleDescriptor(path);
328         }
329 
330         if (moduleDescriptor != null) {
331             result.setModuleNameSource(ModuleNameSource.MODULEDESCRIPTOR);
332         } else {
333             String moduleName = manifestModuleNameExtractor.extract(path);
334 
335             if (moduleName != null) {
336                 result.setModuleNameSource(ModuleNameSource.MANIFEST);
337             } else {
338                 moduleName = fileModulenameExtractor.extract(path);
339 
340                 if (moduleName != null) {
341                     result.setModuleNameSource(ModuleNameSource.FILENAME);
342                 }
343             }
344 
345             if (moduleName != null) {
346                 moduleDescriptor =
347                         JavaModuleDescriptor.newAutomaticModule(moduleName).build();
348             }
349         }
350         result.setModuleDescriptor(moduleDescriptor);
351 
352         return result;
353     }
354 
355     private void selectRequires(
356             JavaModuleDescriptor module,
357             Map<String, JavaModuleDescriptor> availableModules,
358             Map<String, Set<String>> availableProviders,
359             Set<String> namedModules,
360             boolean isRootModule,
361             boolean includeAsTransitive,
362             boolean includeStatic) {
363         for (JavaModuleDescriptor.JavaRequires requires : module.requires()) {
364             // includeTransitive is one level deeper compared to includeStatic
365             if (isRootModule
366                     || includeStatic
367                     || includeAsTransitive
368                     || !requires.modifiers().contains(JavaModuleDescriptor.JavaRequires.JavaModifier.STATIC)
369                     || requires.modifiers().contains(JavaModuleDescriptor.JavaRequires.JavaModifier.TRANSITIVE)) {
370                 selectModule(
371                         requires.name(),
372                         availableModules,
373                         availableProviders,
374                         namedModules,
375                         false,
376                         includeStatic,
377                         includeStatic);
378             }
379         }
380 
381         for (String uses : module.uses()) {
382             if (availableProviders.containsKey(uses)) {
383                 for (String providerModule : availableProviders.get(uses)) {
384                     JavaModuleDescriptor requiredModule = availableModules.get(providerModule);
385 
386                     if (requiredModule != null && namedModules.add(providerModule)) {
387                         selectRequires(
388                                 requiredModule,
389                                 availableModules,
390                                 availableProviders,
391                                 namedModules,
392                                 false,
393                                 includeAsTransitive,
394                                 includeStatic);
395                     }
396                 }
397             }
398         }
399     }
400 
401     private void selectModule(
402             String module,
403             Map<String, JavaModuleDescriptor> availableModules,
404             Map<String, Set<String>> availableProviders,
405             Set<String> namedModules,
406             boolean isRootModule,
407             boolean includeTransitive,
408             boolean includeStatic) {
409         JavaModuleDescriptor requiredModule = availableModules.get(module);
410 
411         if (requiredModule != null && namedModules.add(module)) {
412             selectRequires(
413                     requiredModule,
414                     availableModules,
415                     availableProviders,
416                     namedModules,
417                     false,
418                     includeTransitive,
419                     includeStatic);
420         }
421     }
422 
423     private void collectProviders(JavaModuleDescriptor moduleDescriptor, Map<String, Set<String>> availableProviders) {
424         for (JavaProvides provides : moduleDescriptor.provides()) {
425             // module-info.class uses FQN, i.e. $-separator for subclasses
426             final String serviceClassName = provides.service().replace('$', '.');
427 
428             Set<String> providingModules = availableProviders.computeIfAbsent(serviceClassName, k -> new HashSet<>());
429 
430             providingModules.add(moduleDescriptor.name());
431         }
432     }
433 }