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 53d200b9f55f..89d0c82f3f98 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 @@ 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") 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 5f770feb58b2..b3480cdb5981 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 @@ * * @author Stephane Nicoll * @author Brian Clozel + * @author Sam Brannen * @since 6.0 */ public final class ResourcePatternHints { @@ -81,12 +82,57 @@ public static class Builder { * @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 4e5f3d34cbde..5d9901401c7f 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 @@ 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); } @@ -88,11 +90,18 @@ 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 @@ -100,7 +109,7 @@ 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"))); } @@ -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 @@ -142,7 +151,8 @@ 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 @@ -150,7 +160,8 @@ 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 @@ -179,7 +190,7 @@ private Consumer resourceBundle(String baseName) { 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 648ac6b0eb22..bb907188c6ad 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 @@ 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(); }); } 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 f06f406da805..3c280fb05d5a 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 @@ 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 @@ -64,7 +65,7 @@ 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 @@ -72,7 +73,7 @@ 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 @@ -80,7 +81,7 @@ 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 @@ -88,7 +89,7 @@ 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 @@ -96,7 +97,7 @@ 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 @@ -104,7 +105,7 @@ 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 @@ -112,7 +113,7 @@ 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 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 238ed1293cf8..f65d24e8d8b6 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 @@ * * @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 @@ 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"} ] } @@ -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); } 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 2774f59919bd..e79012235931 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 @@ * @author Sebastien Deleuze * @author Brian Clozel */ -public class ResourceHintsWriterTests { +class ResourceHintsWriterTests { @Test void empty() throws JSONException { @@ -50,6 +50,8 @@ void registerExactMatch() throws JSONException { "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 @@ void registerWildcardAtTheBeginningPattern() throws JSONException { { "resources": { "includes": [ - { "pattern": ".*\\\\Q.properties\\\\E"} + { "pattern": ".*\\\\Q.properties\\\\E"}, + { "pattern": "\\\\Q\\/\\\\E"} ] } }""", hints); @@ -78,7 +81,9 @@ void registerWildcardInTheMiddlePattern() throws JSONException { { "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 @@ void registerWildcardAtTheEndPattern() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qstatic/\\\\E.*"} + { "pattern": "\\\\Qstatic/\\\\E.*"}, + { "pattern": "\\\\Qstatic\\\\E"} ] } }""", hints); @@ -108,7 +114,11 @@ void registerPatternWithIncludesAndExcludes() throws JSONException { "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 @@ void registerWithReachableTypeCondition() throws JSONException { { "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 @@ void registerType() throws JSONException { { "resources": { "includes": [ - { "pattern": "\\\\Qjava/lang/String.class\\\\E"} + { "pattern": "\\\\Qjava/lang/String.class\\\\E" }, + { "pattern": "\\\\Qjava\\\\E" }, + { "pattern": "\\\\Qjava/lang\\\\E" } ] } }""", hints);