java.nio.file.Path for a classpath resource
Asked Answered
G

8

195

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.

Gamez answered 29/3, 2013 at 23:41 Comment(1)
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.Anew
A
262

This one works for me:

return Path.of(ClassLoader.getSystemResource(resourceName).toURI());
Aporia answered 14/5, 2014 at 11:59 Comment(5)
@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 #25869928Prehistory
@Prehistory that's a Spring-specific approach. It doesn't work at all when Spring isn't being used.Theomachy
If your application doesn't rely on the system classloader, it should be Thread.currentThread().getContextClassLoader().getResource(resourceName).toURI()Waneta
This solution requires Java 11 isn't it?Conversational
This only works in development mode. When the application is deployed, the resources end up in the JAR, and this code crashes with java.nio.file.FileSystemNotFoundException. See stackoverflow.com/questions/22605666Hamulus
P
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();
Pleven answered 15/9, 2015 at 6:57 Comment(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.Adversity
B
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.

Behring answered 15/3, 2016 at 20:10 Comment(1)
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+.Honeyhoneybee
L
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.

Lated answered 30/3, 2013 at 11:26 Comment(8)
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.Lumberman
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 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)…Behring
Depending on the usage of this solution, the non closed newFileSystem can lead to multiple resources hanging around open for ever. Although @Lumberman 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. @Behring response works well for me.Footwear
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.Bathe
Ie you don't have to use the #getPath(String) method on the FileSystem object.Bathe
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.Bathe
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.Bathe
Please also note, that weirdly Files#isRegularFile(Path) does not return true for resources within Jar files..Bathe
A
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());
}  
Aricaarick answered 3/9, 2014 at 19:42 Comment(0)
S
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);
Strapped answered 5/10, 2016 at 18:54 Comment(0)
G
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();
    }
Groggy answered 16/3, 2018 at 6:26 Comment(0)
R
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)) {
            ....
        }
    }
Renowned answered 2/12, 2015 at 10:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.