diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverResult.java index e7ab86d0110..c8f125eb7e6 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverResult.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverResult.java @@ -18,12 +18,15 @@ */ package org.apache.maven.api.services; +import java.io.IOException; +import java.lang.module.ModuleDescriptor; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.maven.api.Dependency; +import org.apache.maven.api.DependencyScope; import org.apache.maven.api.JavaPathType; import org.apache.maven.api.Node; import org.apache.maven.api.PathType; @@ -100,6 +103,38 @@ public interface DependencyResolverResult { @Nonnull Map getDependencies(); + /** + * Returns the Java module name of the dependency at the given path. + * The given dependency should be one of the paths returned by {@link #getDependencies()}. + * The module name is extracted from the {@code module-info.class} file if present, otherwise from + * the {@code "Automatic-Module-Name"} attribute of the {@code META-INF/MANIFEST.MF} file if present. + * + *

A typical usage is to invoke this method for all dependencies having a + * {@link DependencyScope#TEST TEST} or {@link DependencyScope#TEST_ONLY TEST_ONLY} + * {@linkplain Dependency#getScope() scope}. An {@code --add-reads} option may need + * to be generated for compiling and running the test classes that use such dependencies.

+ * + * @param dependency path to the dependency for which to get the module name + * @return module name of the dependency at the given path, or empty if the dependency is not modular + * @throws IOException if the module information of the specified dependency cannot be read + */ + Optional getModuleName(@Nonnull Path dependency) throws IOException; + + /** + * Returns the Java module descriptor of the dependency at the given path. + * The given dependency should be one of the paths returned by {@link #getDependencies()}. + * The module descriptor is extracted from the {@code module-info.class} file if present. + * + *

{@link #getModuleName(Path)} is preferred when only the module name is desired, + * because a name may be present even if the descriptor is absent. This method is for + * cases when more information is desired, such as the set of exported packages.

+ * + * @param dependency path to the dependency for which to get the module name + * @return module name of the dependency at the given path, or empty if the dependency is not modular + * @throws IOException if the module information of the specified dependency cannot be read + */ + Optional getModuleDescriptor(@Nonnull Path dependency) throws IOException; + /** * If the module-path contains at least one filename-based auto-module, prepares a warning message. * The module path is the collection of dependencies associated to {@link JavaPathType#MODULES}. diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependencyResolverResult.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependencyResolverResult.java index ffad2adab51..be6a8cbb3a4 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependencyResolverResult.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/DefaultDependencyResolverResult.java @@ -19,6 +19,7 @@ package org.apache.maven.internal.impl; import java.io.IOException; +import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -154,7 +155,7 @@ private void addPathElement(PathType type, Path path) { * @param test the test output directory, or {@code null} if none * @throws IOException if an error occurred while reading module information * - * TODO: this is currently not called + * TODO: this is currently not called. This is intended for use by Surefire and may move there. */ void addOutputDirectory(Path main, Path test) throws IOException { if (outputModules != null) { @@ -169,8 +170,9 @@ void addOutputDirectory(Path main, Path test) throws IOException { if (test != null) { boolean addToClasspath = true; PathModularization testModules = cache.getModuleInfo(test); - boolean isModuleHierarchy = outputModules.isModuleHierarchy() || testModules.isModuleHierarchy(); - for (String moduleName : outputModules.getModuleNames().values()) { + boolean isModuleHierarchy = outputModules.isModuleHierarchy || testModules.isModuleHierarchy; + for (Object value : outputModules.descriptors.values()) { + String moduleName = name(value); Path subdir = test; if (isModuleHierarchy) { // If module hierarchy is used, the directory names shall be the module names. @@ -189,8 +191,8 @@ void addOutputDirectory(Path main, Path test) throws IOException { * If the test output directory provides some modules of its own, add them. * Except for this unusual case, tests should never be added to the module-path. */ - for (Map.Entry entry : testModules.getModuleNames().entrySet()) { - if (!outputModules.containsModule(entry.getValue())) { + for (Map.Entry entry : testModules.descriptors.entrySet()) { + if (!outputModules.containsModule(name(entry.getValue()))) { addPathElement(JavaPathType.MODULES, entry.getKey()); addToClasspath = false; } @@ -246,9 +248,9 @@ void addDependency(Node node, Dependency dep, Predicate filter, Path p outputModules = PathModularization.NONE; } PathType type = null; - for (Map.Entry info : - cache.getModuleInfo(path).getModuleNames().entrySet()) { - String moduleName = info.getValue(); + for (Map.Entry info : + cache.getModuleInfo(path).descriptors.entrySet()) { + String moduleName = name(info.getValue()); type = JavaPathType.patchModule(moduleName); if (!containsModule(moduleName)) { /* @@ -269,9 +271,9 @@ void addDependency(Node node, Dependency dep, Predicate filter, Path p if (type == null) { Path main = findArtifactPath(dep.getGroupId(), dep.getArtifactId()); if (main != null) { - for (Map.Entry info : - cache.getModuleInfo(main).getModuleNames().entrySet()) { - type = JavaPathType.patchModule(info.getValue()); + for (Map.Entry info : + cache.getModuleInfo(main).descriptors.entrySet()) { + type = JavaPathType.patchModule(name(info.getValue())); addPathElement(type, info.getKey()); // There is usually no more than one element, but nevertheless allow multi-modules. } @@ -360,6 +362,31 @@ public Map getDependencies() { return dependencies; } + @Override + public Optional getModuleDescriptor(Path dependency) throws IOException { + Object value = cache.getModuleInfo(dependency).descriptors.get(dependency); + return (value instanceof ModuleDescriptor) ? Optional.of((ModuleDescriptor) value) : Optional.empty(); + } + + @Override + public Optional getModuleName(Path dependency) throws IOException { + return Optional.ofNullable( + name(cache.getModuleInfo(dependency).descriptors.get(dependency))); + } + + /** + * Returns the module name for the given value of the {@link PathModularization#descriptors} map. + */ + private static String name(final Object value) { + if (value instanceof String) { + return (String) value; + } else if (value instanceof ModuleDescriptor) { + return ((ModuleDescriptor) value).name(); + } else { + return null; + } + } + @Override public Optional warningForFilenameBasedAutomodules() { try { diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/PathModularization.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/PathModularization.java index 2728fb4aca6..d8e9e941ddc 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/PathModularization.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/PathModularization.java @@ -70,23 +70,24 @@ class PathModularization { * It may however contain more than one entry if module hierarchy was detected, * in which case there is one key per sub-directory. * + *

Values are instances of either {@link ModuleDescriptor} or {@link String}. + * The latter case happens when a JAR file has no {@code module-info.class} entry + * but has an automatic name declared in {@code META-INF/MANIFEST.MF}.

+ * *

This map may contain null values if the constructor was invoked with {@code resolve} * parameter set to false. This is more efficient when only the module existence needs to * be tested, and module descriptors are not needed.

- * - * @see #getModuleNames() */ - private final Map descriptors; + @Nonnull + final Map descriptors; /** * Whether module hierarchy was detected. If false, then package hierarchy is assumed. * In a package hierarchy, the {@linkplain #descriptors} map has either zero or one entry. * In a module hierarchy, the descriptors map may have an arbitrary number of entries, * including one (so the map size cannot be used as a criterion). - * - * @see #isModuleHierarchy() */ - private final boolean isModuleHierarchy; + final boolean isModuleHierarchy; /** * Constructs an empty instance for non-modular dependencies. @@ -144,13 +145,13 @@ private PathModularization() { */ Path file = path.resolve(MODULE_INFO); if (Files.isRegularFile(file)) { - String name = null; + ModuleDescriptor descriptor = null; if (resolve) { try (InputStream in = Files.newInputStream(file)) { - name = getModuleName(in); + descriptor = ModuleDescriptor.read(in); } } - descriptors = Collections.singletonMap(file, name); + descriptors = Collections.singletonMap(file, descriptor); isModuleHierarchy = false; return; } @@ -160,27 +161,27 @@ private PathModularization() { * source files. */ if (Files.isDirectory(file)) { - Map names = new HashMap<>(); + var multi = new HashMap(); try (Stream subdirs = Files.list(file)) { subdirs.filter(Files::isDirectory).forEach((subdir) -> { Path mf = subdir.resolve(MODULE_INFO); if (Files.isRegularFile(mf)) { - String name = null; + ModuleDescriptor descriptor = null; if (resolve) { try (InputStream in = Files.newInputStream(mf)) { - name = getModuleName(in); + descriptor = ModuleDescriptor.read(in); } catch (IOException e) { throw new UncheckedIOException(e); } } - names.put(mf, name); + multi.put(mf, descriptor); } }); } catch (UncheckedIOException e) { throw e.getCause(); } - if (!names.isEmpty()) { - descriptors = Collections.unmodifiableMap(names); + if (!multi.isEmpty()) { + descriptors = Collections.unmodifiableMap(multi); isModuleHierarchy = true; return; } @@ -194,13 +195,13 @@ private PathModularization() { try (JarFile jar = new JarFile(path.toFile())) { ZipEntry entry = jar.getEntry(MODULE_INFO); if (entry != null) { - String name = null; + ModuleDescriptor descriptor = null; if (resolve) { try (InputStream in = jar.getInputStream(entry)) { - name = getModuleName(in); + descriptor = ModuleDescriptor.read(in); } } - descriptors = Collections.singletonMap(path, name); + descriptors = Collections.singletonMap(path, descriptor); isModuleHierarchy = false; return; } @@ -209,7 +210,7 @@ private PathModularization() { if (mf != null) { Object name = mf.getMainAttributes().get(AUTO_MODULE_NAME); if (name instanceof String) { - descriptors = Collections.singletonMap(path, (String) name); + descriptors = Collections.singletonMap(path, name); isModuleHierarchy = false; return; } @@ -220,15 +221,6 @@ private PathModularization() { isModuleHierarchy = false; } - /** - * {@return the module name declared in the given {@code module-info} descriptor}. - * The input stream may be for a file or for an entry in a JAR file. - */ - @Nonnull - private static String getModuleName(InputStream in) throws IOException { - return ModuleDescriptor.read(in).name(); - } - /** * {@return the type of path detected}. The return value is {@link JavaPathType#MODULES} * if the dependency is a modular JAR file or a directory containing module descriptor(s), @@ -253,31 +245,6 @@ public void addIfFilenameBasedAutomodules(Collection automodulesDetected } } - /** - * {@return whether module hierarchy was detected}. If {@code false}, then package hierarchy is assumed. - * In a package hierarchy, the {@linkplain #getModuleNames()} map of modules has either zero or one entry. - * In a module hierarchy, the descriptors map may have an arbitrary number of entries, - * including one (so the map size cannot be used as a criterion). - */ - public boolean isModuleHierarchy() { - return isModuleHierarchy; - } - - /** - * {@return the module names for the path specified at construction time}. - * This map is usually either empty if no module was found, or a singleton map. - * It may however contain more than one entry if module hierarchy was detected, - * in which case there is one key per sub-directory. - * - *

This map may contain null values if the constructor was invoked with {@code resolve} - * parameter set to false. This is more efficient when only the module existence needs to - * be tested, and module descriptors are not needed.

- */ - @Nonnull - public Map getModuleNames() { - return descriptors; - } - /** * {@return whether the dependency contains a module of the given name}. */