Search code examples
javamavenfile-ioexe

Java executable can not read resources folder


I'm struggling with this issue now for a while and want to ask if anyone of you has experience with Java resource files in executables.

I have a java application build with maven directly converting the jar to an exe.

pom.xml

<build>
        <resources>
            <resource>
                <directory>${basedir}/src/main/resources</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>20</source>
                    <target>20</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>0.0.8</version>
                <executions>
                    <execution>
                        <!-- Default configuration for running with: mvn clean javafx:run -->
                        <id>default-cli</id>
                        <configuration>
                            <mainClass>com.example.package/com.example.package.Main</mainClass>
                            <launcher>${project.artifactId}</launcher>
                            <jlinkZipName>${project.artifactId}</jlinkZipName>
                            <jlinkImageName>${project.artifactId}</jlinkImageName>
                            <noManPages>true</noManPages>
                            <stripDebug>true</stripDebug>
                            <noHeaderFiles>true</noHeaderFiles>
                            <compress>2</compress>
                        </configuration>
                        <phase>package</phase>
                        <goals>
                            <goal>jlink</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>${project.basedir}\tools\warp-packer.exe</executable>
                    <arguments>
                        <argument>--arch</argument>
                        <argument>windows-x64</argument>

                        <argument>--input_dir</argument>
                        <argument>${project.build.directory}\${project.artifactId}</argument>

                        <argument>--exec</argument>
                        <argument>bin\${project.artifactId}.bat</argument>

                        <argument>--output</argument>
                        <argument>${project.build.directory}\${project.artifactId}.exe</argument>
                    </arguments>
                </configuration>
            </plugin>
        </plugins>
    </build>

The code contains a function trying to read all files in a resource folder:

public static String [] getFiles(){
    String[] outStrings = new File(Utillity.class.getResource("files").getPath()).list();
    System.out.println(Arrays.toString(out_strings));
    return outStrings;
}

This function causes an Exception getting executed as exe, because getResource("files") returns null. In the IDE is working everything fine.

After investigation I found that because I'm working in a jar environment I have to be careful with files.

After a look in several tutorials i tried figuring out a global path. This failed. For example this code:

String jarPath = CallingPythonScripts.class
    .getProtectionDomain()
    .getCodeSource()
    .getLocation()
    .toURI()
    .getPath();
System.out.println("JAR Path : " + jarPath);

// Get name of the JAR file
String jarName = jarPath.substring(jarPath.lastIndexOf("/") + 1);
System.out.println("JAR Name: " + jarName);

provided the following output:

JAR Path: /com.example.package
JAR Name: com.example.package

I also know that there has to be some kind of working solution, because the following code provides the content of a test file in the same folder hierachy level as the "files" folder:

InputStream is = CallingPythonScripts.class.getResourceAsStream("demo.txt");

try (
   InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
   BufferedReader br = new BufferedReader(isr)) {

       br.lines().forEach(line -> System.out.println(line));
}catch (IOException e){
    e.printStackTrace();
}

Also instead of "demo.txt", "files/demo2.txt" is readable. For the folder itself it returns null.

I'm quite sure it has something to do with the internal file representation. Also in the IDE everything is working fine, just the execution of the exe causes this.


Solution

  • new File(Utillity.class.getResource("files").getPath()).list();

    There's your problem. You cannot LIST resources, period.

    One serious problem is that the above code works 'fine' if your resources aren't jarred up. More robust takes on this idea even work on certain platforms (where you attempt to get a dir as a resource and open it, on some platforms you get a listing), but it is not supported - the spec does not mention that a classloader has to support this, and indeed, many do not.

    The conclusion is simply: It is impossible to list resources. If you run into a library that says it can, it is lying: At best it is a hack that only works under certain conditions.

    The standard workaround is what you should use for this - Service Loaders.

    The concept works as follows:

    • Either the programmers write it out by hand, or use a build system plugin, to end up with a text file containing the listing you are interested in. This text file is then included in a hardcoded path.
    • The application then loads that text file - which is fine. The resource system cannot list you contents of directories. But it has no problem giving you the full contents of a text file in a known location.
    • This listing is then used to load whatever you need to load.

    There's even a class baked into java that can do this: ServiceLoader. You should search the web for tutorials with that term and you'll find tons.

    There are lots of libraries and blog posts out there that try to work around this. Some for example get resources as URLs and then try to parse these and e.g. open up jar files. The thing is, the ClassLoader system is entirely abstract. A ClassLoader can load resources from the network, decrypt them on the fly, or make them up whole cloth. And they do not have to support listing as a primitive. Hence, any such attempt is incomplete. Use the service loader paradigm, as that is 'complete' - it works regardless of classloader implementation.