Skip to content

Commit 528b7e9

Browse files
committed
Add working directory support for ApplicationResourceLoader
See gh-41137
1 parent d8b470a commit 528b7e9

File tree

2 files changed

+182
-8
lines changed

2 files changed

+182
-8
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,11 @@
1616

1717
package org.springframework.boot.io;
1818

19+
import java.io.File;
20+
import java.io.FileNotFoundException;
21+
import java.io.IOException;
22+
import java.io.UncheckedIOException;
23+
import java.nio.file.Path;
1924
import java.util.List;
2025

2126
import org.springframework.core.io.ClassPathResource;
@@ -40,6 +45,7 @@
4045
* {@code DefaultResourceLoader}, which resolves unqualified paths to classpath resources.
4146
*
4247
* @author Scott Frederick
48+
* @author Moritz Halbritter
4349
* @author Phillip Webb
4450
* @since 3.3.0
4551
*/
@@ -109,7 +115,23 @@ public static ResourceLoader get(ClassLoader classLoader) {
109115
* @since 3.4.0
110116
*/
111117
public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader) {
112-
return get(ApplicationFileSystemResourceLoader.get(classLoader), springFactoriesLoader);
118+
return get(classLoader, springFactoriesLoader, null);
119+
}
120+
121+
/**
122+
* Return a {@link ResourceLoader} supporting additional {@link ProtocolResolver
123+
* ProtocolResolvers} registered in {@code spring.factories}.
124+
* @param classLoader the class loader to use or {@code null} to use the default class
125+
* loader
126+
* @param springFactoriesLoader the {@link SpringFactoriesLoader} used to load
127+
* {@link ProtocolResolver ProtocolResolvers}
128+
* @param workingDirectory the working directory
129+
* @return a {@link ResourceLoader} instance
130+
* @since 3.5.0
131+
*/
132+
public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader,
133+
Path workingDirectory) {
134+
return get(ApplicationFileSystemResourceLoader.get(classLoader, workingDirectory), springFactoriesLoader);
113135
}
114136

115137
/**
@@ -170,19 +192,54 @@ private static ResourceLoader get(ResourceLoader resourceLoader, SpringFactories
170192
*/
171193
private static final class ApplicationFileSystemResourceLoader extends DefaultResourceLoader {
172194

173-
private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null);
195+
private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null, null);
196+
197+
private final Path workingDirectory;
174198

175-
private ApplicationFileSystemResourceLoader(ClassLoader classLoader) {
199+
private ApplicationFileSystemResourceLoader(ClassLoader classLoader, Path workingDirectory) {
176200
super(classLoader);
201+
this.workingDirectory = workingDirectory;
202+
}
203+
204+
@Override
205+
public Resource getResource(String location) {
206+
Resource resource = super.getResource(location);
207+
if (this.workingDirectory == null) {
208+
return resource;
209+
}
210+
if (!resource.isFile()) {
211+
return resource;
212+
}
213+
return resolveFile(resource);
214+
}
215+
216+
private Resource resolveFile(Resource resource) {
217+
try {
218+
File file = resource.getFile();
219+
if (file.isAbsolute()) {
220+
return resource;
221+
}
222+
return new ApplicationResource(new File(this.workingDirectory.toFile(), file.getPath()).getPath());
223+
}
224+
catch (FileNotFoundException ex) {
225+
return resource;
226+
}
227+
catch (IOException ex) {
228+
throw new UncheckedIOException(ex);
229+
}
177230
}
178231

179232
@Override
180233
protected Resource getResourceByPath(String path) {
181234
return new ApplicationResource(path);
182235
}
183236

184-
static ResourceLoader get(ClassLoader classLoader) {
185-
return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader)
237+
static ResourceLoader get(ClassLoader classLoader, Path workingDirectory) {
238+
if (classLoader == null && workingDirectory != null) {
239+
throw new IllegalArgumentException(
240+
"It's not possible to use null as 'classLoader' but specify a 'workingDirectory'");
241+
}
242+
return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader, workingDirectory)
186243
: ApplicationFileSystemResourceLoader.shared;
187244
}
188245

@@ -218,7 +275,7 @@ private static class ProtocolResolvingResourceLoader implements ResourceLoader {
218275

219276
private final boolean preferFileResolution;
220277

221-
private Class<?> servletContextResourceClass;
278+
private final Class<?> servletContextResourceClass;
222279

223280
ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
224281
boolean preferFileResolution) {

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.io.IOException;
2121
import java.net.URL;
2222
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Path;
2324
import java.util.Base64;
2425
import java.util.Enumeration;
2526
import java.util.function.UnaryOperator;
@@ -45,6 +46,7 @@
4546
* Tests for {@link ApplicationResourceLoader}.
4647
*
4748
* @author Phillip Webb
49+
* @author Moritz Halbritter
4850
*/
4951
class ApplicationResourceLoaderTests {
5052

@@ -61,6 +63,121 @@ void getIncludesProtocolResolvers() throws IOException {
6163
assertThat(contentAsString(resource)).isEqualTo("test");
6264
}
6365

66+
@Test
67+
void shouldLoadAbsolutePath() throws IOException {
68+
Resource resource = ApplicationResourceLoader.get().getResource("/root/file.txt");
69+
assertThat(resource.isFile()).isTrue();
70+
assertThat(resource.getFile()).hasParent("/root").hasName("file.txt");
71+
}
72+
73+
@Test
74+
void shouldLoadAbsolutePathWithWorkingDirectory() throws IOException {
75+
ClassLoader classLoader = getClass().getClassLoader();
76+
Resource resource = ApplicationResourceLoader
77+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
78+
Path.of("/working-directory"))
79+
.getResource("/root/file.txt");
80+
assertThat(resource.isFile()).isTrue();
81+
assertThat(resource.getFile()).hasParent("/root").hasName("file.txt");
82+
}
83+
84+
@Test
85+
void shouldLoadRelativeFilename() throws IOException {
86+
Resource resource = ApplicationResourceLoader.get().getResource("file.txt");
87+
assertThat(resource.isFile()).isTrue();
88+
assertThat(resource.getFile()).hasNoParent().hasName("file.txt");
89+
}
90+
91+
@Test
92+
void shouldLoadRelativeFilenameWithWorkingDirectory() throws IOException {
93+
ClassLoader classLoader = getClass().getClassLoader();
94+
Resource resource = ApplicationResourceLoader
95+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
96+
Path.of("/working-directory"))
97+
.getResource("file.txt");
98+
assertThat(resource.isFile()).isTrue();
99+
assertThat(resource.getFile()).hasParent("/working-directory").hasName("file.txt");
100+
}
101+
102+
@Test
103+
void shouldLoadRelativePathWithWorkingDirectory() throws IOException {
104+
ClassLoader classLoader = getClass().getClassLoader();
105+
Resource resource = ApplicationResourceLoader
106+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
107+
Path.of("/working-directory"))
108+
.getResource("a/file.txt");
109+
assertThat(resource.isFile()).isTrue();
110+
assertThat(resource.getFile()).hasParent("/working-directory/a").hasName("file.txt");
111+
}
112+
113+
@Test
114+
void shouldLoadClasspathLocations() {
115+
Resource resource = ApplicationResourceLoader.get().getResource("classpath:a-file");
116+
assertThat(resource.exists()).isTrue();
117+
}
118+
119+
@Test
120+
void shouldLoadNonExistentClasspathLocations() {
121+
Resource resource = ApplicationResourceLoader.get().getResource("classpath:doesnt-exist");
122+
assertThat(resource.exists()).isFalse();
123+
}
124+
125+
@Test
126+
void shouldLoadClasspathLocationsWithWorkingDirectory() {
127+
ClassLoader classLoader = getClass().getClassLoader();
128+
Resource resource = ApplicationResourceLoader
129+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
130+
Path.of("/working-directory"))
131+
.getResource("classpath:a-file");
132+
assertThat(resource.exists()).isTrue();
133+
}
134+
135+
@Test
136+
void shouldLoadNonExistentClasspathLocationsWithWorkingDirectory() {
137+
ClassLoader classLoader = getClass().getClassLoader();
138+
Resource resource = ApplicationResourceLoader
139+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
140+
Path.of("/working-directory"))
141+
.getResource("classpath:doesnt-exist");
142+
assertThat(resource.exists()).isFalse();
143+
}
144+
145+
@Test
146+
void shouldLoadRelativeFileUris() throws IOException {
147+
Resource resource = ApplicationResourceLoader.get().getResource("file:file.txt");
148+
assertThat(resource.isFile()).isTrue();
149+
assertThat(resource.getFile()).hasNoParent().hasName("file.txt");
150+
}
151+
152+
@Test
153+
void shouldLoadAbsoluteFileUris() throws IOException {
154+
Resource resource = ApplicationResourceLoader.get().getResource("file:/file.txt");
155+
assertThat(resource.isFile()).isTrue();
156+
assertThat(resource.getFile()).hasParent("/").hasName("file.txt");
157+
}
158+
159+
@Test
160+
void shouldLoadRelativeFileUrisWithWorkingDirectory() throws IOException {
161+
ClassLoader classLoader = getClass().getClassLoader();
162+
Resource resource = ApplicationResourceLoader
163+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
164+
Path.of("/working-directory"))
165+
.getResource("file:file.txt");
166+
assertThat(resource.isFile()).isTrue();
167+
assertThat(resource.getFile()).hasParent("/working-directory").hasName("file.txt");
168+
}
169+
170+
@Test
171+
void shouldLoadAbsoluteFileUrisWithWorkingDirectory() throws IOException {
172+
ClassLoader classLoader = getClass().getClassLoader();
173+
Resource resource = ApplicationResourceLoader
174+
.get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader),
175+
Path.of("/working-directory"))
176+
.getResource("file:/file.txt");
177+
assertThat(resource.isFile()).isTrue();
178+
assertThat(resource.getFile()).hasParent("/").hasName("file.txt");
179+
}
180+
64181
@Test
65182
void getWithClassPathIncludesProtocolResolvers() throws IOException {
66183
ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);

0 commit comments

Comments
 (0)