8

When we create Java runtime by jlink it takes all java classes/resources and places them into JRT image file: lib/modules.

Here is basic Maven project resources structure I use:

src
  main
    resources
      dict
        xkcd_en

I'm just trying to read xkcd_en text file. If we look into JRT file, here it is:

>> jimage list /path/to/lib/modules
...
Module: main
    dict/xkcd_en
...

Also I've explicitly opened it in module-info, just in case:

module main {
    opens dict;
    // ..rest code omitted
}

The only way I could read the file is obtaining it as input stream:

WORKS:

public static InputStream getResourceAsStream(String resource) {
    return FileUtils.class.getResourceAsStream(resource);
}

System.out.println(new BufferedReader(
    new InputStreamReader(getResourceAsStream("/dict/xkcd_en")))
            .lines().collect(Collectors.joining("\n"))
);

DOESN'T WORK:

But if I'm trying to get the file URI and read it via Java NIO API, it doesn't work:

public static URL getResourceOrThrow(String resource) {
    URL url = FileUtils.class.getResource(resource);
    Objects.requireNonNull(url);
    return url;
}

1 - Java NIO can't find the file. But it definitely does exist, otherwise getResource() returns null.

System.out.println(Paths.get(getResourceOrThrow("/dict/xkcd_en").toURI()));
// /main/dict/xkcd_en

Files.readAllLines(Paths.get(getResourceOrThrow("/dict/xkcd_en").toURI()));

Caused by: java.nio.file.NoSuchFileException: /main/dict/xkcd_en
        at java.base/jdk.internal.jrtfs.JrtFileSystem.checkNode(JrtFileSystem.java:494)
        at java.base/jdk.internal.jrtfs.JrtFileSystem.getFileContent(JrtFileSystem.java:253)
        at java.base/jdk.internal.jrtfs.JrtFileSystem.newInputStream(JrtFileSystem.java:342)
        at java.base/jdk.internal.jrtfs.JrtPath.newInputStream(JrtPath.java:631)
        at java.base/jdk.internal.jrtfs.JrtFileSystemProvider.newInputStream(JrtFileSystemProvider.java:322)

2 - The same if you'll use FileSystem directly:

FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
System.out.println(fs.getPath("main/dict/xkcd_en"));
// main/dict/xkcd_en

Files.readAllLines(fs.getPath("main/dict/xkcd_en")));

Caused by: java.nio.file.NoSuchFileException: /main/dict/xkcd_en
    at java.base/jdk.internal.jrtfs.JrtFileSystem.checkNode(JrtFileSystem.java:494)

3 - Java NIO doesn't even know what jrt:/ scheme is.

Files.readAllLines(Paths.get(getResourceOrThrow("/dict/xkcd_en").toExternalForm()));

Caused by: java.nio.file.InvalidPathException: Illegal char <:> at index 3: jrt:/main/dict/xkcd_en
    at java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
    at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
    at java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:229)
    at java.base/java.nio.file.Path.of(Path.java:147)
    at java.base/java.nio.file.Paths.get(Paths.java:69)

Here is the specs of JRT FS.

A jrt URL is a hierarchical URI, per RFC 3986, with the syntax

jrt:/[$MODULE[/$PATH]]

where $MODULE is an optional module name and $PATH, if present, is the path to a specific class or resource file within that module. The meaning of a jrt URL depends upon its structure:

  • jrt:/$MODULE/$PATH refers to the specific class or resource file named $PATH within the given $MODULE.
  • jrt:/$MODULE refers to all of the class and resource files in the module $MODULE.
  • jrt:/ refers to the entire collection of class and resource files stored in the current run-time image.

So obtained path looks ok to me. Where am I wrong?

Slaw
  • 37,820
  • 8
  • 53
  • 80
Evan
  • 649
  • 1
  • 9
  • 22
  • 5
    There is a bug in the jrt file system provider, see: https://bugs.openjdk.java.net/browse/JDK-8216553 You can work around it by prefixing the file path with "/modules". Also just to point out that your example using toExternalForm invoke Paths.get(String) which is for converting a path string to a file on the platform file system, not jrtfs. – Alan Bateman Jan 11 '19 at 09:55
  • @AlanBateman I thought, someone must have already reported this. I should have checked the bug database. After all, I added a work-around to [this answer](https://stackoverflow.com/a/36021165/2711488) more than a year ago, so I suppose, I know this bug even longer. – Holger Jan 11 '19 at 13:46
  • @AlanBateman Is it just the conversion from `URI` to `Path` that's the bug or is the `modules` directory supposed to be "transparent" to users of jrtfs? – Slaw Jan 11 '19 at 15:28
  • 4
    It's the URL -> jrt Path conversion that has an issue. The modules directory is important for users of jrtfs. – Alan Bateman Jan 11 '19 at 15:59

1 Answers1

8

JRT File System

The part of the JEP you quote deals specifically with URLs. If you read on a little further you'll find where it discusses the JRT file system:

A built-in NIO FileSystem provider for the jrt URL scheme ensures that development tools can enumerate and read the class and resource files in a run-time image by loading the FileSystem named by the URL jrt:/, as follows:

FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
byte[] jlo = Files.readAllBytes(fs.getPath("modules", "java.base",
                                          "java/lang/Object.class"));

The top-level modules directory [emphasis added] in this filesystem contains one subdirectory for each module in the image. The top-level packages directory [emphasis added] contains one subdirectory for each package in the image, and that subdirectory contains a symbolic link to the subdirectory for the module that defines that package.

As you can see, the JRT file system has two directories directly under the root: modules and packages. These were added as part of JDK-8066492 and their purpose is described by that issue. So the problem is not that the NIO API cannot read resources in a JRT image. The problem is that:

/main/dict/xkcd_en

Really does not exist. The resource is actually located at:

/modules/main/dict/xkcd_en

JRT URLs

A JRT URL takes one of three forms (all mentioned in the part of the JEP you quote in your question):

  1. jrt:/$MODULE/$PATH
  2. jrt:/$MODULE
  3. jrt:/

The first form is for accessing specific resources in the JRT image and the one we care about. As you can see, the URL does not include the top-level directories mentioned above. You can think of the URL as always being relative to the modules directory.


JRT File System Provider Bug

That said, as pointed out by @Alan Bateman, you are encountering a bug. When you have a JRT URL and you try to convert it into a Path you should be getting a Path which points to an existing file. The problem is this conversion does not take the modules directory into account.

This bug was fixed in Java 13 by JDK-8224946.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Thanks for the great explanation. I think as API user I shoudn't care how exactly filesystem stores its data internally. If URL was resolved successfully then it must be possible to use it for access to corresonding resource. – Evan Jan 11 '19 at 15:11
  • 1
    According to the issue Alan Bateman linked in [his comment](https://stackoverflow.com/questions/54140750/java-nio-cant-read-files-from-jrt-image/54142975?noredirect=1#comment95119300_54140750), it appears that at least some of this behavior is a bug. – Slaw Jan 11 '19 at 15:25