Reputation: 1068
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/
?
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
Reputation: 597
I was about to ask how to do just that, in a less hacky way than my solution (plus also dealing with ClassPathResource
s 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