193

Is there an API to get a classpath resource (e.g. what I'd get from Class.getResource(String)) as a java.nio.file.Path? Ideally, I'd like to use the fancy new Path APIs with classpath resources.

JuanMoreno
  • 2,498
  • 1
  • 25
  • 34
Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • 3
    Well, taking the long path (pun intended), you have `Paths.get(URI)`, then ´URL.toURI()`, and last `getResource()` which returns a `URL`. You might be able to chain those together. Haven´t tried though. – NilsH Mar 29 '13 at 23:50

8 Answers8

259

This one works for me:

return Path.of(ClassLoader.getSystemResource(resourceName).toURI());
Adam Burley
  • 5,551
  • 4
  • 51
  • 72
keyoxy
  • 4,423
  • 2
  • 21
  • 18
  • 10
    @VGR if resources in .jar file could try this ` Resource resource = new ClassPathResource("usage.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));` please see http://stackoverflow.com/questions/25869428/classpath-resource-not-found-when-running-as-jar – zhuguowei Jan 02 '16 at 06:43
  • 10
    @zhuguowei that's a Spring-specific approach. It doesn't work at all when Spring isn't being used. – Ryan J. McDonough Jun 01 '18 at 21:04
  • 3
    If your application doesn't rely on the system classloader, it should be `Thread.currentThread().getContextClassLoader().getResource(resourceName).toURI()` – ThrawnCA Sep 13 '19 at 02:42
  • 1
    This solution requires Java 11 isn't it? – happy_marmoset Apr 20 '22 at 12:48
31

Guessing that what you want to do, is call Files.lines(...) on a resource that comes from the classpath - possibly from within a jar.

Since Oracle convoluted the notion of when a Path is a Path by not making getResource return a usable path if it resides in a jar file, what you need to do is something like this:

Stream<String> stream = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream("/filename.txt"))).lines();
user2163960
  • 1,871
  • 19
  • 22
  • 1
    whether the preceding "/" is needed in your case I don't know, but in my case `class.getResource` requires a slash but `getSystemResourceAsStream` can't find the file when prefixed with a slash. – Adam Jul 12 '16 at 13:18
15

The most general solution is as follows:

interface IOConsumer<T> {
    void accept(T t) throws IOException;
}
public static void processRessource(URI uri, IOConsumer<Path> action) throws IOException{
    try {
        Path p=Paths.get(uri);
        action.accept(p);
    }
    catch(FileSystemNotFoundException ex) {
        try(FileSystem fs = FileSystems.newFileSystem(
                uri, Collections.<String,Object>emptyMap())) {
            Path p = fs.provider().getPath(uri);
            action.accept(p);
        }
    }
}

The main obstacle is to deal with the two possibilities, either, having an existing filesystem that we should use, but not close (like with file URIs or the Java 9’s module storage), or having to open and thus safely close the filesystem ourselves (like zip/jar files).

Therefore, the solution above encapsulates the actual action in an interface, handles both cases, safely closing afterwards in the second case, and works from Java 7 to Java 18. It probes whether there is already an open filesystem before opening a new one, so it also works in the case that another component of your application has already opened a filesystem for the same zip/jar file.

It can be used in all Java versions named above, e.g. to list the contents of a package (java.lang in the example) as Paths, like this:

processRessource(Object.class.getResource("Object.class").toURI(),new IOConsumer<Path>(){
    public void accept(Path path) throws IOException {
        try(DirectoryStream<Path> ds = Files.newDirectoryStream(path.getParent())) {
            for(Path p: ds)
                System.out.println(p);
        }
    }
});

With Java 8 or newer, you can use lambda expressions or method references to represent the actual action, e.g.

processRessource(Object.class.getResource("Object.class").toURI(), path -> {
    try(Stream<Path> stream = Files.list(path.getParent())) {
        stream.forEach(System.out::println);
    }
});

to do the same.


The final release of Java 9’s module system has broken the above code example. The Java versions from 9 to 12 inconsistently return the path /java.base/java/lang/Object.class for Paths.get(Object.class.getResource("Object.class")) whereas it should be /modules/java.base/java/lang/Object.class. This can be fixed by prepending the missing /modules/ when the parent path is reported as non-existent:

processRessource(Object.class.getResource("Object.class").toURI(), path -> {
    Path p = path.getParent();
    if(!Files.exists(p))
        p = p.resolve("/modules").resolve(p.getRoot().relativize(p));
    try(Stream<Path> stream = Files.list(p)) {
        stream.forEach(System.out::println);
    }
});

Then, it will again work with all versions and storage methods. Starting with JDK 13, this work-around is not necessary anymore.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 2
    This solution works great! I can confirm that this works with all resources (files, directories) in both directory classpaths and jar classpaths. This is definitely how copying lots of resources should be done in Java 7+. – Mitchell Skaggs Apr 14 '20 at 07:09
11

It turns out you can do this, with the help of the built-in Zip File System provider. However, passing a resource URI directly to Paths.get won't work; instead, one must first create a zip filesystem for the jar URI without the entry name, then refer to the entry in that filesystem:

static Path resourceToPath(URL resource)
throws IOException,
       URISyntaxException {

    Objects.requireNonNull(resource, "Resource URL cannot be null");
    URI uri = resource.toURI();

    String scheme = uri.getScheme();
    if (scheme.equals("file")) {
        return Paths.get(uri);
    }

    if (!scheme.equals("jar")) {
        throw new IllegalArgumentException("Cannot convert to Path: " + uri);
    }

    String s = uri.toString();
    int separator = s.indexOf("!/");
    String entryName = s.substring(separator + 2);
    URI fileURI = URI.create(s.substring(0, separator));

    FileSystem fs = FileSystems.newFileSystem(fileURI,
        Collections.<String, Object>emptyMap());
    return fs.getPath(entryName);
}

Update:

It’s been rightly pointed out that the above code contains a resource leak, since the code opens a new FileSystem object but never closes it. The best approach is to pass a Consumer-like worker object, much like how Holger’s answer does it. Open the ZipFS FileSystem just long enough for the worker to do whatever it needs to do with the Path (as long as the worker doesn’t try to store the Path object for later use), then close the FileSystem.

VGR
  • 40,506
  • 4
  • 48
  • 63
  • 11
    Be careful to the newly created fs. A second call using the same jar will throw an exception complaining about an already existing filesystem. It will be better to do try(FileSystem fs=...){return fs.getPath(entryName);} or if you want to have this cached do more advanced handling. In the current form is risky. – raisercostin Jul 02 '13 at 09:02
  • 3
    Besides the issue of the potentially non-closed new filesystem, the assumptions about the relationship between schemes and the necessity of opening a new filesystem and the puzzling with the URI contents limit the usefulness of the solution. I’ve set up a [new answer](http://stackoverflow.com/a/36021165/2711488) which shows a general approach which simplifies the operation and handles new schemes like the new Java 9 class storage at the same time. It also works when someone else within the application has already opened the filesystem (or the method is called twice for the same jar)… – Holger Mar 15 '16 at 20:18
  • Depending on the usage of this solution, the non closed `newFileSystem` can lead to multiple resources hanging around open for ever. Although @raisercostin addendum avoids the error when trying to create an already created file system, if you try to use the returned `Path` you will get a `ClosedFileSystemException`. @Holger response works well for me. – José Andias Apr 18 '17 at 13:54
  • I wouldn't close the `FileSystem`. If you load a resource from a Jar, and you then create the required `FileSystem` - the `FileSystem` will also allow you to load other resources from the same Jar. Also, once you created the new `FileSystem` you can just try to load the resource again using `Paths.get(Path)` and the implementation will automatically use the new `FileSystem`. – dutoitns Jan 07 '20 at 09:34
  • Ie you don't have to use the `#getPath(String)` method on the `FileSystem` object. – dutoitns Jan 07 '20 at 09:35
  • The `Path` object is also very unusable if you close the `FileSystem`, so depending on how your code is structured it can become very ugly. For example - you can't have a utility method returning a `Path` and close the `FileSystem` inside the utility method, because using the `Path` object will then throw errors due to the underlying `FileSystem` being closed. – dutoitns Jan 07 '20 at 09:37
  • I'm not sure how threadsafe `FileSystems#newFileSystem` is, so you might also want to consider some synchronization so that multiple threads don't try and create `FileSystems` at the same time. – dutoitns Jan 07 '20 at 09:38
  • Please also note, that weirdly `Files#isRegularFile(Path)` does not return `true` for resources within Jar files.. – dutoitns Jan 07 '20 at 09:38
5

I wrote a small helper method to read Paths from your class resources. It is quite handy to use as it only needs a reference of the class you have stored your resources as well as the name of the resource itself.

public static Path getResourcePath(Class<?> resourceClass, String resourceName) throws URISyntaxException {
    URL url = resourceClass.getResource(resourceName);
    return Paths.get(url.toURI());
}  
Michael Stauffer
  • 298
  • 1
  • 4
  • 15
2

You can not create URI from resources inside of the jar file. You can simply write it to the temp file and then use it (java8):

Path path = File.createTempFile("some", "address").toPath();
Files.copy(ClassLoader.getSystemResourceAsStream("/path/to/resource"), path, StandardCopyOption.REPLACE_EXISTING);
user1079877
  • 9,008
  • 4
  • 43
  • 54
2

Read a File from resources folder using NIO, in java8

public static String read(String fileName) {

        Path path;
        StringBuilder data = new StringBuilder();
        Stream<String> lines = null;
        try {
            path = Paths.get(Thread.currentThread().getContextClassLoader().getResource(fileName).toURI());
            lines = Files.lines(path);
        } catch (URISyntaxException | IOException e) {
            logger.error("Error in reading propertied file " + e);
            throw new RuntimeException(e);
        }

        lines.forEach(line -> data.append(line));
        lines.close();
        return data.toString();
    }
Ahmed Ashour
  • 5,179
  • 10
  • 35
  • 56
Real Coder
  • 21
  • 4
0

You need to define the Filesystem to read resource from jar file as mentioned in https://docs.oracle.com/javase/8/docs/technotes/guides/io/fsp/zipfilesystemprovider.html. I success to read resource from jar file with below codes:

Map<String, Object> env = new HashMap<>();
try (FileSystem fs = FileSystems.newFileSystem(uri, env)) {

        Path path = fs.getPath("/path/myResource");

        try (Stream<String> lines = Files.lines(path)) {
            ....
        }
    }