Skip to content

Commit

Permalink
Add getModuleName(Path) and getModuleDescription(Path) methods in…
Browse files Browse the repository at this point in the history
… `DependencyResolverResult`.

Those methods are helpful for plugins that need to provide `--add-reads` and similar options,
as they allow to reuse the cached values instead of decoding `module-info.class` many times.
  • Loading branch information
desruisseaux committed Aug 5, 2024
1 parent 4bc46b4 commit 52db57c
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -100,6 +103,38 @@ public interface DependencyResolverResult {
@Nonnull
Map<Dependency, Path> 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.
*
* <p>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.</p>
*
* @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<String> 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.
*
* <p>{@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.</p>
*
* @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<ModuleDescriptor> 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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand All @@ -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<Path, String> entry : testModules.getModuleNames().entrySet()) {
if (!outputModules.containsModule(entry.getValue())) {
for (Map.Entry<Path, Object> entry : testModules.descriptors.entrySet()) {
if (!outputModules.containsModule(name(entry.getValue()))) {
addPathElement(JavaPathType.MODULES, entry.getKey());
addToClasspath = false;
}
Expand Down Expand Up @@ -246,9 +248,9 @@ void addDependency(Node node, Dependency dep, Predicate<PathType> filter, Path p
outputModules = PathModularization.NONE;
}
PathType type = null;
for (Map.Entry<Path, String> info :
cache.getModuleInfo(path).getModuleNames().entrySet()) {
String moduleName = info.getValue();
for (Map.Entry<Path, Object> info :
cache.getModuleInfo(path).descriptors.entrySet()) {
String moduleName = name(info.getValue());
type = JavaPathType.patchModule(moduleName);
if (!containsModule(moduleName)) {
/*
Expand All @@ -269,9 +271,9 @@ void addDependency(Node node, Dependency dep, Predicate<PathType> filter, Path p
if (type == null) {
Path main = findArtifactPath(dep.getGroupId(), dep.getArtifactId());
if (main != null) {
for (Map.Entry<Path, String> info :
cache.getModuleInfo(main).getModuleNames().entrySet()) {
type = JavaPathType.patchModule(info.getValue());
for (Map.Entry<Path, Object> 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.
}
Expand Down Expand Up @@ -360,6 +362,31 @@ public Map<Dependency, Path> getDependencies() {
return dependencies;
}

@Override
public Optional<ModuleDescriptor> 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<String> 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<String> warningForFilenameBasedAutomodules() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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}.</p>
*
* <p>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.</p>
*
* @see #getModuleNames()
*/
private final Map<Path, String> descriptors;
@Nonnull
final Map<Path, Object> 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.
Expand Down Expand Up @@ -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;
}
Expand All @@ -160,27 +161,27 @@ private PathModularization() {
* source files.
*/
if (Files.isDirectory(file)) {
Map<Path, String> names = new HashMap<>();
var multi = new HashMap<Path, ModuleDescriptor>();
try (Stream<Path> 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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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),
Expand All @@ -253,31 +245,6 @@ public void addIfFilenameBasedAutomodules(Collection<String> 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.
*
* <p>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.</p>
*/
@Nonnull
public Map<Path, String> getModuleNames() {
return descriptors;
}

/**
* {@return whether the dependency contains a module of the given name}.
*/
Expand Down

0 comments on commit 52db57c

Please sign in to comment.