jchamberlain
jchamberlain

Reputation: 1068

Find the relative path of a FileSystemResource (Spring Boot)

Summary

In Spring, I can access a resource by specifying a path relative to my src/main/resources/ directory. For example, if I ask for /public/index.html, I'll get a FileSystemResource representing /Users/.../src/main/resources/public/index.html. However, I don't see a way to go the opposite direction.

Given a FileSystemResource, is there a way to find its path relative to src/main/resouces/?

Example

I'm using PathMatchingResourcePatternResolver to get a list of file resources in my app. The resources I need are located in my app's src/main/resources/public/ folder.

ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
for (Resource r : resolver.getResources("/public/**")) {
    // r.getURI().getString() gives me the absolute path.
}

I can easily get the absolute path, but I'd like a way to get the portion starting at /public/, since that's how Spring found it to begin with.

Upvotes: 2

Views: 4099

Answers (1)

EndlosSchleife
EndlosSchleife

Reputation: 597

I was about to ask how to do just that, in a less hacky way than my solution (plus also dealing with ClassPathResources which can occur in my case). So here is my answer and question:

A resource returned by ResourcePatternResolver.getResources can be a FileSystemResource or a ClassPathResource, depending on whether the glob pattern contains a wildcard or not. (In my case, the glob pattern is given as a parameter and any pattern should be supported.) So, no getFile; the only path information available for both seems to be getDescription.

To determine directory depth of the root, I put a reference file "input-files.txt" there. This is because I think that the root directory resource "classpath:/input-files/" need not exist in case of a jar-based classpath. With the root depth, I can remove that many directories from the resource description.

Does anyone know of a simpler or less hacky solution?

Like mine, it should work with both jar and directory based classpaths.

import java.io.IOException;
import java.nio.file.Path;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;

public class TestClasspathRelativePath implements CommandLineRunner {

    private static final String RESOURCE_ROOT = "classpath:/input-files/";
    private static final String RESOURCE_ROOT_MARKER_FILE = "input-files.txt";
    private static final String FILE_SEPARATOR_PATTERN = "[/\\\\]+";

    @Inject
    private ApplicationContext appContext;

    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .sources(TestClasspathRelativePath.class)
                .web(false)
                .build()
                .run("**/*");
    }

    @Override
    public void run(String... args) throws Exception {
        for (String templateFilenamePattern : args) {
            for (Resource template : appContext.getResources(RESOURCE_ROOT + templateFilenamePattern)) {
                createInputFile(template);
            }
        }
    }

    private void createInputFile(Resource template) throws IOException {

        // A resource whose immediate parent is the root input
        // files directory, without wildcard lookup. (As of Spring 4.3.14, with
        // a directory classpath entry, this is a ClassPathResource.)
        Resource topLevelFile = appContext.getResource(RESOURCE_ROOT + RESOURCE_ROOT_MARKER_FILE);

        // A resource whose immediate parent is the root input
        // files directory, with wildcard lookup. (As of Spring 4.3.14, with a
        // directory classpath entry, this is a FileSystemResource.)
        Resource wildcardTopLevelFile =
                // match with min description length is
                // RESOURCE_ROOT/input-files.txt (probably sole match anyway)
                Stream.of(appContext.getResources(RESOURCE_ROOT + "**/" + RESOURCE_ROOT_MARKER_FILE)).min(
                        (r1, r2) -> Integer.valueOf(r1.getDescription().length()).compareTo(Integer.valueOf(r2.getDescription().length())))
                        .orElseThrow(() -> new IllegalStateException("resource " + RESOURCE_ROOT + "/" + RESOURCE_ROOT_MARKER_FILE + " not found"));

        // In the real program, both top level resources are computed only once and on-demand.

        String targetFilename = "<target-dir>/input/" + relativize(template, topLevelFile, wildcardTopLevelFile);
        System.out.println(targetFilename);
        // ... read template, process, write results to targetFilename
    }

    /**
     * Replacement for {@link Path#relativize(Path)} which should also work with
     * non-file resources.
     */
    private static String relativize(Resource child, Resource... topLevelFiles) {
        // find the top-level file for child's type
        Resource referenceFile = Stream.of(topLevelFiles)
                .filter(f -> f.getClass().isInstance(child))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("don't know how to relativize " + child));
        int rootLevel = descriptionUpToName(referenceFile).split(FILE_SEPARATOR_PATTERN, -1).length - 1;
        return Stream.of(descriptionUpToName(child).split(FILE_SEPARATOR_PATTERN, -1)).skip(rootLevel)
                .collect(Collectors.joining("/"));
    }

    /**
     * Hack to strip the suffix after the name from a resource description. The
     * prefix need not be stripped, because
     * {@link #relativize(Resource, Resource...)} uses a safer way to get rid of
     * that.
     *
     * @return e.g. "file [C:\foo\classpath\input-files\a\b.txt" if the
     *         resource's description is "file
     *         [C:\foo\classpath\input-files\a\b.txt]"
     */
    private static String descriptionUpToName(Resource resource) {
        String path = resource.getDescription();
        int i = path.lastIndexOf(resource.getFilename());
        return path.substring(0, i + resource.getFilename().length());
    }
}

Upvotes: 1

Related Questions