Skip to content

Commit 3acea58

Browse files
committed
Add support for working with resources in tests
Closes gh-44444
1 parent abf320d commit 3acea58

25 files changed

+1397
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Annotation that indicates that the content of a resource should be injected. Supported
26+
* on parameters of type:
27+
*
28+
* <ul>
29+
* <li>{@link String}</li>
30+
* </ul>
31+
*
32+
* @author Andy Wilkinson
33+
*/
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target(ElementType.PARAMETER)
36+
public @interface ResourceContent {
37+
38+
/**
39+
* The name of the resource whose content should be injected.
40+
* @return the resource name
41+
*/
42+
String value();
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.io.File;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.nio.file.Path;
25+
26+
/**
27+
* Annotation that indicates that the path of a resource should be injected. Supported on
28+
* parameters of type:
29+
*
30+
* <ul>
31+
* <li>{@link File}</li>
32+
* <li>{@link Path}</li>
33+
* <li>{@link String}</li>
34+
* </ul>
35+
*
36+
* @author Andy Wilkinson
37+
*/
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Target(ElementType.PARAMETER)
40+
public @interface ResourcePath {
41+
42+
/**
43+
* The name of the resource whose path should be injected.
44+
* @return the resource name
45+
*/
46+
String value();
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.net.URISyntaxException;
22+
import java.net.URL;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.Paths;
26+
import java.util.Arrays;
27+
import java.util.Collections;
28+
import java.util.Enumeration;
29+
import java.util.HashSet;
30+
import java.util.Set;
31+
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.FileSystemUtils;
34+
35+
/**
36+
* A collection of resources.
37+
*
38+
* @author Andy Wilkinson
39+
*/
40+
class Resources {
41+
42+
private final Path root;
43+
44+
Resources(Path root) {
45+
this.root = root;
46+
}
47+
48+
Resources addPackage(Package root, String[] resourceNames) {
49+
Set<String> unmatchedNames = new HashSet<>(Arrays.asList(resourceNames));
50+
try {
51+
Enumeration<URL> sources = getClass().getClassLoader().getResources(root.getName().replace(".", "/"));
52+
for (URL source : Collections.list(sources)) {
53+
Path sourceRoot = Paths.get(source.toURI());
54+
for (String resourceName : resourceNames) {
55+
Path resource = sourceRoot.resolve(resourceName);
56+
if (Files.isRegularFile(resource)) {
57+
Path target = this.root.resolve(resourceName);
58+
Path targetDirectory = target.getParent();
59+
if (!Files.isDirectory(targetDirectory)) {
60+
Files.createDirectories(targetDirectory);
61+
}
62+
Files.copy(resource, target);
63+
unmatchedNames.remove(resourceName);
64+
}
65+
}
66+
}
67+
Assert.isTrue(unmatchedNames.isEmpty(),
68+
"Package '" + root.getName() + "' did not contain resources: " + unmatchedNames);
69+
}
70+
catch (IOException ex) {
71+
throw new UncheckedIOException(ex);
72+
}
73+
catch (URISyntaxException ex) {
74+
throw new RuntimeException(ex);
75+
}
76+
return this;
77+
}
78+
79+
Resources addResource(String name, String content) {
80+
Path resourcePath = this.root.resolve(name);
81+
if (Files.isDirectory(resourcePath)) {
82+
throw new IllegalStateException(
83+
"Cannot create resource '" + name + "' as a directory already exists at that location");
84+
}
85+
Path parent = resourcePath.getParent();
86+
try {
87+
if (!Files.isDirectory(resourcePath)) {
88+
Files.createDirectories(parent);
89+
}
90+
Files.writeString(resourcePath, processContent(content));
91+
}
92+
catch (IOException ex) {
93+
throw new UncheckedIOException(ex);
94+
}
95+
return this;
96+
}
97+
98+
private String processContent(String content) {
99+
return content.replace("${resourceRoot}", this.root.toString());
100+
}
101+
102+
Resources addDirectory(String name) {
103+
Path directoryPath = this.root.resolve(name);
104+
if (Files.isRegularFile(directoryPath)) {
105+
throw new IllegalStateException(
106+
"Cannot create directory '" + name + " as a file already exists at that location");
107+
}
108+
try {
109+
Files.createDirectories(directoryPath);
110+
}
111+
catch (IOException ex) {
112+
throw new UncheckedIOException(ex);
113+
}
114+
return this;
115+
}
116+
117+
void delete() {
118+
try {
119+
FileSystemUtils.deleteRecursively(this.root);
120+
}
121+
catch (IOException ex) {
122+
throw new UncheckedIOException(ex);
123+
}
124+
}
125+
126+
Path getRoot() {
127+
return this.root;
128+
}
129+
130+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.net.URL;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.Collections;
25+
import java.util.Enumeration;
26+
import java.util.List;
27+
28+
/**
29+
* A {@link ClassLoader} that provides access to {@link Resources resources}.
30+
*
31+
* @author Andy Wilkinson
32+
*/
33+
class ResourcesClassLoader extends ClassLoader {
34+
35+
private final Resources resources;
36+
37+
ResourcesClassLoader(ClassLoader parent, Resources resources) {
38+
super(parent);
39+
this.resources = resources;
40+
}
41+
42+
@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+
}
52+
}
53+
return null;
54+
}
55+
56+
@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();
60+
}
61+
62+
}

0 commit comments

Comments
 (0)