Add working directory support for ApplicationResourceLoader

See gh-41137
This commit is contained in:
Moritz Halbritter
2025-02-11 10:08:25 +01:00
parent d8b470a511
commit 528b7e9ad9
2 changed files with 182 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,11 @@
package org.springframework.boot.io;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.List;
import org.springframework.core.io.ClassPathResource;
@@ -40,6 +45,7 @@ import org.springframework.util.StringUtils;
* {@code DefaultResourceLoader}, which resolves unqualified paths to classpath resources.
*
* @author Scott Frederick
* @author Moritz Halbritter
* @author Phillip Webb
* @since 3.3.0
*/
@@ -109,7 +115,23 @@ public class ApplicationResourceLoader extends DefaultResourceLoader {
* @since 3.4.0
*/
public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader) {
return get(ApplicationFileSystemResourceLoader.get(classLoader), springFactoriesLoader);
return get(classLoader, springFactoriesLoader, null);
}
/**
* Return a {@link ResourceLoader} supporting additional {@link ProtocolResolver
* ProtocolResolvers} registered in {@code spring.factories}.
* @param classLoader the class loader to use or {@code null} to use the default class
* loader
* @param springFactoriesLoader the {@link SpringFactoriesLoader} used to load
* {@link ProtocolResolver ProtocolResolvers}
* @param workingDirectory the working directory
* @return a {@link ResourceLoader} instance
* @since 3.5.0
*/
public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader,
Path workingDirectory) {
return get(ApplicationFileSystemResourceLoader.get(classLoader, workingDirectory), springFactoriesLoader);
}
/**
@@ -170,10 +192,41 @@ public class ApplicationResourceLoader extends DefaultResourceLoader {
*/
private static final class ApplicationFileSystemResourceLoader extends DefaultResourceLoader {
private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null);
private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null, null);
private ApplicationFileSystemResourceLoader(ClassLoader classLoader) {
private final Path workingDirectory;
private ApplicationFileSystemResourceLoader(ClassLoader classLoader, Path workingDirectory) {
super(classLoader);
this.workingDirectory = workingDirectory;
}
@Override
public Resource getResource(String location) {
Resource resource = super.getResource(location);
if (this.workingDirectory == null) {
return resource;
}
if (!resource.isFile()) {
return resource;
}
return resolveFile(resource);
}
private Resource resolveFile(Resource resource) {
try {
File file = resource.getFile();
if (file.isAbsolute()) {
return resource;
}
return new ApplicationResource(new File(this.workingDirectory.toFile(), file.getPath()).getPath());
}
catch (FileNotFoundException ex) {
return resource;
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
@Override
@@ -181,8 +234,12 @@ public class ApplicationResourceLoader extends DefaultResourceLoader {
return new ApplicationResource(path);
}
static ResourceLoader get(ClassLoader classLoader) {
return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader)
static ResourceLoader get(ClassLoader classLoader, Path workingDirectory) {
if (classLoader == null && workingDirectory != null) {
throw new IllegalArgumentException(
"It's not possible to use null as 'classLoader' but specify a 'workingDirectory'");
}
return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader, workingDirectory)
: ApplicationFileSystemResourceLoader.shared;
}
@@ -218,7 +275,7 @@ public class ApplicationResourceLoader extends DefaultResourceLoader {
private final boolean preferFileResolution;
private Class<?> servletContextResourceClass;
private final Class<?> servletContextResourceClass;
ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
boolean preferFileResolution) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Enumeration;
import java.util.function.UnaryOperator;
@@ -45,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link ApplicationResourceLoader}.
*
* @author Phillip Webb
* @author Moritz Halbritter
*/
class ApplicationResourceLoaderTests {
@@ -61,6 +63,121 @@ class ApplicationResourceLoaderTests {
assertThat(contentAsString(resource)).isEqualTo("test");
}
@Test
void shouldLoadAbsolutePath() throws IOException {
Resource resource = ApplicationResourceLoader.get().getResource("/root/file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/root").hasName("file.txt");
}
@Test
void shouldLoadAbsolutePathWithWorkingDirectory() throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("/root/file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/root").hasName("file.txt");
}
@Test
void shouldLoadRelativeFilename() throws IOException {
Resource resource = ApplicationResourceLoader.get().getResource("file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasNoParent().hasName("file.txt");
}
@Test
void shouldLoadRelativeFilenameWithWorkingDirectory() throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/working-directory").hasName("file.txt");
}
@Test
void shouldLoadRelativePathWithWorkingDirectory() throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("a/file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/working-directory/a").hasName("file.txt");
}
@Test
void shouldLoadClasspathLocations() {
Resource resource = ApplicationResourceLoader.get().getResource("classpath:a-file");
assertThat(resource.exists()).isTrue();
}
@Test
void shouldLoadNonExistentClasspathLocations() {
Resource resource = ApplicationResourceLoader.get().getResource("classpath:doesnt-exist");
assertThat(resource.exists()).isFalse();
}
@Test
void shouldLoadClasspathLocationsWithWorkingDirectory() {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("classpath:a-file");
assertThat(resource.exists()).isTrue();
}
@Test
void shouldLoadNonExistentClasspathLocationsWithWorkingDirectory() {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("classpath:doesnt-exist");
assertThat(resource.exists()).isFalse();
}
@Test
void shouldLoadRelativeFileUris() throws IOException {
Resource resource = ApplicationResourceLoader.get().getResource("file:file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasNoParent().hasName("file.txt");
}
@Test
void shouldLoadAbsoluteFileUris() throws IOException {
Resource resource = ApplicationResourceLoader.get().getResource("file:/file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/").hasName("file.txt");
}
@Test
void shouldLoadRelativeFileUrisWithWorkingDirectory() throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("file:file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/working-directory").hasName("file.txt");
}
@Test
void shouldLoadAbsoluteFileUrisWithWorkingDirectory() throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
Resource resource = ApplicationResourceLoader
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
Path.of("/working-directory"))
.getResource("file:/file.txt");
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFile()).hasParent("/").hasName("file.txt");
}
@Test
void getWithClassPathIncludesProtocolResolvers() throws IOException {
ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);