Reputation: 11
I am trying to programmatically load a project that is using Java Platform Module System, so I can analyze how many modules exist and what are the relationships. I wrote the following code:
public void retrieveModules() throws IOException {
moduleRefs = new ArrayList<ModuleReference>();
// search for all module-info.class
Stream<Path> classPaths = Files.walk(Paths.get(this.basePath));
List<String> moduleInfoClassFiles = classPaths
.filter(p -> p.getFileName()
.toString()
.toLowerCase()
.contains("module-info.class"))
.map(p -> p.toString())
.collect(Collectors.toList());
// load modules
for (String classFilePath : moduleInfoClassFiles) {
Path dir = Paths.get(classFilePath.replace("/module-info.class", ""));
ModuleFinder finder = ModuleFinder.of(dir);
moduleRefs.addAll(finder.findAll());
}
}
This code has been working fine as finder.findAll()
finds the module and I can retrieve the correct module descriptor. However, when I am trying to run this on JUnit 5 (https://github.com/junit-team/junit5)
I got the following error message:
Exception in thread "main" java.lang.module.FindException: Error reading module: /.../junit-team_junit5/junit-platform-engine/build/classes/java/module/org.junit.platform.commons
at java.base/jdk.internal.module.ModulePath.readModule(ModulePath.java:350)
at java.base/jdk.internal.module.ModulePath.scan(ModulePath.java:237)
at java.base/jdk.internal.module.ModulePath.scanNextEntry(ModulePath.java:190)
at java.base/jdk.internal.module.ModulePath.findAll(ModulePath.java:166)
at RepoAnalyzer.retrieveModules(RepoAnalyzer.java:41)
at RepoAnalyzer.main(RepoAnalyzer.java:23)
Caused by: java.lang.module.InvalidModuleDescriptorException: Package org.junit.platform.commons.util not found in module
I tried to find out what went wrong. I verified that packages are correctly exported and required for both modules. Does anyone know what I am doing wrong?
Upvotes: 1
Views: 291
Reputation: 298153
There is no need to perform string manipulations with the paths you encountered. Your code can be simplified to
public void retrieveModules() throws IOException {
// search for all module-info.class
try(Stream<Path> classPaths = Files.walk(Paths.get(this.basePath))) {
moduleRefs = ModuleFinder.of(classPaths
.filter(p -> p.getFileName().toString().equals("module-info.class"))
.map(Path::getParent)
.toArray(Path[]::new)).findAll();
}
}
assuming that you can change moduleRefs
to Set
or Collection
rather than List
. Otherwise, wrap the expression in a new ArrayList<>(…)
.
It seems that you are scanning an “exploded module” on the default filesystem and the encountered module-info.class
has not an explicit declaration of all packages. What happens inside the ModuleFinder
is equivalent to a call to ModuleDescriptor.read(InputStream in, Supplier<Set<String>> packageFinder)
whereas the supplied package finder will scan the subtree of the directory containing the module-info for directories containing at least one regular file, a class or resource, and having a name that forms a valid package name. All of them are considered packages of the module.
The documentation of the method states:
Throws
InvalidModuleDescriptorException - If an invalid module descriptor is detected or the set of packages returned by the packageFinder does not include all of the packages obtained from the module descriptor
The packages obtained from the module descriptor are named in the packages()
method:
The set of packages includes all exported and open packages, as well as the packages of any service providers, and the package for the main class.
So the package org.junit.platform.commons.util
has been identified as mandatory through the module-info
but not found through the package finder, i.e. the corresponding directory might not exist.
You may try to narrow the problem by retracing these steps manually:
public void checkModules() throws IOException {
try(Stream<Path> classPaths = Files.walk(Paths.get(this.basePath))) {
classPaths
.filter(p -> p.getFileName().toString().equals("module-info.class"))
.forEach(this::check);
}
}
private void check(Path path) {
try {
ModuleDescriptor md = md(path);
System.out.println("module " + md.name());
for(String pkg: md.packages()) {
Path pLoc = path.getParent();
for(String sub: pkg.split("\\.")) pLoc = pLoc.resolve(sub);
System.out.print("package " + pkg + ": " + pLoc);
if(!Files.exists(pLoc)) System.out.println(" does not exist");
else if(!Files.isDirectory(pLoc)) System.out.println(" is not a directory");
else if(!checkClassOrResource(pLoc))
System.out.println(" contains no class or resource");
else System.out.println(" seems ok");
}
}
catch(IOException ex) {
throw new UncheckedIOException(ex);
}
}
private boolean checkClassOrResource(Path pLoc) throws IOException {
try(Stream<Path> members = Files.list(pLoc)) {
return members.anyMatch(Files::isRegularFile);
}
}
private ModuleDescriptor md(Path p) throws IOException {
try(InputStream is = Files.newInputStream(p)) {
return ModuleDescriptor.read(is);
}
}
This will check for the directories corresponding to the packages and print a diagnostic message. Note that the method ModuleDescriptor.read(InputStream is)
variant without a package finder used above does not verify the package locations but uses only those found in the module-info
which does already include all packages relevant for cross-module relationships. If you are not interested in the other packages in your analysis, you could simply use that method to construct a ModuleDescriptor
for each module-info
and proceed with them.
try(Stream<Path> classPaths = Files.walk(Paths.get(this.basePath))) {
List<ModuleDescriptor> modules = classPaths
.filter(p -> p.getFileName().toString().equals("module-info.class"))
.map(path -> {
try(InputStream is = Files.newInputStream(path)) {
return ModuleDescriptor.read(is);
} catch(IOException ex) {
throw new UncheckedIOException(ex);
}
})
.collect(Collectors.toList());
}
Upvotes: 1