From 29f085bd1a690ff7281e00b3a925e1160850ffbb Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 30 Oct 2022 18:37:02 +0100 Subject: [PATCH] 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 --- ...lassPostProcessorAotContributionTests.java | 10 +++- .../aot/hint/ResourcePatternHints.java | 52 +++++++++++++++++-- .../aot/hint/ResourceHintsTests.java | 39 +++++++++----- .../aot/hint/RuntimeHintsTests.java | 4 +- ...ilePatternResourceHintsRegistrarTests.java | 17 +++--- .../FileNativeConfigurationWriterTests.java | 10 ++-- .../aot/nativex/ResourceHintsWriterTests.java | 28 +++++++--- 7 files changed, 123 insertions(+), 37 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 53d200b9f5..89d0c82f3f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -149,7 +149,15 @@ class ConfigurationClassPostProcessorAotContributionTests { .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") diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java index 5f770feb58..b3480cdb59 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java @@ -31,6 +31,7 @@ import org.springframework.lang.Nullable; * * @author Stephane Nicoll * @author Brian Clozel + * @author Sam Brannen * @since 6.0 */ public final class ResourcePatternHints { @@ -81,12 +82,57 @@ public final class ResourcePatternHints { * @return {@code this}, to facilitate method chaining */ public Builder includes(@Nullable TypeReference reachableType, String... includes) { - List 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. + *

This is necessary to support classpath scanning within a GraalVM + * native image. + * @see gh-29403 + */ + private List 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 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) diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java index 4e5f3d34cb..5d9901401c 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java @@ -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. @@ -46,21 +46,23 @@ 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 @@ -68,16 +70,16 @@ class ResourceHintsTests { 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); } @@ -88,11 +90,18 @@ class ResourceHintsTests { 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 @@ -100,7 +109,7 @@ class ResourceHintsTests { 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"))); } @@ -109,7 +118,7 @@ class ResourceHintsTests { 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 @@ -142,7 +151,8 @@ class ResourceHintsTests { 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 @@ -150,7 +160,8 @@ class ResourceHintsTests { 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 @@ -179,7 +190,7 @@ class ResourceHintsTests { private Consumer patternOf(List includes, List 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); }; } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java index 648ac6b0eb..bb907188c6 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java @@ -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); @@ -47,7 +48,8 @@ class RuntimeHintsTests { 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(); }); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java index f06f406da8..3c280fb05d 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java @@ -37,6 +37,7 @@ class FilePatternResourceHintsRegistrarTests { private final ResourceHints hints = new ResourceHints(); + @Test void createWithInvalidName() { assertThatIllegalArgumentException().isThrownBy(() -> new FilePatternResourceHintsRegistrar( @@ -56,7 +57,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -64,7 +65,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -72,7 +73,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -80,7 +81,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -88,7 +89,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -96,7 +97,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -104,7 +105,7 @@ class FilePatternResourceHintsRegistrarTests { 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 @@ -112,7 +113,7 @@ class FilePatternResourceHintsRegistrarTests { 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 diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java index 238ed1293c..f65d24e8d8 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java @@ -49,12 +49,14 @@ import static org.assertj.core.api.Assertions.assertThat; * * @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"); @@ -174,6 +176,8 @@ public class FileNativeConfigurationWriterTests { "resources": { "includes": [ {"pattern": "\\\\Qcom/example/test.properties\\\\E"}, + {"pattern": "\\\\Qcom\\\\E"}, + {"pattern": "\\\\Qcom/example\\\\E"}, {"pattern": "\\\\Qcom/example/another.properties\\\\E"} ] } @@ -191,12 +195,12 @@ public class FileNativeConfigurationWriterTests { 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); } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java index 2774f59919..e790122359 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java @@ -32,7 +32,7 @@ import org.springframework.aot.hint.TypeReference; * @author Sebastien Deleuze * @author Brian Clozel */ -public class ResourceHintsWriterTests { +class ResourceHintsWriterTests { @Test void empty() throws JSONException { @@ -50,6 +50,8 @@ public class ResourceHintsWriterTests { "resources": { "includes": [ { "pattern": "\\\\Qcom/example/test.properties\\\\E"}, + { "pattern": "\\\\Qcom\\\\E"}, + { "pattern": "\\\\Qcom/example\\\\E"}, { "pattern": "\\\\Qcom/example/another.properties\\\\E"} ] } @@ -64,7 +66,8 @@ public class ResourceHintsWriterTests { { "resources": { "includes": [ - { "pattern": ".*\\\\Q.properties\\\\E"} + { "pattern": ".*\\\\Q.properties\\\\E"}, + { "pattern": "\\\\Q\\/\\\\E"} ] } }""", hints); @@ -78,7 +81,9 @@ public class ResourceHintsWriterTests { { "resources": { "includes": [ - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"} + { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, + { "pattern": "\\\\Qcom\\\\E"}, + { "pattern": "\\\\Qcom/example\\\\E"} ] } }""", hints); @@ -92,7 +97,8 @@ public class ResourceHintsWriterTests { { "resources": { "includes": [ - { "pattern": "\\\\Qstatic/\\\\E.*"} + { "pattern": "\\\\Qstatic/\\\\E.*"}, + { "pattern": "\\\\Qstatic\\\\E"} ] } }""", hints); @@ -108,7 +114,11 @@ public class ResourceHintsWriterTests { "resources": { "includes": [ { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, - { "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"} + { "pattern": "\\\\Qcom\\\\E"}, + { "pattern": "\\\\Qcom/example\\\\E"}, + { "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"}, + { "pattern": "\\\\Qorg\\\\E"}, + { "pattern": "\\\\Qorg/other\\\\E"} ], "excludes": [ { "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"}, @@ -126,7 +136,9 @@ public class ResourceHintsWriterTests { { "resources": { "includes": [ - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"} + { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"}, + { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom\\\\E"}, + { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example\\\\E"} ] } }""", hints); @@ -140,7 +152,9 @@ public class ResourceHintsWriterTests { { "resources": { "includes": [ - { "pattern": "\\\\Qjava/lang/String.class\\\\E"} + { "pattern": "\\\\Qjava/lang/String.class\\\\E" }, + { "pattern": "\\\\Qjava\\\\E" }, + { "pattern": "\\\\Qjava/lang\\\\E" } ] } }""", hints);