Search code examples
javajarioresourcesexecutable-jar

Issue to list names of files from `resources` directory. (in JAR/EXE application)


I want to randomly display .gif(s) in my application. I would rather have my code be "self-aware" of it's content. That's why instead of manually adding file names [like List blabla .add("/example.gif")]

This works just fine within IDE (intellij), but stopps in .jar archive. I always struggle with similiar problems when dealing with resources, yet still can not overcome this one.

This is my class that is meant to list all .gif files that are contained in my resource dir:

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class ResourceHandler {
   private static final String RESOURCE_PATH = "resources";
   private static final List<String> GIF_NAMES_LIST = new ArrayList<>();

   public ResourceHandler() {
       File resourceDir = new File(RESOURCE_PATH);
       File[] files = resourceDir.listFiles();
       if (files != null) {
           for (File file : files) {
               GIF_NAMES_LIST.add(file.getName());
           }
       }
   }

   List<String> getGifNamesList() {
       return GIF_NAMES_LIST;
   }
}

What I am doing next in my Main:

            String gifName = GIF_NAMES_LIST.get(RANDOM.nextInt(GIF_NAMES_LIST.size()));

What I am getting: Exception in thread "main" java.lang.IllegalArgumentException: bound must be positive at java.base/java.util.Random.nextInt(Random.java:322) at Main.main(Main.java:28)

I understand that it does not read from resources, hence length of 0. How do I modify my ResourceHandler class then?


Solution

  • There seem to be a lot of misleading answers to this frequently asked question, and a lot of misconceptions about what will work.

    Short answer: It is not possible to list application resources, except using a ModuleReader in a named module. A safe, universal solution is to include a text file that contains all of the resource paths. It’s your application, so you know what’s in it at build time.

    1. You cannot use the File class.

    Your classes may not be in a directory. They may be in a .jar file or an application image. The File class only works on directory entries.

    A .jar file is one archive file, not a directory. The entries inside a .jar file are not files; they are just parts of the archive, compressed data embedded with the .jar file. They cannot be listed or read with the File class.

    An application image is a single file that combines an application’s modules. Its format is undocumented. Attempting to understand that format is not safe, since it may change in the future without notice. Entries in the image cannot be listed or read with the File class.

    2. You cannot scan the .jar file.

    Some solutions suggest scanning the .jar file using JarFile, JarInputStream, or the zip file system. This is not safe for two reasons:

    • Using getClass().getProtectionDomain().getCodeSource().getLocation() to find the application .jar file will fail eventually, because:

    • An application is not guaranteed to be in a .jar file. The standard jlink tool produces an application image which usually does not contain .jar files. Also, some web application containers deploy applications using a scheme which is neither a plain directory nor a .jar file.

    3. A plain text listing file is the best approach.

    It’s your application. You already know what’s in it. So create a plain text file that lists the path of each resource. At runtime, you can easily read that resource:

    Collection<String> resourcePaths;
    
    try (BufferedReader listing =
             new BufferedReader(
                 new InputStreamReader(
                     MyApplication.class.getResourceAsStream(
                         "data-file-list.txt"),
                     StandardCharsets.UTF_8));
         Stream<String> lines = listing.lines()) {
    
        resourcePaths = lines.toList();
    }
    

    It is even possible to automatically generate this listing file at build time (not at runtime), if one is proficient with certain build tools.

    4. You can list resources in a Java module.

    There is one way to safely list resources, but it only works if your application is in a named module. You can use ModuleReader.list():

    Collection<String> resourcePaths;
    String imagesPath = "com/example/myapp/images/";
    
    Module m = MyApplication.class.getModule();
    String moduleName = m.getName();
    ResolvedModule resolved = 
        m.getLayer().configuration().findModule(moduleName).get();
    try (ModuleReader reader = resolved.reference().open();
         Stream<String> resources = reader.list()) {
    
        resourcePaths =
            resources.filter(r -> r.startsWith(imagesPath)).toList();
    }