Skip to content

Commit 29f085b

Browse files
committed
Automatically register directories for registered resource hints
When a hint such as `graphql/*.*` is registered for resources that are looked up via classpath scanning using a pattern such as `classpath*:graphql/**/*.graphqls`, an appropriate pattern is in fact registered in the generated `resource-config.json` file for GraalVM native images; however, classpath scanning fails since GraalVM currently does not make the `graphql` directory automatically available as a classpath resource. This can be very confusing and cumbersome for users since a file such as `graphql/schema.graphqls` will not be discovered via classpath scanning even though the file is present in the native image filesystem. To address this, this commit automatically registers resource hints for enclosing directories for a registered pattern. If the GraalVM team later decides to perform automatic directory registration, we can then remove the code introduced in conjunction with this issue. Closes gh-29403
1 parent d03102e commit 29f085b

File tree

7 files changed

+123
-37
lines changed

7 files changed

+123
-37
lines changed

spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,15 @@ void applyToWhenHasImportAwareConfigurationRegistersHints() {
149149
.singleElement()
150150
.satisfies(resourceHint -> assertThat(resourceHint.getIncludes())
151151
.map(ResourcePatternHint::getPattern)
152-
.containsOnly("org/springframework/context/testfixture/context/annotation/ImportConfiguration.class"));
152+
.containsExactlyInAnyOrder(
153+
"org",
154+
"org/springframework",
155+
"org/springframework/context",
156+
"org/springframework/context/testfixture",
157+
"org/springframework/context/testfixture/context",
158+
"org/springframework/context/testfixture/context/annotation",
159+
"org/springframework/context/testfixture/context/annotation/ImportConfiguration.class"
160+
));
153161
}
154162

155163
@SuppressWarnings("unchecked")

spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
*
3232
* @author Stephane Nicoll
3333
* @author Brian Clozel
34+
* @author Sam Brannen
3435
* @since 6.0
3536
*/
3637
public final class ResourcePatternHints {
@@ -81,12 +82,57 @@ public static class Builder {
8182
* @return {@code this}, to facilitate method chaining
8283
*/
8384
public Builder includes(@Nullable TypeReference reachableType, String... includes) {
84-
List<ResourcePatternHint> newIncludes = Arrays.stream(includes)
85-
.map(include -> new ResourcePatternHint(include, reachableType)).toList();
86-
this.includes.addAll(newIncludes);
85+
Arrays.stream(includes)
86+
.map(this::expandToIncludeDirectories)
87+
.flatMap(List::stream)
88+
.map(include -> new ResourcePatternHint(include, reachableType))
89+
.forEach(this.includes::add);
8790
return this;
8891
}
8992

93+
/**
94+
* Expand the supplied include pattern into multiple patterns that include
95+
* all parent directories for the ultimate resource or resources.
96+
* <p>This is necessary to support classpath scanning within a GraalVM
97+
* native image.
98+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/29403">gh-29403</a>
99+
*/
100+
private List<String> expandToIncludeDirectories(String includePattern) {
101+
// Root resource or no explicit subdirectories?
102+
if (!includePattern.contains("/")) {
103+
if (includePattern.contains("*")) {
104+
// If it's a root pattern, include the root directory as well as the pattern
105+
return List.of("/", includePattern);
106+
}
107+
else {
108+
// Include only the root resource
109+
return List.of(includePattern);
110+
}
111+
}
112+
113+
List<String> includePatterns = new ArrayList<>();
114+
// Ensure the original pattern is always included
115+
includePatterns.add(includePattern);
116+
StringBuilder path = new StringBuilder();
117+
for (String pathElement : includePattern.split("/")) {
118+
if (pathElement.isEmpty()) {
119+
// Skip empty path elements
120+
continue;
121+
}
122+
if (pathElement.contains("*")) {
123+
// Stop at the first encountered wildcard, since we cannot reliably reason
124+
// any further about the directory structure below this path element.
125+
break;
126+
}
127+
if (!path.isEmpty()) {
128+
path.append("/");
129+
}
130+
path.append(pathElement);
131+
includePatterns.add(path.toString());
132+
}
133+
return includePatterns;
134+
}
135+
90136
/**
91137
* Include resources matching the specified patterns.
92138
* @param includes the include patterns (see {@link ResourcePatternHint} documentation)

spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -46,38 +46,40 @@ class ResourceHintsTests {
4646
void registerType() {
4747
this.resourceHints.registerType(String.class);
4848
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
49-
patternOf("java/lang/String.class"));
49+
patternOf("java", "java/lang", "java/lang/String.class"));
5050
}
5151

5252
@Test
5353
void registerTypeWithNestedType() {
5454
this.resourceHints.registerType(TypeReference.of(Nested.class));
5555
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
56-
patternOf("org/springframework/aot/hint/ResourceHintsTests$Nested.class"));
56+
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint",
57+
"org/springframework/aot/hint/ResourceHintsTests$Nested.class"));
5758
}
5859

5960
@Test
6061
void registerTypeWithInnerNestedType() {
6162
this.resourceHints.registerType(TypeReference.of(Inner.class));
6263
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
63-
patternOf("org/springframework/aot/hint/ResourceHintsTests$Nested$Inner.class"));
64+
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint",
65+
"org/springframework/aot/hint/ResourceHintsTests$Nested$Inner.class"));
6466
}
6567

6668
@Test
6769
void registerTypeSeveralTimesAddsOnlyOneEntry() {
6870
this.resourceHints.registerType(String.class);
6971
this.resourceHints.registerType(TypeReference.of(String.class));
7072
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
71-
patternOf("java/lang/String.class"));
73+
patternOf("java", "java/lang", "java/lang/String.class"));
7274
}
7375

7476
@Test
75-
void registerExactMatch() {
77+
void registerExactMatches() {
7678
this.resourceHints.registerPattern("com/example/test.properties");
7779
this.resourceHints.registerPattern("com/example/another.properties");
7880
assertThat(this.resourceHints.resourcePatternHints())
79-
.anySatisfy(patternOf("com/example/test.properties"))
80-
.anySatisfy(patternOf("com/example/another.properties"))
81+
.anySatisfy(patternOf("com", "com/example", "com/example/test.properties"))
82+
.anySatisfy(patternOf("com", "com/example", "com/example/another.properties"))
8183
.hasSize(2);
8284
}
8385

@@ -88,19 +90,26 @@ void registerRootDirectory() {
8890
patternOf("/"));
8991
}
9092

93+
@Test
94+
void registerRootPattern() {
95+
this.resourceHints.registerPattern("*.properties");
96+
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
97+
patternOf("/", "*.properties"));
98+
}
99+
91100
@Test
92101
void registerPattern() {
93102
this.resourceHints.registerPattern("com/example/*.properties");
94103
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
95-
patternOf("com/example/*.properties"));
104+
patternOf("com", "com/example", "com/example/*.properties"));
96105
}
97106

98107
@Test
99108
void registerPatternWithIncludesAndExcludes() {
100109
this.resourceHints.registerPattern(resourceHint ->
101110
resourceHint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties"));
102111
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(
103-
List.of("com/example/*.properties"),
112+
List.of("com", "com/example", "com/example/*.properties"),
104113
List.of("com/example/to-ignore.properties")));
105114
}
106115

@@ -109,7 +118,7 @@ void registerIfPresentRegisterExistingLocation() {
109118
this.resourceHints.registerPatternIfPresent(null, "META-INF/",
110119
resourceHint -> resourceHint.includes("com/example/*.properties"));
111120
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
112-
patternOf("com/example/*.properties"));
121+
patternOf("com", "com/example", "com/example/*.properties"));
113122
}
114123

115124
@Test
@@ -142,15 +151,17 @@ void registerResourceWithExistingClassPathResource() {
142151
String path = "org/springframework/aot/hint/support";
143152
ClassPathResource resource = new ClassPathResource(path);
144153
this.resourceHints.registerResource(resource);
145-
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(path));
154+
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
155+
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint", path));
146156
}
147157

148158
@Test
149159
void registerResourceWithExistingRelativeClassPathResource() {
150160
String path = "org/springframework/aot/hint/support";
151161
ClassPathResource resource = new ClassPathResource("support", RuntimeHints.class);
152162
this.resourceHints.registerResource(resource);
153-
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(path));
163+
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
164+
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint", path));
154165
}
155166

156167
@Test
@@ -179,7 +190,7 @@ private Consumer<ResourceBundleHint> resourceBundle(String baseName) {
179190

180191
private Consumer<ResourcePatternHints> patternOf(List<String> includes, List<String> excludes) {
181192
return pattern -> {
182-
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(includes);
193+
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyInAnyOrderElementsOf(includes);
183194
assertThat(pattern.getExcludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(excludes);
184195
};
185196
}

spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class RuntimeHintsTests {
3131

3232
private final RuntimeHints hints = new RuntimeHints();
3333

34+
3435
@Test
3536
void reflectionHintWithClass() {
3637
this.hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
@@ -47,7 +48,8 @@ void reflectionHintWithClass() {
4748
void resourceHintWithClass() {
4849
this.hints.resources().registerType(String.class);
4950
assertThat(this.hints.resources().resourcePatternHints()).singleElement().satisfies(resourceHint -> {
50-
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern).containsExactly("java/lang/String.class");
51+
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern)
52+
.containsExactlyInAnyOrder("java", "java/lang", "java/lang/String.class");
5153
assertThat(resourceHint.getExcludes()).isEmpty();
5254
});
5355
}

spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class FilePatternResourceHintsRegistrarTests {
3737

3838
private final ResourceHints hints = new ResourceHints();
3939

40+
4041
@Test
4142
void createWithInvalidName() {
4243
assertThatIllegalArgumentException().isThrownBy(() -> new FilePatternResourceHintsRegistrar(
@@ -56,63 +57,63 @@ void registerWithSinglePattern() {
5657
new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt"))
5758
.registerHints(this.hints, null);
5859
assertThat(this.hints.resourcePatternHints()).singleElement()
59-
.satisfies(includes("test*.txt"));
60+
.satisfies(includes("/", "test*.txt"));
6061
}
6162

6263
@Test
6364
void registerWithMultipleNames() {
6465
new FilePatternResourceHintsRegistrar(List.of("test", "another"), List.of(""), List.of(".txt"))
6566
.registerHints(this.hints, null);
6667
assertThat(this.hints.resourcePatternHints()).singleElement()
67-
.satisfies(includes("test*.txt", "another*.txt"));
68+
.satisfies(includes("/" , "test*.txt", "another*.txt"));
6869
}
6970

7071
@Test
7172
void registerWithMultipleLocations() {
7273
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("", "META-INF"), List.of(".txt"))
7374
.registerHints(this.hints, null);
7475
assertThat(this.hints.resourcePatternHints()).singleElement()
75-
.satisfies(includes("test*.txt", "META-INF/test*.txt"));
76+
.satisfies(includes("/", "test*.txt", "META-INF", "META-INF/test*.txt"));
7677
}
7778

7879
@Test
7980
void registerWithMultipleExtensions() {
8081
new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt", ".conf"))
8182
.registerHints(this.hints, null);
8283
assertThat(this.hints.resourcePatternHints()).singleElement()
83-
.satisfies(includes("test*.txt", "test*.conf"));
84+
.satisfies(includes("/", "test*.txt", "test*.conf"));
8485
}
8586

8687
@Test
8788
void registerWithLocationWithoutTrailingSlash() {
8889
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("META-INF"), List.of(".txt"))
8990
.registerHints(this.hints, null);
9091
assertThat(this.hints.resourcePatternHints()).singleElement()
91-
.satisfies(includes("META-INF/test*.txt"));
92+
.satisfies(includes("META-INF", "META-INF/test*.txt"));
9293
}
9394

9495
@Test
9596
void registerWithLocationWithLeadingSlash() {
9697
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("/"), List.of(".txt"))
9798
.registerHints(this.hints, null);
9899
assertThat(this.hints.resourcePatternHints()).singleElement()
99-
.satisfies(includes("test*.txt"));
100+
.satisfies(includes("/", "test*.txt"));
100101
}
101102

102103
@Test
103104
void registerWithLocationUsingResourceClasspathPrefix() {
104105
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:META-INF"), List.of(".txt"))
105106
.registerHints(this.hints, null);
106107
assertThat(this.hints.resourcePatternHints()).singleElement()
107-
.satisfies(includes("META-INF/test*.txt"));
108+
.satisfies(includes("META-INF", "META-INF/test*.txt"));
108109
}
109110

110111
@Test
111112
void registerWithLocationUsingResourceClasspathPrefixAndTrailingSlash() {
112113
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:/META-INF"), List.of(".txt"))
113114
.registerHints(this.hints, null);
114115
assertThat(this.hints.resourcePatternHints()).singleElement()
115-
.satisfies(includes("META-INF/test*.txt"));
116+
.satisfies(includes("META-INF", "META-INF/test*.txt"));
116117
}
117118

118119
@Test

spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@
4949
*
5050
* @author Sebastien Deleuze
5151
* @author Janne Valkealahti
52+
* @author Sam Brannen
5253
*/
53-
public class FileNativeConfigurationWriterTests {
54+
class FileNativeConfigurationWriterTests {
5455

5556
@TempDir
5657
static Path tempDir;
5758

59+
5860
@Test
5961
void emptyConfig() {
6062
Path empty = tempDir.resolve("empty");
@@ -174,6 +176,8 @@ void resourceConfig() throws IOException, JSONException {
174176
"resources": {
175177
"includes": [
176178
{"pattern": "\\\\Qcom/example/test.properties\\\\E"},
179+
{"pattern": "\\\\Qcom\\\\E"},
180+
{"pattern": "\\\\Qcom/example\\\\E"},
177181
{"pattern": "\\\\Qcom/example/another.properties\\\\E"}
178182
]
179183
}
@@ -191,12 +195,12 @@ void namespace() {
191195
resourceHints.registerPattern("com/example/test.properties");
192196
generator.write(hints);
193197
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename);
194-
assertThat(jsonFile.toFile().exists()).isTrue();
198+
assertThat(jsonFile.toFile()).exists();
195199
}
196200

197201
private void assertEquals(String expectedString, String filename) throws IOException, JSONException {
198202
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(filename);
199-
String content = new String(Files.readAllBytes(jsonFile));
203+
String content = Files.readString(jsonFile);
200204
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE);
201205
}
202206

0 commit comments

Comments
 (0)