lubik
lubik

Reputation: 79

java - How to list directories in resources in jar

Let's have a maven project with resources in following structure:

src/main/resources

Although I found many answers how to list file resources, I am stuck to find a way how to list all direct subdirectories of a directory (or path) in resources, that would work when app is run both from IDE (IntelliJ) and jar.

The goal is to get names of subdirectories of directory "dir1": subdir1, subdir2

I tried

Thread.currentThread().getContextClassLoader().getResourceAsStream("dir1");

and to use returned InputStream (containing names of subdirectories), but does not work from jar. Works from IntelliJ.

I also tried to use spring framework but

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("dir1/*");

does not work as well from jar - resources array is empty. Works from IntelliJ.

I tried to modify to

Resource[] resources = resolver.getResources("dir1/*/");

and it works from jar, but does not work from IntelliJ.

When I make it work from jar, I break the other way and vice versa. My last idea is to use

Resource[] resources = resolver.getResources("dir1/**");

to get all resources under the directory and then get only ones that are required. Is there a better (preferably not hacky) way?

Upvotes: 0

Views: 6267

Answers (4)

jekho
jekho

Reputation: 31

Thanks Lubik for your first post, it works very well within Springboot 2.1., in IDE or jar.

I needed to copy and read some resources files from jar (or IDE in dev), returning Resource[] instead, next copy them where you want on the disk thanks to resource.getInputStream():

for (Resource resource: mapResources) {
 Path destPath =
 someFolder.resolve(resource.getFilename());

 Files.copy(resource.getInputStream(), destPath);

}

Upvotes: 0

lubik
lubik

Reputation: 79

I finally ended up with this (using spring):

public class ResourceScanner {
    private PathMatchingResourcePatternResolver resourcePatternResolver;

    public ResourceScanner() {
        this.resourcePatternResolver = new PathMatchingResourcePatternResolver();
    }

    public Resource getResource(String path) {
        path = path.replace("\\", "/");
        return resourcePatternResolver.getResource(path);
    }

    public Resource[] getResources(String path) throws IOException {
        path = path.replace("\\", "/");
        return resourcePatternResolver.getResources(path);
    }

    public Resource[] getResourcesIn(String path) throws IOException {
        // Get root dir URI
        Resource root = getResource(path);
        String rootUri =  root.getURI().toString();

        // Search all resources under the root dir
        path = (path.endsWith("/")) ? path + "**" : path + "/**";

        // Filter only direct children
        return Arrays.stream(getResources(path)).filter(resource -> {
            try {
                String uri = resource.getURI().toString();

                boolean isChild = uri.length() > rootUri.length() && !uri.equals(rootUri + "/");
                if (isChild) {
                    boolean isDirInside = uri.indexOf("/", rootUri.length() + 1) == uri.length() - 1;
                    boolean isFileInside = uri.indexOf("/", rootUri.length() + 1) == -1;
                    return isDirInside || isFileInside;
                }

                return false;
            } catch (IOException e) {
                return false;
            }
        }).toArray(Resource[]::new);
    }

    public String[] getResourcesNamesIn(String path) throws IOException {
        // Get root dir URI
        Resource root = getResource(path);
        String rootUri = URLDecoder.decode(root.getURI().toString().endsWith("/") ? root.getURI().toString() : root.getURI().toString() + "/", "UTF-8");

        // Get direct children names
        return Arrays.stream(getResourcesIn(path)).map(resource -> {
            try {
                String uri = URLDecoder.decode(resource.getURI().toString(), "UTF-8");

                boolean isFile = uri.indexOf("/", rootUri.length()) == -1;
                if (isFile) {
                    return uri.substring(rootUri.length());
                } else {
                    return uri.substring(rootUri.length(), uri.indexOf("/", rootUri.length() + 1));
                }
            } catch (IOException e) {
                return null;
            }
        }).toArray(String[]::new);
    }
}

Upvotes: 1

lubik
lubik

Reputation: 79

Seems like the following works:

public String[] getResourcesNames(String path) {
    try {
        URL url = getClassLoader().getResource(path);
        if (url == null) {
            return null;
        }

        URI uri = url.toURI();
        if (uri.getScheme().equals("jar")) { // Run from jar
            try (FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap())){
                Path resourcePath = fileSystem.getPath(path);

                // Get all contents of a resource (skip resource itself), if entry is a directory remove trailing /
                List<String> resourcesNames =
                        Files.walk(resourcePath, 1)
                                .skip(1)
                                .map(p -> {
                                    String name = p.getFileName().toString();
                                    if (name.endsWith("/")) {
                                        name = name.substring(0, name.length() - 1);
                                    }
                                    return name;
                                })
                                .sorted()
                                .collect(Collectors.toList());

                return resourcesNames.toArray(new String[resourcesNames.size()]);
            }
        } else { // Run from IDE
            File resource = new File(uri);
            return resource.list();
        }
    } catch (IOException | URISyntaxException e) {
        return null;
    }
}

private ClassLoader getClassLoader() {
    return Thread.currentThread().getContextClassLoader();
}

Upvotes: 1

Jose da Silva Gomes
Jose da Silva Gomes

Reputation: 3974

The problem with a jar is that there are not folder as such, there are entries, and folders are just entries with a trailing slash in it's name. This is probably why dir1/*/ return the folders in jar but not from de IDE where the folder name does not end with /. For a similar reason dir1/* work on the IDE but not in the jar since again, the folder name ends with the slash in the jar.

Spring handles the resources based on files, if you try to get an empty folder as resource it will fail, and this empty folder normally is not added to the jar. Also looking for folders is not that common, since the content is in files, and the only useful information is what files it contains.

The easiest way you can achieve this is to use dir1/** pattern, since this will list all the files, and folders with files under the dir1 directory.

'Detecting' if the program is being executed from a jar file, to choose a different strategy to list the folders can also be done, but that solution is even more 'hacky'.

Here an example using first option, pattern dir1/** (just in case needed):

String folderName = "dir1";
String folderPath = String.format("%s/**", folderName);

Resource[] resources = resolver.getResources(folderPath);
Map<String, Resource> resourceByURI = Arrays.stream(resources)
        .collect(Collectors.toMap(resource -> {
            try {
                return resource.getURI().toString();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }, Function.identity()));

The resourceByURI is a map containing the Resource, identified by its URI.

Resource folder = resolver.getResource(folderName);
int folderLength = folder.getURI().toString().length();
Map<String, String> subdirectoryByName = resourceByURI.keySet().stream()
        .filter(name -> name.length() > folderLength
                && name.indexOf("/", folderLength + 1) == name.length() - 1)
        .collect(Collectors.toMap(Function.identity(), name -> name.substring(folderLength,
                name.indexOf("/", folderLength + 1))));

Then you can get the URI of the folder, to calculate the offset of the path (similar to what spring does), then check that the name is longer that the current folder (to discard the same folder in the jar case) and that the name constains a single / in the end. Finally collect to a Map again identified by the URI, containing the name of the folders. The collection of folder names is subdirectoryByName.values().

Upvotes: 0

Related Questions