yhuai
yhuai

Reputation: 11

Java 15 ModuleFinder encounters Error reading module

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

Answers (1)

Holger
Holger

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

Related Questions