2

When TestNG invokes Class.getResourceAsStream() on an external module, it is returning null.

Per Resources visibility with java9 modules when a user wants to access /resources/someResource.xml in a library that contains module-info.java, the module must opens resources unconditionally.

Fair enough. But what happens if a user wants to access /META-INF/resources/someResource.xml in such a library? What opens instruction is necessary in this case?

UPDATE:

The bug report was filed against version 7.5 of TestNG. I just noticed that if we look at the source code of that version we see it is trying to load /META-INF/resources/webjars/jquery/3.5.1/jquery.min.js.

Gili
  • 86,244
  • 97
  • 390
  • 689
  • *... then the module must contain opens resources.* No - that's not necessary – g00se Jul 24 '23 at 18:19
  • What makes you think you need an `opens` instruction for reading resources? When I create two modules, one with a resource and one using it with `getClassLoader().getResourceAsStream("META-INF/res/A.txt")`, I don't need any `opens` statements. – dan1st Jul 24 '23 at 19:05
  • @g00se then how do you explain [this exception](https://github.com/testng-team/testng/issues/2727#issue-1128971797)? [Class.getResourceAsStream()](https://github.com/testng-team/testng/blob/04e3e0fa7f8d93151dec408c936d1e27f85b56fe/testng-core/src/main/java/org/testng/reporters/jq/Main.java#L109) is returning `null` when looking up a resource in an external module. – Gili Jul 25 '23 at 16:03
  • Did you try using `ClassLoader#getResourceAsStream` and the full resource name? Can you try to provide an [mcve]? – dan1st Jul 25 '23 at 16:05
  • *@g00se then how do you explain this exception?* The path to the resource is wrong? In the exception message the resource name is simply the plain file name. That would mean it would have (generally) to be in the same package as the class that loaded it. That, needless to say, is not the correct state of affairs: that Javascript resource should be in a separate resource folder – g00se Jul 25 '23 at 16:23
  • @g00se I agree that the path looks wrong, but why would this code work fine on the classpath but break on the module path? Does Java include `/META-INF/resources` in the search path for non-modular code, but removes it for modular code? That seems odd. – Gili Jul 25 '23 at 16:28
  • Try to create a minimal reproducer (using the command line) of the issue and show that in your question. I assume there might be some `.class` file or similar in the `META-INF` directory. – dan1st Jul 25 '23 at 16:34
  • `META-INF` is generally nothing to do with resources. To debug, I would print the value of system property `java.class.path` at the app's entry point – g00se Jul 25 '23 at 16:37
  • Are you maybe using multi-release JARs? Because these have `.class` files somewhere in the `META-INF` directory. Try without a multi-release build. – dan1st Jul 25 '23 at 16:38
  • You might also want to make sure you are using a `Class` from the same `ClassLoader` the resource is located under as well. – dan1st Jul 25 '23 at 16:47

1 Answers1

0

From my testing, opening the package is only necessary when the file is in the same package as a class in the module. In that case, you need to open the package of the class in the same module.

So, assume you have a project like the following:

|-A
| |-module-info.java
| |-a
|   |-A.java
|   |-x
|     |-X
|-B
  |-module-info.java
  |-b
    |-B.java

and A/module-info.java:

module a {
    exports a;
}

as well as B/module-info.java:

module b {
    requires a;
}

then compile it with the following commands:

cd A
javac module-info.java a\A.java
cd ..
cd B
javac --module-path ..\A module-info.java b\B.java
cd ..

The content of class A is irrelevant here (it just needs to exist and have a class/interface/enum/record/whatever declaration with the correct name).

We then let B read the file a/x/X from module A:

package b;

import java.io.*;
import a.A;

public class B{
    public static void main(String[] args) throws Exception{
        try(BufferedReader br = new BufferedReader(new InputStreamReader(A.class.getClassLoader().getResourceAsStream("a/x/X")))){
            System.out.println(br.readLine());
        }
    }
}

When running it with

java --module-path A;B -m b/b.B

it displays the first line of a/x/X.

However, we cannot access files in the same directory as the A.class file:

package b;

import java.io.*;
import a.A;

public class B{
    public static void main(String[] args) throws Exception{
        try(BufferedReader br = new BufferedReader(new InputStreamReader(A.class.getResourceAsStream("A.java")))){
            System.out.println(br.readLine());
        }
    }
}

If we now add opens a in A/module-info.java (and recompile the module with the above command), the resource can be read.

If we want to read /META-INF/a/A.txt or similar (in the A module), no opens statement is required as there is no class in the same package.

dan1st
  • 12,568
  • 8
  • 34
  • 67
  • You're right. Per https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/Module.html#getResourceAsStream(java.lang.String): `If the resource is not in a package in the module then the resource is not encapsulated. [...] A resource name with the name "META-INF/MANIFEST.MF" is never encapsulated because "META-INF" is not a legal package name.` I don't know what is going on with TestNG but this doesn't seem to have anything to do with Java Modules. Thank you for pointing in the right direction. – Gili Jul 25 '23 at 16:48
  • Note that technically, you could have a class with an "illegal package name" but it cannot be created with `javac` (you'd need to create the class file differently) and it may be different for multi-release JARs where class files are located in subdirectories of `META-INF` (e.g. `META-INF/versions/`). – dan1st Jul 25 '23 at 16:49