JAR-compatible File and Resource handling

This is a short description of how to correctly deal with the loading of resources in Java code.  The immediate reaction is to use Strings representing Paths, and pass them in to the File API to get OS file handles back.  This is, unfortunately, not a good way to do things.  It breaks as soon as the code is compiled in to a .jar.

In effect, we need to stop thinking of files contained within our IDE as just plain old files on the file system, and start thinking about what happens when our code gets turned in to a .jar and how that impacts the machinations that go in to dealing with resources. 

When to use java.io.File

The Java File APIs are designed for interacting with files outside of the class path (more on the class path later).  For now, think of this as meaning any file that will not be inside of the .jar when it is built or deployed.  Say, creating a handle to a document that will be read/written in a user's Home folder, or loading a configuration file.  If you want to load a resource that is part of the distribution (like a mesh, image file for a GUI, .sdf model, etc.) then you should not use Strings and Files.

All about the class path

Now that we know when to avoid the File API's, we need to know what API's we should use.  First, we need a crash-course on the concept of the class path.  This is a central part of understanding Java, but is completely abstracted away by IDEs.

The typical project structure in your IDE probably looks something like this:

Project
    ├── src
    ├── test
	[...] other stuff

The src and test folders have special significance that is usually indicated in the GUI.  In Eclipse, they are all just referred to as "source folders".  In IntelliJ IDEA, they are identified separately as source and test folders.  Either way, these are folders that are marked to be compiled in to .class files and then placed in to an output directory (src and test may have different output directories).  In turn, when you launch a "main" method from your IDE, the compiled .class files are loaded in to the class path.  This represents all loadable classes and resources available to the running program (even if you don't actually use any of it in your code path entered in to from your main, if it's on your class path it's available to the runtime).

Compiled class files retain the "package" structure that they had when they were source files, and packages in Java are mapped to folders in the output directory.  So the package us.ihmc.foo would map to a layout that looks like this in the src dir:

Project
└── src
	└── us
		└── ihmc
			└── foo

While this looks like a directory structure, try not to think of it as such.  Just think of it as a hierarchical addressing system which happens to correspond to directories when it is expressed on disk.  The same hierarchical addressing applies to the output directory.  So how does this relate to resources?

Resources on the class path

Java provides a way to address resources on the class path in an agnostic way, to deal with the fact that in the .jar case you can't use the File API's.  This is done by making a request of the ClassLoader to find a resource on the class path and open it as a "stream".  This does introduce more overhead on the developer side of things, as the InputStream interfaces aren't nearly as useful as the File API's when it comes to things like parsing text.  But for most cases, when dealing with items like images or the like, the encasing API's can usually be passed Streams directly, making it easy enough to deal with.  Given this, there are a few ways to get resources on to the class path and then load them reliably.  The way to do so is to add an additional folder to your IDE's list of folders to compile on to the class path, a resource folder, with a sane and simple layout that makes addressing by absolute address a simple prospect.  The structure looks like the following:

Project
    ├── src
    ├── test
    ├── resources
	[...] other stuff

The new "resources" folder will be added and auto-configured by Gradle, but if you need to do the configuration in your IDE manually you can simply add it as a new source folder in Eclipse or as a Resource folder in IntelliJ IDEA.  When there are multiple folders on the same class path, they get merged in the compile process in to a unified class path.  To visualize this, take a look at the following:

Project1
    ├── src
	|	└── us
	|		└── ihmc
	|			└── util
	|				└── ExampleClass.java
    ├── resources
	|	└── images
	|		└── Image.jpg
 
Project2
    ├── src
	|	└── us
	|		└── ihmc
	|			└── AwesomeClass.java
    ├── resources
	|	└── images
	|		└── Image.gif

Assuming that there's a dependency (either Project1 depends on Project2, or vice versa, it's not important for this example), the compilation step will produce an output with the following structure:

output
└── us
|	└── ihmc
|		├── AwesomeClass.class
|		└── util
|			└── ExampleClass.class
└── images
	├── Image.jpg
	└── Image.gif

The structures underneath each "source folder" get transferred in to the class path, with the results of the compilation (in the case of non-code, simply a copy operation), and everything gets merged together (in this sense, you should also be cognizant of naming collisions among non-code resources since your IDE won't detect them).  As such, the "absolute" resource address for the file in your workspace identified by the file path of "<ProjectDir>/resources/images/Image.jpg" would just be "/images/Image.jpg", without having to do any path relativization or anything like that.  This is extremely helpful when a resource lives in a project that is a dependency; since we're working with the class path instead of the file system, we no longer care which project the resource lives in since the class paths get merged.

URL's and getResourceAsStream instead of Strings and Files

Once this structure has been adopted, switching over to this new form addressing is pretty simple.  The base operating is querying the Class Loader for a resource.  A common pattern for this is:

getClass().getResourceAsStream(<absolute resource address>)

Note that the absolute resource addresses can always use "/" as a separator, regardless of whether or not the user is on Windows, and will get converted to a package path/URL behind the scenes.  An alternative pattern is to use:

getClass().getClassLoader().getResourceAsStream(<absolute resource address>)

The difference between these two implementations is that using the ClassLoader method, the resource address will always be treated as absolute, so you do not need the beginning forward slash (in fact, if you include it, the resource won't resolve correctly).

There is also a generic getResource() method, that doesn't return a Stream.  This is used to get the URL to a resource without converting it directly to an input stream, which is useful if you need to do some sort of identification on the resource before using it.  That looks like this:

getClass().getResource(<absolute path to resource>)

Similar to the above example, there is another always absolute version of this method attached to the ClassLoader.

What about working with entire directories?

 Unfortunately, there is no way to "iterate" over a collection of resources; that is, you can't query a package as a resource and then treat it as a directory.  You will, at the very least, need to know the name of the file you wish to load in advance and then attempt to load against a set of Strings representing class paths if you don't know exactly what directory it is in.

More Examples

Load System Resource
jFrame.setIconImage(new ImageIcon(ClassLoader.getSystemResource("us/ihmc/tools/icons/running-man-32x32.png").getPath()).getImage());

Debug Code to List Contents of a Resource Directory

private List<String> getResourceFiles(String path) throws IOException
{
   List<String> filenames = new ArrayList<>();
   final InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
   try (InputStream in = inputStream == null ? getClass().getResourceAsStream(path) : inputStream;
         BufferedReader br = new BufferedReader(new InputStreamReader(in)))
   {
      String resource;
      while ((resource = br.readLine()) != null) filenames.add(resource);
   }
   return filenames;
}

Filter by label

There are no items with the selected labels at this time.