Skip to content

Commit 5683d39

Browse files
committed
Allow resources to hide existing resources with the same name
See gh-44444
1 parent d5d2746 commit 5683d39

File tree

7 files changed

+183
-56
lines changed

7 files changed

+183
-56
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testsupport.classpath.resources;
18+
19+
import java.nio.file.Path;
20+
21+
/**
22+
* A resource that is to be made available in tests.
23+
*
24+
* @param path the path of the resoure
25+
* @param additional whether the resource should be made available in addition to those
26+
* that already exist elsewhere
27+
* @author Andy Wilkinson
28+
*/
29+
record Resource(Path path, boolean additional) {
30+
31+
}

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/Resources.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import java.util.Arrays;
2727
import java.util.Collections;
2828
import java.util.Enumeration;
29+
import java.util.HashMap;
2930
import java.util.HashSet;
31+
import java.util.Map;
3032
import java.util.Set;
3133

3234
import org.springframework.util.Assert;
@@ -41,6 +43,8 @@ class Resources {
4143

4244
private final Path root;
4345

46+
private final Map<String, Resource> resources = new HashMap<>();
47+
4448
Resources(Path root) {
4549
this.root = root;
4650
}
@@ -60,6 +64,7 @@ Resources addPackage(Package root, String[] resourceNames) {
6064
Files.createDirectories(targetDirectory);
6165
}
6266
Files.copy(resource, target);
67+
register(resourceName, target, true);
6368
unmatchedNames.remove(resourceName);
6469
}
6570
}
@@ -76,7 +81,7 @@ Resources addPackage(Package root, String[] resourceNames) {
7681
return this;
7782
}
7883

79-
Resources addResource(String name, String content) {
84+
Resources addResource(String name, String content, boolean additional) {
8085
Path resourcePath = this.root.resolve(name);
8186
if (Files.isDirectory(resourcePath)) {
8287
throw new IllegalStateException(
@@ -88,13 +93,37 @@ Resources addResource(String name, String content) {
8893
Files.createDirectories(parent);
8994
}
9095
Files.writeString(resourcePath, processContent(content));
96+
register(name, resourcePath, additional);
9197
}
9298
catch (IOException ex) {
9399
throw new UncheckedIOException(ex);
94100
}
95101
return this;
96102
}
97103

104+
private void register(String name, Path resourcePath, boolean additional) {
105+
Resource resource = new Resource(resourcePath, additional);
106+
register(name, resource);
107+
Path ancestor = resourcePath.getParent();
108+
while (!this.root.equals(ancestor)) {
109+
Resource ancestorResource = new Resource(ancestor, additional);
110+
register(this.root.relativize(ancestor).toString(), ancestorResource);
111+
ancestor = ancestor.getParent();
112+
}
113+
}
114+
115+
private void register(String name, Resource resource) {
116+
this.resources.put(name, resource);
117+
if (Files.isDirectory(resource.path())) {
118+
if (name.endsWith("/")) {
119+
this.resources.put(name.substring(0, name.length() - 1), resource);
120+
}
121+
else {
122+
this.resources.put(name + "/", resource);
123+
}
124+
}
125+
}
126+
98127
private String processContent(String content) {
99128
return content.replace("${resourceRoot}", this.root.toString());
100129
}
@@ -107,6 +136,7 @@ Resources addDirectory(String name) {
107136
}
108137
try {
109138
Files.createDirectories(directoryPath);
139+
register(name, directoryPath, true);
110140
}
111141
catch (IOException ex) {
112142
throw new UncheckedIOException(ex);
@@ -117,6 +147,7 @@ Resources addDirectory(String name) {
117147
void delete() {
118148
try {
119149
FileSystemUtils.deleteRecursively(this.root);
150+
this.resources.clear();
120151
}
121152
catch (IOException ex) {
122153
throw new UncheckedIOException(ex);
@@ -127,4 +158,8 @@ Path getRoot() {
127158
return this.root;
128159
}
129160

161+
Resource find(String name) {
162+
return this.resources.get(name);
163+
}
164+
130165
}

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesClassLoader.java

+23-16
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@
1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
2121
import java.net.URL;
22-
import java.nio.file.Files;
23-
import java.nio.file.Path;
22+
import java.util.ArrayList;
2423
import java.util.Collections;
2524
import java.util.Enumeration;
26-
import java.util.List;
2725

2826
/**
2927
* A {@link ClassLoader} that provides access to {@link Resources resources}.
@@ -40,23 +38,32 @@ class ResourcesClassLoader extends ClassLoader {
4038
}
4139

4240
@Override
43-
protected URL findResource(String name) {
44-
Path resource = this.resources.getRoot().resolve(name);
45-
if (Files.exists(resource)) {
46-
try {
47-
return resource.toUri().toURL();
48-
}
49-
catch (IOException ex) {
50-
throw new UncheckedIOException(ex);
51-
}
41+
public URL getResource(String name) {
42+
Resource resource = this.resources.find(name);
43+
return (resource != null) ? urlOf(resource) : getParent().getResource(name);
44+
}
45+
46+
private URL urlOf(Resource resource) {
47+
try {
48+
return resource.path().toUri().toURL();
49+
}
50+
catch (IOException ex) {
51+
throw new UncheckedIOException(ex);
5252
}
53-
return null;
5453
}
5554

5655
@Override
57-
protected Enumeration<URL> findResources(String name) throws IOException {
58-
URL resourceUrl = findResource(name);
59-
return (resourceUrl != null) ? Collections.enumeration(List.of(resourceUrl)) : Collections.emptyEnumeration();
56+
public Enumeration<URL> getResources(String name) throws IOException {
57+
Resource resource = this.resources.find(name);
58+
ArrayList<URL> urls = new ArrayList<>();
59+
if (resource != null) {
60+
URL resourceUrl = urlOf(resource);
61+
urls.add(resourceUrl);
62+
}
63+
if (resource == null || resource.additional()) {
64+
urls.addAll(Collections.list(getParent().getResources(name)));
65+
}
66+
return Collections.enumeration(urls);
6067
}
6168

6269
}

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/ResourcesExtension.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ public void beforeEach(ExtensionContext context) throws Exception {
6363
Resources resources = new Resources(Files.createTempDirectory("resources"));
6464
store.put(RESOURCES_KEY, resources);
6565
Method testMethod = context.getRequiredTestMethod();
66-
resourcesOf(testMethod).forEach((resource) -> resources.addResource(resource.name(), resource.content()));
66+
resourcesOf(testMethod)
67+
.forEach((resource) -> resources.addResource(resource.name(), resource.content(), resource.additional()));
6768
resourceDirectoriesOf(testMethod).forEach((directory) -> resources.addDirectory(directory.value()));
6869
packageResourcesOf(testMethod).forEach((withPackageResources) -> resources
6970
.addPackage(testMethod.getDeclaringClass().getPackage(), withPackageResources.value()));
@@ -148,8 +149,7 @@ else if (parameterType.isAssignableFrom(File.class)) {
148149
private Object resolveResourcePath(ParameterContext parameterContext, ExtensionContext extensionContext) {
149150
Resources resources = getResources(extensionContext);
150151
Class<?> parameterType = parameterContext.getParameter().getType();
151-
Path resourcePath = resources.getRoot()
152-
.resolve(parameterContext.findAnnotation(ResourcePath.class).get().value());
152+
Path resourcePath = resources.find(parameterContext.findAnnotation(ResourcePath.class).get().value()).path();
153153
if (parameterType.isAssignableFrom(Path.class)) {
154154
return resourcePath;
155155
}
@@ -166,8 +166,7 @@ else if (parameterType.isAssignableFrom(String.class)) {
166166
private Object resolveResourceContent(ParameterContext parameterContext, ExtensionContext extensionContext) {
167167
Resources resources = getResources(extensionContext);
168168
Class<?> parameterType = parameterContext.getParameter().getType();
169-
Path resourcePath = resources.getRoot()
170-
.resolve(parameterContext.findAnnotation(ResourceContent.class).get().value());
169+
Path resourcePath = resources.find(parameterContext.findAnnotation(ResourceContent.class).get().value()).path();
171170
if (parameterType.isAssignableFrom(String.class)) {
172171
try (InputStream in = Files.newInputStream(resourcePath)) {
173172
return StreamUtils.copyToString(in, StandardCharsets.UTF_8);

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/resources/WithResource.java

+7
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,11 @@
5454
*/
5555
String content() default "";
5656

57+
/**
58+
* Whether the resource should be available in addition to those that are already on
59+
* the classpath are instead of any existing resources with the same name.
60+
* @return whether this is an additional resource
61+
*/
62+
boolean additional() default true;
63+
5764
}

spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/resources/ResourcesTests.java

+58-34
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.nio.file.Path;
2020

21+
import org.junit.jupiter.api.BeforeEach;
2122
import org.junit.jupiter.api.Test;
2223
import org.junit.jupiter.api.io.TempDir;
2324

@@ -34,99 +35,122 @@ class ResourcesTests {
3435
@TempDir
3536
private Path root;
3637

38+
private Resources resources;
39+
40+
@BeforeEach
41+
void setUp() {
42+
this.resources = new Resources(this.root);
43+
}
44+
3745
@Test
38-
void whenAddResourceThenResourceIsCreated() {
39-
new Resources(this.root).addResource("test", "test-content");
46+
void whenAddResourceThenResourceIsCreatedAndCanBeFound() {
47+
this.resources.addResource("test", "test-content", true);
4048
assertThat(this.root.resolve("test")).hasContent("test-content");
49+
assertThat(this.resources.find("test")).isNotNull();
4150
}
4251

4352
@Test
4453
void whenAddResourceHasContentReferencingResourceRootThenResourceIsCreatedWithReferenceToRoot() {
45-
new Resources(this.root).addResource("test", "*** ${resourceRoot} ***");
54+
this.resources.addResource("test", "*** ${resourceRoot} ***", true);
4655
assertThat(this.root.resolve("test")).hasContent("*** " + this.root + " ***");
4756
}
4857

4958
@Test
50-
void whenAddResourceWithPathThenResourceIsCreated() {
51-
new Resources(this.root).addResource("a/b/c/test", "test-content");
59+
void whenAddResourceWithPathThenResourceIsCreatedAndItAndItsAncestorsCanBeFound() {
60+
this.resources.addResource("a/b/c/test", "test-content", true);
5261
assertThat(this.root.resolve("a/b/c/test")).hasContent("test-content");
62+
assertThat(this.resources.find("a/b/c/test")).isNotNull();
63+
assertThat(this.resources.find("a/b/c/")).isNotNull();
64+
assertThat(this.resources.find("a/b/")).isNotNull();
65+
assertThat(this.resources.find("a/")).isNotNull();
5366
}
5467

5568
@Test
5669
void whenAddResourceAndResourceAlreadyExistsThenResourcesIsOverwritten() {
57-
Resources resources = new Resources(this.root);
58-
resources.addResource("a/b/c/test", "original-content");
59-
resources.addResource("a/b/c/test", "new-content");
70+
this.resources.addResource("a/b/c/test", "original-content", true);
71+
this.resources.addResource("a/b/c/test", "new-content", true);
6072
assertThat(this.root.resolve("a/b/c/test")).hasContent("new-content");
6173
}
6274

6375
@Test
64-
void whenAddPackageThenNamedResourcesFromPackageAreCreated() {
65-
new Resources(this.root).addPackage(getClass().getPackage(),
66-
new String[] { "resource-1.txt", "sub/resource-3.txt" });
76+
void whenAddPackageThenNamedResourcesFromPackageAreCreatedAndCanBeFound() {
77+
this.resources.addPackage(getClass().getPackage(), new String[] { "resource-1.txt", "sub/resource-3.txt" });
6778
assertThat(this.root.resolve("resource-1.txt")).hasContent("one");
6879
assertThat(this.root.resolve("resource-2.txt")).doesNotExist();
6980
assertThat(this.root.resolve("sub/resource-3.txt")).hasContent("three");
81+
assertThat(this.resources.find("resource-1.txt")).isNotNull();
82+
assertThat(this.resources.find("resource-2.txt")).isNull();
83+
assertThat(this.resources.find("sub/resource-3.txt")).isNotNull();
84+
assertThat(this.resources.find("sub/")).isNotNull();
7085
}
7186

7287
@Test
73-
void whenAddResourceAndDeleteThenResourceDoesNotExist() {
74-
Resources resources = new Resources(this.root);
75-
resources.addResource("test", "test-content");
88+
void whenAddResourceAndDeleteThenResourceDoesNotExistAndCannotBeFound() {
89+
this.resources.addResource("test", "test-content", true);
7690
assertThat(this.root.resolve("test")).hasContent("test-content");
77-
resources.delete();
91+
assertThat(this.resources.find("test")).isNotNull();
92+
this.resources.delete();
7893
assertThat(this.root.resolve("test")).doesNotExist();
94+
assertThat(this.resources.find("test")).isNull();
7995
}
8096

8197
@Test
82-
void whenAddPackageAndDeleteThenResourcesDoNotExist() {
83-
Resources resources = new Resources(this.root);
84-
resources.addPackage(getClass().getPackage(),
98+
void whenAddPackageAndDeleteThenResourcesDoNotExistAndCannotBeFound() {
99+
this.resources.addPackage(getClass().getPackage(),
85100
new String[] { "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" });
86101
assertThat(this.root.resolve("resource-1.txt")).hasContent("one");
87102
assertThat(this.root.resolve("resource-2.txt")).hasContent("two");
88103
assertThat(this.root.resolve("sub/resource-3.txt")).hasContent("three");
89-
resources.delete();
104+
assertThat(this.resources.find("resource-1.txt")).isNotNull();
105+
assertThat(this.resources.find("resource-2.txt")).isNotNull();
106+
assertThat(this.resources.find("sub/resource-3.txt")).isNotNull();
107+
assertThat(this.resources.find("sub/")).isNotNull();
108+
this.resources.delete();
90109
assertThat(this.root.resolve("resource-1.txt")).doesNotExist();
91110
assertThat(this.root.resolve("resource-2.txt")).doesNotExist();
92111
assertThat(this.root.resolve("sub/resource-3.txt")).doesNotExist();
93112
assertThat(this.root.resolve("sub")).doesNotExist();
113+
assertThat(this.resources.find("resource-1.txt")).isNull();
114+
assertThat(this.resources.find("resource-2.txt")).isNull();
115+
assertThat(this.resources.find("sub/resource-3.txt")).isNull();
116+
assertThat(this.resources.find("sub/")).isNull();
94117
}
95118

96119
@Test
97-
void whenAddDirectoryThenDirectoryIsCreated() {
98-
Resources resources = new Resources(this.root);
99-
resources.addDirectory("dir");
120+
void whenAddDirectoryThenDirectoryIsCreatedAndCanBeFound() {
121+
this.resources.addDirectory("dir");
100122
assertThat(this.root.resolve("dir")).isDirectory();
123+
assertThat(this.resources.find("dir/")).isNotNull();
101124
}
102125

103126
@Test
104-
void whenAddDirectoryWithPathThenDirectoryIsCreated() {
105-
Resources resources = new Resources(this.root);
106-
resources.addDirectory("one/two/three/dir");
127+
void whenAddDirectoryWithPathThenDirectoryIsCreatedAndItAndItsAncestorsCanBeFound() {
128+
this.resources.addDirectory("one/two/three/dir");
107129
assertThat(this.root.resolve("one/two/three/dir")).isDirectory();
130+
assertThat(this.resources.find("one/two/three/dir/")).isNotNull();
131+
assertThat(this.resources.find("one/two/three/")).isNotNull();
132+
assertThat(this.resources.find("one/two/")).isNotNull();
133+
assertThat(this.resources.find("one/")).isNotNull();
108134
}
109135

110136
@Test
111137
void whenAddDirectoryAndDirectoryAlreadyExistsThenDoesNotThrow() {
112-
Resources resources = new Resources(this.root);
113-
resources.addDirectory("one/two/three/dir");
114-
resources.addDirectory("one/two/three/dir");
138+
this.resources.addDirectory("one/two/three/dir");
139+
this.resources.addDirectory("one/two/three/dir");
115140
assertThat(this.root.resolve("one/two/three/dir")).isDirectory();
116141
}
117142

118143
@Test
119144
void whenAddDirectoryAndResourceAlreadyExistsThenIllegalStateExceptionIsThrown() {
120-
Resources resources = new Resources(this.root);
121-
resources.addResource("one/two/three/", "content");
122-
assertThatIllegalStateException().isThrownBy(() -> resources.addDirectory("one/two/three"));
145+
this.resources.addResource("one/two/three/", "content", true);
146+
assertThatIllegalStateException().isThrownBy(() -> this.resources.addDirectory("one/two/three"));
123147
}
124148

125149
@Test
126150
void whenAddResourceAndDirectoryAlreadyExistsThenIllegalStateExceptionIsThrown() {
127-
Resources resources = new Resources(this.root);
128-
resources.addDirectory("one/two/three");
129-
assertThatIllegalStateException().isThrownBy(() -> resources.addResource("one/two/three", "content"));
151+
this.resources.addDirectory("one/two/three");
152+
assertThatIllegalStateException()
153+
.isThrownBy(() -> this.resources.addResource("one/two/three", "content", true));
130154
}
131155

132156
}

0 commit comments

Comments
 (0)