javamodulejarresourcesjpackage

How to make images, properties, and other resources available to my modular application after jpackage?


I have the following project structure.

ProjectName
|
|---src
    |
    |---main
        |
        |---java
        |   |
        |   |---ModuleName
        |       |
        |       |---module-info.java
        |       |
        |       |---PackageName
        |           |
        |           |---Main.java
        |
        |---resources
            |
            |---ResourceParentFolder
                |
                |---ResourceSubFolderA
                |   |
                |   |---Resource1A.png
                |   |---Resource2A.png
                |   |---Resource3A.png
                |
                |---ResourceSubFolderB
                    |
                    |---Resource1B.png
                    |---Resource2B.png
                    |---Resource3B.png

Here is my Main.java.

package PackageName;

public class Main
{

   public static void main(String[] args) throws Exception
   {
   
      new Main();
   
   }

   public Main() throws Exception
   {
   
      System.out.println("0  - " + ModuleLayer.boot().findModule("ModuleName")     .map(o -> o.getClassLoader().getResource("PackageName/Main.class")));
      System.out.println("1  - " + ModuleLayer.boot().findModule("ModuleName")     .map(o -> o.getClassLoader().getResource("ResourceParentFolder")));
      System.out.println("2  - " + ModuleLayer.boot().findModule("ModuleName")     .map(o -> o.getClassLoader().getResource("ResourceParentFolder/ResourceSubFolderA")));
      System.out.println("3  - " + ModuleLayer.boot().findModule("ModuleName")     .map(o -> o.getClassLoader().getResource("/ResourceParentFolder")));
      System.out.println("4  - " + ModuleLayer.boot().findModule("ModuleName")     .map(o -> o.getClassLoader().getResource("/ResourceParentFolder/ResourceSubFolderA")));
      System.out.println("4b - " + ModuleLayer.boot().findModule("ModuleName")     .map(o -> o.getPackages()));
      System.out.println("5  - " + ModuleLayer.boot().modules());
      System.out.println("6  - " + ModuleLayer.boot().parents());
      System.out.println("7  - " + ModuleLayer.boot().toString());
      System.out.println("8  - " + Main.class.getClassLoader().getResource("ResourceParentFolder"));
      System.out.println("9  - " + Main.class.getClassLoader().getResource("/ResourceParentFolder"));
      System.out.println("10 - " + Main.class.getClassLoader().getResource("ResourceParentFolder/ResourceSubFolderA"));
      System.out.println("11 - " + Main.class.getClassLoader().getResource("/ResourceParentFolder/ResourceSubFolderA"));
      System.out.println("12 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("ResourceParentFolder"));
      System.out.println("13 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("/ResourceParentFolder"));
      System.out.println("14 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("ResourceParentFolder/ResourceSubFolderA"));
      System.out.println("15 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("/ResourceParentFolder/ResourceSubFolderA"));
   
   }

}
module ModuleName
{

   requires java.base;

}

Here is my shell script which performs compilation, executes it, creates a modular jar, executes that, and then calls jpackage.

echo "STARTING TO COMPILE MODULAR SOURCE CODE"

javac                                      \
        --module-source-path=src/main/java \
        --module=ModuleName            \
        -d classes

echo "STARTING TO RUN  MODULAR SOURCE CODE"

java                                             \
        --module-path="classes"                  \
        --module=ModuleName/PackageName.Main

echo "STARTING TO BUILD A MODULAR JAR"

jar                                               \
        --verbose                                 \
        --create                                  \
        --file run/executable/jar/ProjectName.jar \
        --main-class PackageName.Main             \
        -C classes/ModuleName .

echo "STARTING TO RUN  A MODULAR JAR"

java                                                      \
        --module-path="run/executable/jar"                \
        --module=ModuleName/PackageName.Main

echo "STARTING TO RUN JPACKAGE"

jpackage                                                  \
    --verbose                                             \
    --type msi                                            \
    --name ProjectName                                    \
    --input src/main/resources                            \
    --install-dir davidalayachew_applications/ProjectName \
    --vendor "David Alayachew"                            \
    --win-dir-chooser                                     \
    --module-path run/executable/jar                      \
    --module ModuleName/PackageName.Main                  \
    --win-console                                         \
    --java-options "--enable-preview"                     \
    --dest run/executable/installer

As you can see, it creates a few directories.

Now, the output during executions is where my frustration is.

Here is the output when running immediately after compiling.

STARTING TO RUN  MODULAR SOURCE CODE
0  - Optional[file:**ignore**/ProjectName/classes/ModuleName/PackageName/Main.class]
1  - Optional[file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder]
2  - Optional[file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder/ResourceSubFolderA]
3  - Optional.empty
4  - Optional.empty
4b - Optional[[PackageName]]
5  - [**ignore**]
6  - []
7  - **ignore**
8  - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder
9  - null
10 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder/ResourceSubFolderA
11 - null
12 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder
13 - null
14 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder/ResourceSubFolderA
15 - null

Ok, it found the folders. And since it is java.net.URLConnection, I can traverse the directory by modifying the URL. So, things worked running the code immediately after compile.

Here is the output when running the created jar file.

STARTING TO RUN  A MODULAR JAR
0  - Optional[jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/PackageName/Main.class]
1  - Optional[jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/]
2  - Optional[jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/ResourceSubFolderA/]
3  - Optional.empty
4  - Optional.empty
4b - Optional[[ResourceParentFolder.ResourceSubFolderB, ResourceParentFolder.ResourceSubFolderA, ResourceParentFolder, PackageName]]
5  - [**ignore**]
6  - []
7  - **ignore**
8  - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/
9  - null
10 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/ResourceSubFolderA/
11 - null
12 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/
13 - null
14 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/ResourceSubFolderA/
15 - null

Ok, a different type of connection, but we still found the same folders. I will likely have to do URL construction a little differently, but the path forward is fairly clear.

Now, here is what happens after I run my newly installed jpackage version of my application.

0  - Optional[jrt:/ModuleName/PackageName/Main.class]
1  - Optional.empty
2  - Optional.empty
3  - Optional.empty
4  - Optional.empty
4b - Optional[[ResourceParentFolder, PackageName, ResourceParentFolder.ResourceSubFolderA, ResourceParentFolder.ResourceSubFolderB]]
5  - [module ModuleName, module java.base]
6  - []
7  - ModuleName, java.base
8  - null
9  - null
10 - null
11 - null
12 - null
13 - null
14 - null
15 - null

Nothing can be found aside from the .class file I put as a sanity check.

So, I opened up the installation directory, and I see that my resources are definitely there. The whole directory, subdirectories, and files. Everything inside of and including ResourceParentFolder from my tree structure has been copied over to the C:/Program File/davidalayachew_applications/ProjectName/app folder.

I tried a laundry list of configurations, like prepending the getResource parameters with app, but that was no good. I have cycled through >100 permutations, and truthfully, I have forgotten most of them.

Can someone give me a pointer on how to get my resources to be accessible to my application after calling jpackage? I specifically need to be able to see the contents of a folder. Not just fetching individual files.


Solution

  • You should not use .getResource() or .getResourceAsStream() to walk directories or view their contents. That was never part of their design. They are only meant to fetch files, not scan or see the contents of the directories.

    That was the source of my bug. Yes, I can simply view the folder contents by doing .getResource() or .getResourceAsStream(), but that works for plain folders and jar files only because plain folders (obviously) have that functionality built in and jars can halfway simulate the same thing. But trying to do that for an executable file (.exe file), will just plain not work. Here is the real way to do it.

    Modules are a beautiful thing. If you want to read the contents of your Module, just get an instance of a java.lang.module.ModuleReader. From there, you can get the directory searching abilities I was trying to get with getResource() or .getResourceAsStream(). Just call ModuleReader.list(). Please note, it implements AutoCloseable, so use Try-with-resources. I am also following suit with what @Slaw did in this answer. https://stackoverflow.com/a/77030323/10118965

    package PackageName;
    
    public class Main
    {
    
       public static void main(String[] args) throws Exception
       {
       
          new Main();
       
       }
    
       public Main() throws Exception
       {
          var module = Main.class.getModule();
          var reference = module.getLayer()
                  .configuration()
                  .findModule(module.getName())
                  .orElseThrow()
                  .reference();
          try (var reader = reference.open()) {
              reader.list().forEach(System.out::println);
          }
       }
    

    From there, I can scan my directories and see the contents of my modules, and all I need to do is use a ModuleReader. I can pass the values into a getResource or getResourceAsStream call and it should work just fine. Ideally, one of the Module specific ones.

    So, let's apply these new strategies to my problem. I have already pasted @Slaw 's Main.java above in this answer. Here are my new shell scripts.

    First is the raw compile command. That remains unchanged for me.

    javac                                      \
            --module-source-path=src/main/java \
            --module=ModuleName                \
            -d classes
    

    Next is running the modular source code. This one needs to change slightly.

    java                                                 \
            --module-path="classes"                      \
            --module=ModuleName/PackageName.Main         \
            --patch-module ModuleName=src/main/resources
    

    As you can see, I added a --patch-module command line there. Special thanks (yet again!) to @Slaw 's answer -- https://stackoverflow.com/a/77030323/10118965

    The --patch-module command has documentation here -- https://docs.oracle.com/en/java/javase/20/docs/specs/man/java.html (Ctrl+F "--patch-module")

    This command is what you use to insert resources into modules. All you do is specify the module you want to "patch" on the left side of the equals, and then on the right, put the directory that you want to copy over, recursively.

    So, in my case, I want to copy the contents of src/main/resources (not the directory itself, but its contents) into my module. So, I just call the above command when doing my run command, and things are good!

    I would encourage reading @Slaw 's answer, it goes into more detail.

    Next is the script to create my jar file. This one will also change slightly.

    jar                                               \
            --verbose                                 \
            --create                                  \
            --file run/executable/jar/ProjectName.jar \
            --main-class PackageName.Main             \
            -C classes/ModuleName .                   \
            -C src/main/resources .
    

    The only new addition is the second -C call. The jar command does not have the --patch-module option, but you can think of the -C option as essentially an alias for --patch-module. By doing this, I achieve the exact same effect (to my understanding).

    Next is my run command for the jar. This one remains unchanged.

    java                                                      \
            --module-path="run/executable/jar"                \
            --module=ModuleName/PackageName.Main
    

    The reason why it is unchanged is because -C actually inserts the src/main/resources into the jar. So, no need to insert any special commands! Since we are ingesting the jar, then we are ingesting all of its contents too - which means that we get the resources too!

    Finally, is the jpackage shell script. This one actually gets simpler!

    jpackage                                                  \
        --verbose                                             \
        --type msi                                            \
        --name ProjectName                                    \
        --install-dir davidalayachew_applications/ProjectName \
        --vendor "David Alayachew"                            \
        --win-dir-chooser                                     \
        --module-path run/executable/jar                      \
        --module ModuleName/PackageName.Main                  \
        --win-console                                         \
        --java-options "--enable-preview"                     \
        --dest run/executable/installer
    

    The only change here is that I removed the --input option from the jpackage script. Special thanks to @VGR for pointing this out! --input is now redundant because we already have the resources inside of our jar -- no need to manually insert it!

    Now, when I run all of the above, things work out beautifully!

    Here is the output when I run my modular code (not a jar).

    module-info.class
    PackageName/
    PackageName/Main.class
    ResourceParentFolder/
    ResourceParentFolder/ResourceSubFolderA/
    ResourceParentFolder/ResourceSubFolderA/Resource1A.png
    ResourceParentFolder/ResourceSubFolderA/Resource2A.png
    ResourceParentFolder/ResourceSubFolderA/Resource3A.png
    ResourceParentFolder/ResourceSubFolderB/
    ResourceParentFolder/ResourceSubFolderB/Resource1B.png
    ResourceParentFolder/ResourceSubFolderB/Resource2B.png
    ResourceParentFolder/ResourceSubFolderB/Resource3B.png
    

    Remember, I changed my output command to simply list the locations of all of my module contents. If I wanted to fetch any of these resources, I would just call my getResource or getResourceAsStream methods.

    But to be absolutely sure, I can instead, make a call to actually fetch one of the resources.

    Here is my new Main.java.

    package PackageName;
       
    public class Main
    {
    
       public static void main(String[] args) throws Exception
       {
       
          new Main();
       
       }
    
       public Main() throws Exception
       {
       
       
          try
          (
             final java.lang.module.ModuleReader reader =
                ModuleLayer
                    .boot()
                    .configuration()
                    .findModule("ModuleName")
                    .get()
                    .reference()
                    .open()
          )
          {
          
             final String firstResource =
                reader
                   .list()
                   .filter(each -> each.contains("ResourceParentFolder/ResourceSubFolderA/Resource1A.png"))
                   .findFirst()
                   .orElseThrow()
                   ;
          
             final var myModule =
                ModuleLayer
                   .boot()
                   .findModule("ModuleName")
                   .orElseThrow()
                   ;
          
             final var inputStream = myModule.getResourceAsStream(firstResource);
          
             final java.awt.Image image = javax.imageio.ImageIO.read(inputStream);
          
             System.out.println(image);
          
          }
       
       
       
       }
    
    }
    

    Now, instead of printing all resources with a Stream<String>, I simply filter down to the one I want, and then load it.

    And when I do it like this, it all just works!

    Here is the output for my run command (no jar).

    BufferedImage@3ecd23d9: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@569cfc36 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 96 height = 88 #numDataElements 4 dataOff[0] = 3
    

    It successfully fetched the image! I can take the returned object and put into my GUI, and everything works just fine!

    Here is the output for my run command (yes jar).

    BufferedImage@de0a01f: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@4c75cab9 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 96 height = 88 #numDataElements 4 dataOff[0] = 3
    

    A 1-to-1 match!

    And here is the output for my jpackage installation.

    BufferedImage@1f554b06: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@694e1548 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 96 height = 88 #numDataElements 4 dataOff[0] = 3
    

    It's a match! I now have a solution for querying my module and fetching resources from it! And the solution is uniform across all execution contexts -- whether I am running some just-compiled .class files, or I am running a jar file, or I am executing the application as a jpackage created executable binary, I am set!