Retrieving resources for a subpackage class
Asked Answered
V

2

7

I haven't been working with java for long, so I'm not sure as what else to look for. I hope somebody can point me in the right direction.

Goal: I want to use a look up table, stored as a text file. But I don't want to use absolute paths, as in the end I'd like to pack a release and be able to call it from any location (the text file will be included int the packed release).

Current setup: I put the text file in a folder called "resources" (because from reading tutorials about java, I got the impression, this is where I'm supposed to put it to maintain a better structured project).

In the root package folder I have a class (MainClass.java) that is calling another class (LookUpClass.java) in a subpackage. The folder setup is as followed:

  • src
    • java
      • main.package.com
        • subpackage
          • LookUpClass.java
          • PlotterClass.java
        • MainClass.java
    • resources
      • LookUpTables
        • LookUpTable1.txt
        • LookUpTable2.txt

I wrote a method in LookUpClass.java that is retrieving a certain line from my lookup tables in resources. To retrieve the file and read out a certain line, I used

// Gets respective line from LUT
private static String getLineFromLUT(int line) {
    URL url = LookUpClass.class.getClass().getResource("/LookUpTables/LookUpTable1.txt");
    File file = new File(url.toURI());
    BufferedReader br = new BufferedReader(new FileReader(file));

    for (int i = 0; i < line; ++i)
        br.readLine();

    return br.readLine;
}

In my project structure the "java" folder is marked as "source", while "resources" is marked as, well, "resources".

My test setup is very simple:

public static void main(String[] args) throws URISyntaxException, IOException {
    String c = LookUpClass.getLineFromLUT(5);
    System.out.println("Color of line 5: " + c);
}

Output:

Color of line 5: 0  0   38

(Which is correct.)

I added the exact same lines to PlotterClass.java and it works fine, too.

Problem:

Now, If I try the same in MainClass.java I get an error with url being null. It seems the resource/resource folder can't be found.

I read through various postings on SO already and tried out several proposed solutions, which all failed so far:

  • If using LookUpClass.class.getClassLoader().getResource("/LookUpTables/LookUpTable1.txt") both callings from MainClass.java and LookUpClass.java fail (url is null).
  • I tried using following paths (all not working in either of the classes): "LookUpTables/LookUpTable1.txt" (removing starting "/") "/subpackage/LookUpTables/LookUpTable1.txt" "../subpackage/LookUpTables/LookUpTable1.txt"
  • Since using Idea IntelliJ, I checked "Settings > Build, Execution, Deployment > Compiter > Resource patterns" and added "*.txt" to the patterns. Nothing changed.
  • If adding Class c = LookUpClass.class.getClass();, in Debug mode c is "class.java.lang.Class". I was expecting something like "main.package.com.subpackage.LookUpClass".
  • At some point I tried using getResourceAsStream(), but I didn't understand how to get my (e.g.) 5th line, so I discarded it. I'm willing to read up on this, if it solves my problem though.

I have no idea how to solve this problem. And I realize that at this point I'm just trying out things, not even understanding why it could or could not work. For me, it just seems LookUpClass.java is run from a different location than MainClass.java. But the "resources"-folder and respective text file location never change. How can the file be found in one case, but not in the other?

Voleta answered 2/4, 2015 at 13:4 Comment(5)
Three further questions: Are you using an IDE (like Eclipse)? Are you using any build tool (like Maven)? Are your text files meant to be updated by the user or are they part of the distribution?Improbity
can you try to ensure the class object you are using to get the class loader from is actually loaded eg by try { Class.forName(MainClass.class.getName()); } catch (final ClassNotFoundException ex) { }Repair
@Improbity Yes to both: I'm using IntelliJ and the project is indeed built with Maven. The text files are meant to be static and part of the distribution. They contain RGB codes for a heat map.Voleta
@Repair I added this code to the beginning of getLineFromLUT, nothing happens, when executing. So I guess the MainClass is actually found. Executed MainClass.java as well as LookUpClass.java.Voleta
since the files are in the resources directory, i recommend using URL url = LookUpClass.class.getClass().getResource("resources/LookUpTables/LookUpTable1.txt"); this works for me as long as the project is not run as a jar.Repair
I
3

Maven has a standard directory layout. The directory src/main/resources is intended for such application resources. Place your text files into it.

You now basically have two options where exactly to place your files:

  1. The resource file belongs to a class.

An example for this is a class representing a GUI element (a panel) that needs to also show some images.

In this case place the resource file into the same directory (package) as the corresponding class. E.g. for a class named your.pkg.YourClass place the resource file into the directory your/pkg:

src/main
 +-- java/
 |    +-- your/pkg/
 |    |    +-- YourClass.java
 +-- resources/
      +-- your/pkg/
           +-- resource-file.txt

You now load the resource via the corresponding class. Inside the class your.pkg.YourClass you have the following code snippet for loading:

String resource = "resource-file.txt"; // the "file name" without any package or directory
Class<?> clazz = this.getClass(); // or YourClass.class
URL resourceUrl = clazz.getResource(resource);
if (resourceUrl != null) {
    try (InputStream input = resourceUrl.openStream()) {
        // load the resource here from the input stream
    }
}

Note: You can also load the resource via the class' class loader:

String resource = "your/pkg/resource-file.txt";
ClassLoader loader = this.getClass().getClassLoader(); // or YourClass.class.getClassLoader()
URL resourceUrl = loader.getResource(resource);
if (resourceUrl != null) {
    try (InputStream input = resourceUrl.openStream()) {
        // load the resource here from the input stream
    }
}

Choose, what you find more convenient.

  1. The resource belongs to the application at whole.

In this case simply place the resource directly into the src/main/resources directory or into an appropriate sub directory. Let's look at an example with your lookup file:

src/main/resources/
    +-- LookupTables/
         +-- LookUpTable1.txt

You then must load the resource via a class loader, using either the current thread's context class loader or the application class loader (whatever is more appropriate - go and search for articles on this issue if interested). I will show you both ways:

String resource = "LookupTables/LookUpTable1.txt";
ClassLoader ctxLoader = Thread.currentThread().getContextClassLoader();
ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
URL resourceUrl = ctxLoader.getResource(resource); // or sysLoader.getResource(resource)
if (resourceUrl != null) {
    try (InputStream input = resourceUrl.openStream()) {
        // load the resource here from the input stream
    }
}

As a first suggestion, use the current thread's context class loader. In a standalone application this will be the system class loader or have the system class loader as a parent. (The distinction between these class loaders will become important for libraries that also load resources.)

You should always use a class loader for loading resource. This way you make loading independent from the place (just take care that the files are inside the class path when launching the application) and you can package the whole application into a JAR file which still finds the resources.

Improbity answered 3/4, 2015 at 6:52 Comment(3)
Thank you very much for your time explaining everything and showing an example! I finally got it to work both with the thread's contex loader and the system class loader. I have yet to try your first solution, but am planning to do so next, since the lookup tables actually only belong to/are used by this one class. (Sorry for the late reply, went away for the extended weekend due to Easter holidays.)Voleta
Weirdly, when trying method 1, the code works if I actually add the full folder path (i.e. String resource = "your/pkg/resource-file.txt"), but it does not work, if I just use the file name alone (String resource = "resource-file.txt"). Did I miss something?Voleta
@Voleta No, you didn't miss something. I did! I mixed up ClassLoader.getResource and Class.getResource. When using a class loader you must also specify the (class-path relative) path. I will edit my answer accordingly.Improbity
R
0

I tried to reproduce your problem given the MWE you provided, but did not succeed. I uploaded my project including a pom.xml (you mentioned you used maven) here: http://www.filedropper.com/stackoverflow This is what my lookup class looks like (also showing how to use the getResourceAsStream method):

public class LookUpClass {

    final static String tableName = "resources/LookUpTables/LookUpTable1.txt";

    public static String getLineFromLUT(final int line) {
        final URL url = LookUpClass.class.getResource(tableName);
        if (url.toString().startsWith("jar:")) {
            try (final URLClassLoader loader = URLClassLoader
                    .newInstance(new URL[] { url })) {
                return getLineFromLUT(
                        new InputStreamReader(
                                loader.getResourceAsStream(tableName)), line);
            } catch (final IOException e) {
                e.printStackTrace();
            }
        } else {
            return getLineFromLUT(
                    new InputStreamReader(
                            LookUpClass.class.getResourceAsStream(tableName)),
                    line);
        }
        return null;
    }

    public static String getLineFromLUT(final Reader reader, final int line) {
        try (final BufferedReader br = new BufferedReader(reader)) {
            for (int i = 0; i < line; ++i)
                br.readLine();
            return br.readLine();
        } catch (final IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
Repair answered 2/4, 2015 at 14:38 Comment(1)
Thank you for taking time to help me solve my problem! Especially the part how to use getResourceAsStream() and then to read a line was very helpful.Voleta

© 2022 - 2024 — McMap. All rights reserved.