Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MNG-8195] Add DependencyResolverResult.getModuleName(Path) method #1625

Merged
merged 1 commit into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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