Locating resources in Java

A resource is a file situated somewhere in the class path. It can be a file in a package folder, in the classes folder or in a jar file. Resources are usually needed at runtime and they can be properties files, images and so on. The ClassLoader and Class classes provide methods to find the desired resources but a little bit of attention has to be payed to the quirks of this API.

The API

At the java.lang.Class level there are two methods offered:

public URL getResource(String name)
public InputStream getResourceAsStream(String name)

The javadoc for class specifies how the methods delegate to the corresponding methods on the java.lang.ClassLoader:

public URL getResource(String name)
public InputStream getResourceAsStream(String name)

public static URL getSystemResource(String name)
public static InputStream getSystemResourceAsStream(String name)

As for the name of the resources again the javadoc specifies:

Before delegation, an absolute resource name is constructed from the given resource name using this algorithm:
* If the name begins with a ‘/’ (‘\u002f’), then the absolute name of the resource is the portion of the name following the ‘/’.
* Otherwise, the absolute name is of the following form: modified_package_name/name, where the modified_package_name is the package name of this object with ‘/’ substituted for ‘.’ (‘\u002e’).

Here is the first trick – use always ‘/’ in the path and not File.separatorChar. On Windows the File.separatorChar will provide some positive results but for sure the solution is not portable and not safe.
The second important trick is a difference between the Class and ClassLoader APIs.
* The Class methods accept absolute paths, starting with ‘/’, and relative paths.
* The ClassLoader only accepts paths relative to the root of the class path – practically an absolute path without the leading ‘/’ character.

On Windows using the ‘\\’ character has interesting results but probably they are a side effect or a bug. The Class methods cannot recognize the absolute paths anymore while the ClassLoader methods now find now resources specified as an absolute path. But again this is not a safe way to use the APIs.

For the test program that will illustrate all these considerations first I define a helper class, a resource wrapper. Its purpose is to provide convenience methods for returning absolute and relative paths/names. It also allows me to test the difference in behavior when I use ‘/’ versus ‘\\’ characters as path separators for resources.

package com.littletutorials.res;

public class ResourceWrapper
{
    /** file separator string */
    public final String SEP = "/";
    /** file separator character */
    public final char SEPC = '/';
    /** file name */
    final private String fileName;
    /** absolute path in package tree with leading file separator */
    final private String absPath;
    /** absolute path in package tree with NO leading file separator */
    final private String relToRootPath;

    /**
     * @param fileName file name
     * @param absPath absolute path of the resource
     */
    public ResourceWrapper(String fileName, String absPath)
    {
        this.fileName = fileName.trim();
        String path = absPath.trim().replace('/', SEPC).replace('\\', SEPC);
        if (path.length() == 0 || (path.charAt(0) != SEPC))
        {
            path = SEPC + path;
        }
        if (path.length() > 1 && path.charAt(path.length() - 1) == SEPC)
        {
            path.substring(0, path.length() - 2);
        }

        this.absPath = path;
        this.relToRootPath = this.absPath.substring(1);
    }

    /**
     * @return  the file name
     */
    public String getFileName()
    {
        return fileName;
    }

    /**
     * @return resource path relative to the root
     */
    public String getNameRelativeToRoot()
    {
        if (absPath.equals(SEP))
        {
            return this.fileName;
        }
        return relToRootPath.concat(SEP).concat(this.fileName);
    }

    /**
     * @return absolute resource path
     */
    public String getAbsoluteName()
    {
        if (absPath.equals(SEP))
        {
            return absPath.concat(this.fileName);
        }

        return absPath.concat(SEP).concat(this.fileName);
    }

    /**
     * @param c class
     * @return relative path
     */
    public String getNameRelativeTo(Class<?> c)
    {
        String pkPath = c.getPackage().getName().replace('.', SEPC);

        String regexp = "[/\\\\]";
        String[] relToRootPathElems = this.relToRootPath.split(regexp);
        String[] pkPathElems = pkPath.split(regexp);

        int identicalLevels = 0;
        for (int i = 0; i < relToRootPathElems.length && i < pkPathElems.length; i++)
        {
            if (relToRootPathElems[i].equals(pkPathElems[i]))
            {
                identicalLevels++;
            }
            else
            {
                break;
            }
        }

        StringBuffer relPath = new StringBuffer();
        // append the return levels
        for (int i = identicalLevels; i < pkPathElems.length; i++)
        {
            relPath.append("..").append(SEP);
        }
        // append the resource branch
        for (int i = identicalLevels; i < relToRootPathElems.length; i++)
        {
            if (relToRootPathElems[i].equals(""))
            {
                continue;
            }
            relPath.append(relToRootPathElems[i]).append(SEP);
        }

        // add the file name
        relPath.append(this.fileName);
        return relPath.toString();
    }
}

Resources
On the constructor the specified absolute path is transformed to a standard format starting with the file separator character. This character can be changed on line 6 and 8 for testing purposes. The getNameRelativeTo(Class c) method takes as a parameter a class instance and calculates a relative path from the location of that class in the package tree to the location of the resource.

For this test I prepared a package tree with this structure:

The resources we are going to try to find are text files located in various places in the package tree. Analyze the above image to understand what the test is trying to prove.

The test program tries to locate all these resources using absolute and relative paths. The relative paths will be relative to the class used to call the getResource() methods or relative to the class path root when the ClassLoader API is used.

package com.littletutorials.res;

import java.io.*;
import java.net.*;

public class ResourceLocator
{
    /**
     * @param resources resources to locate
     */
    private void locate(ResourceWrapper [] resources)
    {
        for (ResourceWrapper rw : resources)
        {
            System.out.println("*** Resource: " + rw.getFileName());
            Class<?> targetClass = getClass();
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

            URL urlAbs1 = targetClass.getResource(rw.getAbsoluteName());
            System.out.println("[method: CLASS][resource ID: " +
                rw.getAbsoluteName() + "][URL: " + urlAbs1 + "]");
            URL urlRelToClass1 = targetClass.getResource(rw.getNameRelativeTo(targetClass));
            System.out.println("[method: CLASS][resource ID: " +
                rw.getNameRelativeTo(targetClass) + "][URL: " + urlRelToClass1 + "]");

            URL urlAbs2 = classLoader.getResource(rw.getAbsoluteName());
            System.out.println("[method: CONTEXT CLASS LOADER][resource ID: " +
                rw.getAbsoluteName() + "][URL: " + urlAbs2 + "]");
            URL urlRelToRoot2 = classLoader.getResource(rw.getNameRelativeToRoot());
            System.out.println("[method: CONTEXT CLASS LOADER][resource ID: " +
                rw.getNameRelativeToRoot() + "][URL: " + urlRelToRoot2 + "]");

            URL urlAbs3 = ClassLoader.getSystemResource(rw.getAbsoluteName());
            System.out.println("[method: CLASS LOADER][resource ID: " +
                rw.getAbsoluteName() + "][URL: " + urlAbs3 + "]");
            URL urlRelToRoot3 = ClassLoader.getSystemResource(rw.getNameRelativeToRoot());
            System.out.println("[method: CLASS LOADER][resource ID: " +
                rw.getNameRelativeToRoot() + "][URL: " + urlRelToRoot3 + "]");
        }
    }

    /**
     * @param args not used
     */
    public static void main(String[] args)
    {
        ResourceWrapper [] resources =
        {
            new ResourceWrapper("root.txt", "/"),
            new ResourceWrapper("same.txt", "/com/littletutorials/res"),
            new ResourceWrapper("parent.txt", "/com/littletutorials"),
            new ResourceWrapper("sub.txt", "/com/littletutorials/res/subpk"),
            new ResourceWrapper("other.txt", "/com/littletutorials/otherpk")
        };

        ResourceLocator rl = new ResourceLocator();
        rl.locate(resources);
    }
}

Running this program will produce this output on a Linux system:

*** Resource: root.txt
[method: CLASS][resource ID: /root.txt][URL: file:/lt/classes/root.txt]
[method: CLASS][resource ID: ../../../root.txt][URL: file:/lt/classes/root.txt]
[method: CONTEXT CLASS LOADER][resource ID: /root.txt][URL: null]
[method: CONTEXT CLASS LOADER][resource ID: root.txt][URL: file:/lt/classes/root.txt]
[method: CLASS LOADER][resource ID: /root.txt][URL: null]
[method: CLASS LOADER][resource ID: root.txt][URL: file:/lt/classes/root.txt]
*** Resource: same.txt
[method: CLASS][resource ID: /com/littletutorials/res/same.txt][URL: file:/lt/classes/com/littletutorials/res/same.txt]
[method: CLASS][resource ID: same.txt][URL: file:/lt/classes/com/littletutorials/res/same.txt]
[method: CONTEXT CLASS LOADER][resource ID: /com/littletutorials/res/same.txt][URL: null]
[method: CONTEXT CLASS LOADER][resource ID: com/littletutorials/res/same.txt][URL: file:/lt/classes/com/littletutorials/res/same.txt]
[method: CLASS LOADER][resource ID: /com/littletutorials/res/same.txt][URL: null]
[method: CLASS LOADER][resource ID: com/littletutorials/res/same.txt][URL: file:/lt/classes/com/littletutorials/res/same.txt]
*** Resource: parent.txt
[method: CLASS][resource ID: /com/littletutorials/parent.txt][URL: file:/lt/classes/com/littletutorials/parent.txt]
[method: CLASS][resource ID: ../parent.txt][URL: file:/lt/classes/com/littletutorials/parent.txt]
[method: CONTEXT CLASS LOADER][resource ID: /com/littletutorials/parent.txt][URL: null]
[method: CONTEXT CLASS LOADER][resource ID: com/littletutorials/parent.txt][URL: file:/lt/classes/com/littletutorials/parent.txt]
[method: CLASS LOADER][resource ID: /com/littletutorials/parent.txt][URL: null]
[method: CLASS LOADER][resource ID: com/littletutorials/parent.txt][URL: file:/lt/classes/com/littletutorials/parent.txt]
*** Resource: sub.txt
[method: CLASS][resource ID: /com/littletutorials/res/subpk/sub.txt][URL: file:/lt/classes/com/littletutorials/res/subpk/sub.txt]
[method: CLASS][resource ID: subpk/sub.txt][URL: file:/lt/classes/com/littletutorials/res/subpk/sub.txt]
[method: CONTEXT CLASS LOADER][resource ID: /com/littletutorials/res/subpk/sub.txt][URL: null]
[method: CONTEXT CLASS LOADER][resource ID: com/littletutorials/res/subpk/sub.txt][URL: file:/lt/classes/com/littletutorials/res/subpk/sub.txt]
[method: CLASS LOADER][resource ID: /com/littletutorials/res/subpk/sub.txt][URL: null]
[method: CLASS LOADER][resource ID: com/littletutorials/res/subpk/sub.txt][URL: file:/lt/classes/com/littletutorials/res/subpk/sub.txt]
*** Resource: other.txt
[method: CLASS][resource ID: /com/littletutorials/otherpk/other.txt][URL: file:/lt/classes/com/littletutorials/otherpk/other.txt]
[method: CLASS][resource ID: ../otherpk/other.txt][URL: file:/lt/classes/com/littletutorials/otherpk/other.txt]
[method: CONTEXT CLASS LOADER][resource ID: /com/littletutorials/otherpk/other.txt][URL: null]
[method: CONTEXT CLASS LOADER][resource ID: com/littletutorials/otherpk/other.txt][URL: file:/lt/classes/com/littletutorials/otherpk/other.txt]
[method: CLASS LOADER][resource ID: /com/littletutorials/otherpk/other.txt][URL: null]
[method: CLASS LOADER][resource ID: com/littletutorials/otherpk/other.txt][URL: file:/lt/classes/com/littletutorials/otherpk/other.txt]

As you can see the ClassLoader API cannot find resources specified with absolute paths.

Running the same program on Windows produces the same results. On the other hand when I change the separator character from ‘/’ to ‘\\’ the output looks like this

*** Resource: root.txt
[method: CLASS][resource ID: \root.txt][URL: null]
[method: CLASS][resource ID: ..\..\..\root.txt][URL: file:/C:/lt/classes/com/littletutorials/res/..%5c..%5c..%5croot.txt]
[method: CONTEXT CLASS LOADER][resource ID: \root.txt][URL: file:/C:/lt/classes/%5croot.txt]
[method: CONTEXT CLASS LOADER][resource ID: root.txt][URL: file:/C:/lt/classes/root.txt]
[method: CLASS LOADER][resource ID: \root.txt][URL: file:/C:/lt/classes/%5croot.txt]
[method: CLASS LOADER][resource ID: root.txt][URL: file:/C:/lt/classes/root.txt]
*** Resource: same.txt
[method: CLASS][resource ID: \com\littletutorials\res\same.txt][URL: null]
[method: CLASS][resource ID: same.txt][URL: file:/C:/lt/classes/com/littletutorials/res/same.txt]
[method: CONTEXT CLASS LOADER][resource ID: \com\littletutorials\res\same.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cres%5csame.txt]
[method: CONTEXT CLASS LOADER][resource ID: com\littletutorials\res\same.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cres%5csame.txt]
[method: CLASS LOADER][resource ID: \com\littletutorials\res\same.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cres%5csame.txt]
[method: CLASS LOADER][resource ID: com\littletutorials\res\same.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cres%5csame.txt]
*** Resource: parent.txt
[method: CLASS][resource ID: \com\littletutorials\parent.txt][URL: null]
[method: CLASS][resource ID: ..\parent.txt][URL: file:/C:/lt/classes/com/littletutorials/res/..%5cparent.txt]
[method: CONTEXT CLASS LOADER][resource ID: \com\littletutorials\parent.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cparent.txt]
[method: CONTEXT CLASS LOADER][resource ID: com\littletutorials\parent.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cparent.txt]
[method: CLASS LOADER][resource ID: \com\littletutorials\parent.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cparent.txt]
[method: CLASS LOADER][resource ID: com\littletutorials\parent.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cparent.txt]
*** Resource: sub.txt
[method: CLASS][resource ID: \com\littletutorials\res\subpk\sub.txt][URL: null]
[method: CLASS][resource ID: subpk\sub.txt][URL: file:/C:/lt/classes/com/littletutorials/res/subpk%5csub.txt]
[method: CONTEXT CLASS LOADER][resource ID: \com\littletutorials\res\subpk\sub.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cres%5csubpk%5csub.txt]
[method: CONTEXT CLASS LOADER][resource ID: com\littletutorials\res\subpk\sub.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cres%5csubpk%5csub.txt]
[method: CLASS LOADER][resource ID: \com\littletutorials\res\subpk\sub.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cres%5csubpk%5csub.txt]
[method: CLASS LOADER][resource ID: com\littletutorials\res\subpk\sub.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cres%5csubpk%5csub.txt]
*** Resource: other.txt
[method: CLASS][resource ID: \com\littletutorials\otherpk\other.txt][URL: null]
[method: CLASS][resource ID: ..\otherpk\other.txt][URL: file:/C:/lt/classes/com/littletutorials/res/..%5cotherpk%5cother.txt]
[method: CONTEXT CLASS LOADER][resource ID: \com\littletutorials\otherpk\other.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cotherpk%5cother.txt]
[method: CONTEXT CLASS LOADER][resource ID: com\littletutorials\otherpk\other.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cotherpk%5cother.txt]
[method: CLASS LOADER][resource ID: \com\littletutorials\otherpk\other.txt][URL: file:/C:/lt/classes/%5ccom%5clittletutorials%5cotherpk%5cother.txt]
[method: CLASS LOADER][resource ID: com\littletutorials\otherpk\other.txt][URL: file:/C:/lt/classes/com%5clittletutorials%5cotherpk%5cother.txt]

In this case the Class API doesn’t recognize the absolute path anymore. The ClassLoader API decides it can work with absolute paths and somehow it finds the resources. All this as I said looks more like a side effect or a bug. And finally we can see why ‘\\’ is not recommended as a separator looking at the URLs – this character is replaced with “%5c” its ASCII representation in hex.

Hopefully spending 10 minutes running and understanding the results of this test will help when resources need to be embedded in JAR files and located at runtime.