0

so my project is here: https://github.com/Potat-OS1/project_thingo and i started the project from a template.

under the champPortrait section is where i'm having my problem. when i run it in the IDE the path works, as i understand it its the relative path of the build folder. does it not use this path when its packaged? what path should i be using?

i can getResourceAsStream the contents of the folder but in this particular case i need the folder its self so i can put all the names of files inside of the folder into a list.

Potat.OS1
  • 3
  • 3
  • 1
    If you're trying to list the contents of a package that is in a jar file, there is no nice way to do that. The Spring Framework has some classes that do so, and the implementation is a bit hacky, but you could emulate that. However, since the contents of a jar file cannot be changed after deployment, probably the easiest thing is just to have a text file in your resources listing the content, and read that text file at runtime. – James_D Aug 10 '22 at 17:28
  • @James_D do you think maybe i could set up a separate base java program in the resource folder that writes the txt containing the names? is that a hacky solution you were referencing? ive written a program just like that a while back for a friend trying to keep a database of songs they downloaded, maybe i can just repurpose it? – Potat.OS1 Aug 10 '22 at 18:48
  • More sensible would be to write a gradle build plugin that checked the relevant *source* folder, and listed its contents into a text file in resources. Then incorporate that into your build process. And of course in the code just read the text file as a resource to get the list of "files" (which are not really files, of course, they're jar entries). – James_D Aug 10 '22 at 19:07
  • The "hacky" solution I was referring to is something like: 1. `URL myURL = getClass().getResource(getClass().getSimpleName()+".class");`. 2. Parse out the resulting URL to get the location of the jar file on the file system. 3. Create a `JarFile` object using the location of the jar file. 4. Get the `entries()` from the jar file and select the ones in the appropriate package. Of course, all this fails if it's not actually bundled as a jar file (e.g. when you're running in the IDE), so you'd need to code around that. – James_D Aug 10 '22 at 19:11
  • @James_D i havent had to make a gradle build plugin before, is there anything i should know before i dive into trying to make one? – Potat.OS1 Aug 10 '22 at 19:12
  • Honestly, though, I would just hard code the text file in the resources hierarchy, and update it manually if I changed the content of the relevant folder. Then read from the text file as a resource in code. – James_D Aug 10 '22 at 19:12
  • No clue, I have never written a gradle plugin either. – James_D Aug 10 '22 at 19:13
  • @James_D thats probably the wiser move to do just writting the text file. i just wanted better scalability i guess is the way to put it? like if i add an entry down the road i'd have to edit the text file, then add the image instead of chucking the image in and having the code update its self. – Potat.OS1 Aug 10 '22 at 19:18

2 Answers2

3

When the application is bundled with jpackage, all classes and resources are packaged in a jar file. So what you are trying to do is read all the entries in a particular package from a jar file. There is no nice way to do that.

Since the contents of the jar file can't be changed after deployment, the easiest solution is probably just to create a text resource listing the files. You just have to make sure the you update the text file at development time if you change the contents of that resource.

So, e.g., if in your source hierarchy you have

resources
    |
    |--- images
            |
            |--- img1.png
            |--- img2.png
            |--- img3.png

I would just create a text file resources/images/imageList.txt with the content

img1.png
img2.png
img3.png

Then in code you can do:

List<Image> images = new ArrayList<>();
String imageBase = "/images/"
try (BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/images/imageList.txt"))) {
    br.lines().forEach(imageName -> {
        URL imageUrl = getClass().getResource(imageBse + imageName);
        Image image = new Image(imageURL.toExternalForm());
        images.add(image);
    }
} catch (Exception exc) {
    exc.printStackTrace();
}

As mentioned, you will need to keep the text file in sync with the contents of the resource folder before building. If you're feeling ambitious, you could look into automating this as part of your build with your build tool (gradle/Maven etc.).

James_D
  • 201,275
  • 16
  • 291
  • 322
2

The Java resource API does not provide a supported way to list the resources in a given package. If you aren't using a framework that provides their own solution (e.g., Spring), then probably the easiest and sufficiently robust solution is to do what @James_D demonstrates: Create another resource that simply lists the names of the resources in the current package. Then you can read that resource to get the names of the other resources.

For a relatively small number of resources, where the number doesn't change often, creating the "name list" resource manually is probably sufficient. But you've tagged this question with , so another option is to have the build tool create these "name list" resources for you. This can be done in plugin, or you could do it directly in your build script.


Example

Here's an example of creating the "plugin" in your build script.

Sources

Source structure:

\---src
    \---main
        +---java
        |   \---sample
        |           Main.java
        |
        \---resources
            \---sample
                    bar.txt
                    baz.txt
                    foo.txt
                    qux.txt

Where each *.txt file in src/main/resources/sample contains a single line which says Hello from <filename>!.

build.gradle.kts (Kotlin DSL):

plugins {
    application // implicitly applies the Java Plugin as well
}

application {
    mainClass.set("sample.Main")
}

// gets the 'processResources' task and augments it to add the desired
// behavior. This task processes resources in the "main" source set.
tasks.processResources {
    // 'doLast' means everything inside happens at the end, or at least
    // near the end, of this task
    doLast {
        /*
         * Get the "main" source set. By default, this essentially
         * represents the files under 'src/main'. There is another
         * source set added by the Java Plugin named "test", which
         * represents the files under 'src/test'.
         */
        val main: SourceSet by sourceSets

        /*
         * Gets *all* the source directories in the main source set
         * used for resources. By default, this will only include
         * 'src/main/resources'. If you add other resource directories
         * to the main source set, then those will be included here as well.
         */
        val source: Set<File> = main.resources.srcDirs
        /*
         * Gets the build output directory for the resources in the
         * main source set. By default, this will be the
         * 'build/resources/main` directory. The '!!' bit at the end
         * of this line of code is a Kotlin language thing, which
         * basically says "I know this won't be null, but fail if it is".
         */
        val target: File = main.output.resourcesDir!!

        /*
         * This calls the 'createResourceListFiles' function for every
         * resource directory in 'source'.
         */
        for (root in source) {
            // the last argument is 'root' because the first package is
            // the so-called "unnamed/default package", which are resources
            // under the "root"
            createResourceListFiles(root, target, root)
        }
    }
}

/**
 * Recursively traverses the package hierarchy of the given resource root and creates
 * a `resource-list.txt` resource in each package containing the absolute names of every
 * resource in that package, with each name on its own line. If a package does not have
 * any resources, then no `resource-list.txt` resource is created for that package.
 * 
 * The `resourceRoot` and `targetDir` arguments will never change. Only the `packageDir`
 * argument changes for each recursive call.
 *
 * @param resourceRoot the root of the resources
 * @param targetDir the output directory for resources; this is where the 
 *                  `resource-list.txt` resource will be created
 * @param packageDir the current package directory
 */
fun createResourceListFiles(resourceRoot: File, targetDir: File, packageDir: File) {
    // get all non-directories in the current package; these are the resources
    val resourceFiles: List<File> = listFiles(packageDir, File::isFile)
    // only create a resource-list.txt file if there are resources in this package
    if (resourceFiles.isNotEmpty()) {
        /*
         * Determine the output file path for the 'resource-list.txt' file. This is
         * computed by getting the path of the current package directory relative
         * to the resource root. And then resolving that relative path against
         * the output directory, and finally resolving the filename 'resource-list.txt'
         * against that directory.
         * 
         * So, if 'resourceRoot' was 'src/main/resources', 'targetDir' was 'build/resources/main',
         * and 'packageDir' was 'src/main/resources/sample', then 'targetFile' will be resolved
         * to 'build/resources/main/sample/resource-list.txt'.
         */
        val targetFile: File = targetDir.resolve(packageDir.relativeTo(resourceRoot)).resolve("resource-list.txt")
        // opens a BufferedWriter to 'targetFile' and will close it when
        // done (that's what 'use' does; it's like try-with-resources in Java)
        targetFile.bufferedWriter().use { writer ->
            // prints the absolute name of each resource on their own lines
            for (file in resourceFiles) {
                /*
                 * Prepends a forward slash to make the name absolute. Gets the rest of the name
                 * by getting the relative path of the resource file from the resource root. Replaces
                 * any backslashes with forward slashes because Java's resource-lookup API uses forward
                 * slashes (needed on e.g., Windows, which uses backslashes for filename separators).
                 * 
                 * So, a resource at 'src/main/resources/sample/foo.txt' would result in
                 * '/sample/foo.txt' being written to the 'resource-list.txt' file.
                 */
                writer.append("/${file.toRelativeString(resourceRoot).replace("\\", "/")}")
                writer.newLine()
            }
        }
    }

    /*
     * Gets all the child directories of the current package directory, as these
     * are the "sub packages", and recursively calls this function for each
     * sub package.
     */
    for (packageSubDir in listFiles(packageDir, File::isDirectory)) {
        createResourceListFiles(resourceRoot, targetDir, packageSubDir)
    }
}

/**
 * @param directory the directory to list the children of
 * @param predicate the filter function; only children for which this function 
 *                  returns `true` are included in the list
 * @return a possibly empty list of files which are the children of `dir`
 */
fun listFiles(directory: File, predicate: (File) -> Boolean): List<File> 
    = directory.listFiles()?.filter(predicate) ?: emptyList()

Main.java:

package sample;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;

public class Main {

  public static void main(String[] args) throws IOException {
    for (var resource : resources()) {
      System.out.printf("Contents of '%s':%n", resource);
      try (var reader = openResource(resource)) {
        String line;
        while ((line = reader.readLine()) != null) {
          System.out.printf("  %s%n", line);
        }
        System.out.println();
      }
    }
  }

  public static List<String> resources() throws IOException {
    try (var input = openResource("/sample/resource-list.txt")) {
      return input.lines().toList();
    }
  }

  public static BufferedReader openResource(String name) throws IOException {
    var input = Main.class.getResourceAsStream(name);
    return new BufferedReader(new InputStreamReader(input));
  }
}

Output

After the processResources task runs, you'll have the following /sample/resource-list.txt file in your build output:

/sample/bar.txt
/sample/baz.txt
/sample/foo.txt
/sample/qux.txt

And running the application (./gradlew clean run) will give the following output:

> Task :run
Contents of '/sample/bar.txt':
  Hello from bar.txt!

Contents of '/sample/baz.txt':
  Hello from baz.txt!

Contents of '/sample/foo.txt':
  Hello from foo.txt!

Contents of '/sample/qux.txt':
  Hello from qux.txt!


BUILD SUCCESSFUL in 2s
4 actionable tasks: 4 executed

Notes

Note that the resource-list.txt resource(s) will only exist in your build output/deployment. It does not exist in your source directories. Also, the way I implemented this, it will only list resources in your source directories. Any resources generated by, for example, an annotation processor will not be included. You could, of course, modify the code to fix that if it becomes an issue for you.

The above will only run for production resources, not test resources (or any other source set). You can modify the code to change this as needed.

If a package does not have any resources, then the above will not create a resource-list.txt resource for that package.

Each name listed in resource-list.txt is the absolute name. It has a leading /. This will work with Class#getResource[AsStream](String), but I believe to call the same methods on ClassLoader (if you need to for some reason) you'll have to remove the leading / (in code).

Finally, I wrote the Kotlin code in the build script rather quickly. There may be more efficient, or at least less verbose, ways to do the same thing. And if you want to apply this to multiple projects, or even multiple subprojects of the same project, you can create a plugin. Though it may be that some plugin already exists for this, if you're willing to search for one.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • so while my project is built in gradle, i actually havent messed with it much (im building my project off of a template). would i add the kotlin code directly to the build.gradle, or would i create a seperate file then reference it in the build.gradle? either way i appreciate this answer! will look to implement it or something similar when i wake up more. – Potat.OS1 Aug 11 '22 at 14:42
  • so im not very familiar with doing stuff with gradle, my project has a build.gradle, not a build.gradle.kts is this meaningful or do i have to do changes to my project to accommodate? and in the first code block you posted im dumb and a little confused about the source, target, resourceDir and targetDir, for the Dir's, am i supposed to put the paths to the folders respectively? like if my structure is: src--- main--- java--- com.example.app resources--- folderwithstuff – Potat.OS1 Aug 11 '22 at 17:51
  • ah its not saving the structure im trying to write out, basically what you wrote with different names. would i put the path to the resource dir for example from the src like src/main/resources/folderwithstuff. and source and target do i use the same paths respectively? – Potat.OS1 Aug 11 '22 at 17:55
  • You can put the code directly in your build script (the `build.gradle[.kts]` file), or you can separate it out into another script. Given the relative simplicity, I would keep in inside the main build script unless you see a reason to refactor. And `build.gradle` means your build script is using the Groovy DSL. A `build.gradle.kts` build script is using the Kotlin DSL. The former uses the Groovy programming language, and the latter uses the Kotlin programming language. I'm not as familiar with Groovy, so I always use the Kotlin DSL. The code I provided is Kotlin, not Groovy. – Slaw Aug 12 '22 at 04:59
  • I've edited my answer to add comments to the Kotlin code in the build script to hopefully help you understand what is happening. But it may be beneficial to read the [Building Java & JVM Projects](https://docs.gradle.org/current/userguide/building_java_projects.html#building_java_projects) Gradle documentation, as understanding how Gradle works and is organized will help also help. Also the [Groovy DLS](https://docs.gradle.org/current/userguide/groovy_build_script_primer.html) and [Kotlin DLS](https://docs.gradle.org/current/userguide/kotlin_dsl.html) primers. – Slaw Aug 12 '22 at 05:56
  • And keep in mind that the entire build script is just code. So, for instance, the `tasks` bit is being called on the implicit [`Project`](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html) instance of the build script, and it returns a `TaskContainer` object. The `processResources` bit is called on the `TaskContainer` object to get the object for that task, which I believe is an instance of `Copy`. Then you have a "closure"/lambda which has the `Copy` object as a receiver, and so `doLast` is implicitly called on that `Copy` object. And so on... – Slaw Aug 12 '22 at 06:03
  • And the code I wrote is "dynamic". If you have `src/main/resources/images/image1.png` then the `resource-list.txt` file generated for your `images` package will contain `/images/image1.png`. – Slaw Aug 12 '22 at 06:14