Skip to content

Commit

Permalink
Automatically register directories for registered resource hints
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Oct 30, 2022
1 parent d03102e commit 29f085b
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,15 @@ void applyToWhenHasImportAwareConfigurationRegistersHints() {
.singleElement()
.satisfies(resourceHint -> assertThat(resourceHint.getIncludes())
.map(ResourcePatternHint::getPattern)
.containsOnly("org/springframework/context/testfixture/context/annotation/ImportConfiguration.class"));
.containsExactlyInAnyOrder(
"org",
"org/springframework",
"org/springframework/context",
"org/springframework/context/testfixture",
"org/springframework/context/testfixture/context",
"org/springframework/context/testfixture/context/annotation",
"org/springframework/context/testfixture/context/annotation/ImportConfiguration.class"
));
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
*
* @author Stephane Nicoll
* @author Brian Clozel
* @author Sam Brannen
* @since 6.0
*/
public final class ResourcePatternHints {
Expand Down Expand Up @@ -81,12 +82,57 @@ public static class Builder {
* @return {@code this}, to facilitate method chaining
*/
public Builder includes(@Nullable TypeReference reachableType, String... includes) {
List<ResourcePatternHint> newIncludes = Arrays.stream(includes)
.map(include -> new ResourcePatternHint(include, reachableType)).toList();
this.includes.addAll(newIncludes);
Arrays.stream(includes)
.map(this::expandToIncludeDirectories)
.flatMap(List::stream)
.map(include -> new ResourcePatternHint(include, reachableType))
.forEach(this.includes::add);
return this;
}

/**
* Expand the supplied include pattern into multiple patterns that include
* all parent directories for the ultimate resource or resources.
* <p>This is necessary to support classpath scanning within a GraalVM
* native image.
* @see <a href="https://github.com/spring-projects/spring-framework/issues/29403">gh-29403</a>
*/
private List<String> expandToIncludeDirectories(String includePattern) {
// Root resource or no explicit subdirectories?
if (!includePattern.contains("/")) {
if (includePattern.contains("*")) {
// If it's a root pattern, include the root directory as well as the pattern
return List.of("/", includePattern);
}
else {
// Include only the root resource
return List.of(includePattern);
}
}

List<String> includePatterns = new ArrayList<>();
// Ensure the original pattern is always included
includePatterns.add(includePattern);
StringBuilder path = new StringBuilder();
for (String pathElement : includePattern.split("/")) {
if (pathElement.isEmpty()) {
// Skip empty path elements
continue;
}
if (pathElement.contains("*")) {
// Stop at the first encountered wildcard, since we cannot reliably reason
// any further about the directory structure below this path element.
break;
}
if (!path.isEmpty()) {
path.append("/");
}
path.append(pathElement);
includePatterns.add(path.toString());
}
return includePatterns;
}

/**
* Include resources matching the specified patterns.
* @param includes the include patterns (see {@link ResourcePatternHint} documentation)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
Expand Down Expand Up @@ -46,38 +46,40 @@ class ResourceHintsTests {
void registerType() {
this.resourceHints.registerType(String.class);
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("java/lang/String.class"));
patternOf("java", "java/lang", "java/lang/String.class"));
}

@Test
void registerTypeWithNestedType() {
this.resourceHints.registerType(TypeReference.of(Nested.class));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org/springframework/aot/hint/ResourceHintsTests$Nested.class"));
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint",
"org/springframework/aot/hint/ResourceHintsTests$Nested.class"));
}

@Test
void registerTypeWithInnerNestedType() {
this.resourceHints.registerType(TypeReference.of(Inner.class));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org/springframework/aot/hint/ResourceHintsTests$Nested$Inner.class"));
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint",
"org/springframework/aot/hint/ResourceHintsTests$Nested$Inner.class"));
}

@Test
void registerTypeSeveralTimesAddsOnlyOneEntry() {
this.resourceHints.registerType(String.class);
this.resourceHints.registerType(TypeReference.of(String.class));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("java/lang/String.class"));
patternOf("java", "java/lang", "java/lang/String.class"));
}

@Test
void registerExactMatch() {
void registerExactMatches() {
this.resourceHints.registerPattern("com/example/test.properties");
this.resourceHints.registerPattern("com/example/another.properties");
assertThat(this.resourceHints.resourcePatternHints())
.anySatisfy(patternOf("com/example/test.properties"))
.anySatisfy(patternOf("com/example/another.properties"))
.anySatisfy(patternOf("com", "com/example", "com/example/test.properties"))
.anySatisfy(patternOf("com", "com/example", "com/example/another.properties"))
.hasSize(2);
}

Expand All @@ -88,19 +90,26 @@ void registerRootDirectory() {
patternOf("/"));
}

@Test
void registerRootPattern() {
this.resourceHints.registerPattern("*.properties");
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("/", "*.properties"));
}

@Test
void registerPattern() {
this.resourceHints.registerPattern("com/example/*.properties");
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("com/example/*.properties"));
patternOf("com", "com/example", "com/example/*.properties"));
}

@Test
void registerPatternWithIncludesAndExcludes() {
this.resourceHints.registerPattern(resourceHint ->
resourceHint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties"));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(
List.of("com/example/*.properties"),
List.of("com", "com/example", "com/example/*.properties"),
List.of("com/example/to-ignore.properties")));
}

Expand All @@ -109,7 +118,7 @@ void registerIfPresentRegisterExistingLocation() {
this.resourceHints.registerPatternIfPresent(null, "META-INF/",
resourceHint -> resourceHint.includes("com/example/*.properties"));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("com/example/*.properties"));
patternOf("com", "com/example", "com/example/*.properties"));
}

@Test
Expand Down Expand Up @@ -142,15 +151,17 @@ void registerResourceWithExistingClassPathResource() {
String path = "org/springframework/aot/hint/support";
ClassPathResource resource = new ClassPathResource(path);
this.resourceHints.registerResource(resource);
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(path));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint", path));
}

@Test
void registerResourceWithExistingRelativeClassPathResource() {
String path = "org/springframework/aot/hint/support";
ClassPathResource resource = new ClassPathResource("support", RuntimeHints.class);
this.resourceHints.registerResource(resource);
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(path));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint", path));
}

@Test
Expand Down Expand Up @@ -179,7 +190,7 @@ private Consumer<ResourceBundleHint> resourceBundle(String baseName) {

private Consumer<ResourcePatternHints> patternOf(List<String> includes, List<String> excludes) {
return pattern -> {
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(includes);
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyInAnyOrderElementsOf(includes);
assertThat(pattern.getExcludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(excludes);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class RuntimeHintsTests {

private final RuntimeHints hints = new RuntimeHints();


@Test
void reflectionHintWithClass() {
this.hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
Expand All @@ -47,7 +48,8 @@ void reflectionHintWithClass() {
void resourceHintWithClass() {
this.hints.resources().registerType(String.class);
assertThat(this.hints.resources().resourcePatternHints()).singleElement().satisfies(resourceHint -> {
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern).containsExactly("java/lang/String.class");
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern)
.containsExactlyInAnyOrder("java", "java/lang", "java/lang/String.class");
assertThat(resourceHint.getExcludes()).isEmpty();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class FilePatternResourceHintsRegistrarTests {

private final ResourceHints hints = new ResourceHints();


@Test
void createWithInvalidName() {
assertThatIllegalArgumentException().isThrownBy(() -> new FilePatternResourceHintsRegistrar(
Expand All @@ -56,63 +57,63 @@ void registerWithSinglePattern() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt"));
.satisfies(includes("/", "test*.txt"));
}

@Test
void registerWithMultipleNames() {
new FilePatternResourceHintsRegistrar(List.of("test", "another"), List.of(""), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt", "another*.txt"));
.satisfies(includes("/" , "test*.txt", "another*.txt"));
}

@Test
void registerWithMultipleLocations() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("", "META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt", "META-INF/test*.txt"));
.satisfies(includes("/", "test*.txt", "META-INF", "META-INF/test*.txt"));
}

@Test
void registerWithMultipleExtensions() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt", ".conf"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt", "test*.conf"));
.satisfies(includes("/", "test*.txt", "test*.conf"));
}

@Test
void registerWithLocationWithoutTrailingSlash() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("META-INF/test*.txt"));
.satisfies(includes("META-INF", "META-INF/test*.txt"));
}

@Test
void registerWithLocationWithLeadingSlash() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("/"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt"));
.satisfies(includes("/", "test*.txt"));
}

@Test
void registerWithLocationUsingResourceClasspathPrefix() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("META-INF/test*.txt"));
.satisfies(includes("META-INF", "META-INF/test*.txt"));
}

@Test
void registerWithLocationUsingResourceClasspathPrefixAndTrailingSlash() {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:/META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("META-INF/test*.txt"));
.satisfies(includes("META-INF", "META-INF/test*.txt"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@
*
* @author Sebastien Deleuze
* @author Janne Valkealahti
* @author Sam Brannen
*/
public class FileNativeConfigurationWriterTests {
class FileNativeConfigurationWriterTests {

@TempDir
static Path tempDir;


@Test
void emptyConfig() {
Path empty = tempDir.resolve("empty");
Expand Down Expand Up @@ -174,6 +176,8 @@ void resourceConfig() throws IOException, JSONException {
"resources": {
"includes": [
{"pattern": "\\\\Qcom/example/test.properties\\\\E"},
{"pattern": "\\\\Qcom\\\\E"},
{"pattern": "\\\\Qcom/example\\\\E"},
{"pattern": "\\\\Qcom/example/another.properties\\\\E"}
]
}
Expand All @@ -191,12 +195,12 @@ void namespace() {
resourceHints.registerPattern("com/example/test.properties");
generator.write(hints);
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename);
assertThat(jsonFile.toFile().exists()).isTrue();
assertThat(jsonFile.toFile()).exists();
}

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

Expand Down
Loading

0 comments on commit 29f085b

Please sign in to comment.