Update AOT resource hints with new GraalVM behavior

Prior to this commit, the resource hints for AOT applications would
generate JSON metadata with `java.util.regex.Pattern`, like:

```
{
  "resources": {
    "includes": [
      {
        "pattern": "\\Qbanner.txt\\E"
      }
    ]
  }
}
```

This regexp feature, as well as "includes" and "excludes" are not supported
anymore with the new metadata format. This commit removes the pattern
format which is now replaced by a "glob" format.
"globs" should only contain "*" (zero or more characters in a path
segment)  or "**" (zero or more path segments).
Some instances of resource hint registration should be migrated as a
result.
For example, "/files/*.ext" matched both "/files/a.ext" and
"/files/folder/b.txt" in the past. The new behavior matches only the
former, unless the "/files/**/*.ext" glob pattern is used.

This commit also removes the "excludes" support, which was not widely
used and very often could lead to subtle behavior.

Closes gh-31340
This commit is contained in:
Brian Clozel
2024-11-29 14:43:52 +01:00
parent bd81abe58d
commit 989eb37fb7
3 changed files with 44 additions and 76 deletions

View File

@@ -16,12 +16,10 @@
package org.springframework.aot.hint;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
/**
@@ -31,16 +29,18 @@ import org.springframework.util.Assert;
* resource on the classpath, or alternatively may contain the special
* {@code *} character to indicate a wildcard match. For example:
* <ul>
* <li>{@code file.properties}: matches just the {@code file.properties}
* <li>"file.properties": matches just the {@code file.properties}
* file at the root of the classpath.</li>
* <li>{@code com/example/file.properties}: matches just the
* <li>"com/example/file.properties": matches just the
* {@code file.properties} file in {@code com/example/}.</li>
* <li>{@code *.properties}: matches all the files with a {@code .properties}
* extension anywhere in the classpath.</li>
* <li>{@code com/example/*.properties}: matches all the files with a {@code .properties}
* extension in {@code com/example/} and its child directories at any depth.</li>
* <li>{@code com/example/*}: matches all the files in {@code com/example/}
* <li>"*.properties": matches all the files with a {@code .properties}
* extension at the root of the classpath.</li>
* <li>"com/example/*.properties": matches all the files with a {@code .properties}
* extension in {@code com/example/}.</li>
* <li>"com/example/{@literal **}": matches all the files in {@code com/example/}
* and its child directories at any depth.</li>
* <li>"com/example/{@literal **}/*.properties": matches all the files with a {@code .properties}
* extension in {@code com/example/} and its child directories at any depth.</li>
* </ul>
*
* <p>A resource pattern must not start with a slash ({@code /}) unless it is the
@@ -54,6 +54,8 @@ import org.springframework.util.Assert;
*/
public final class ResourcePatternHint implements ConditionalHint {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
private final String pattern;
@Nullable
@@ -77,16 +79,11 @@ public final class ResourcePatternHint implements ConditionalHint {
}
/**
* Return the regex {@link Pattern} to use for identifying the resources to match.
* Whether the given path matches the current glob pattern.
* @param path the path to match against
*/
public Pattern toRegex() {
String prefix = (this.pattern.startsWith("*") ? ".*" : "");
String suffix = (this.pattern.endsWith("*") ? ".*" : "");
String regex = Arrays.stream(this.pattern.split("\\*"))
.filter(s -> !s.isEmpty())
.map(Pattern::quote)
.collect(Collectors.joining(".*", prefix, suffix));
return Pattern.compile(regex);
public boolean matches(String path) {
return PATH_MATCHER.match(this.pattern, path);
}
@Nullable

View File

@@ -38,12 +38,9 @@ public final class ResourcePatternHints {
private final List<ResourcePatternHint> includes;
private final List<ResourcePatternHint> excludes;
private ResourcePatternHints(Builder builder) {
this.includes = new ArrayList<>(builder.includes);
this.excludes = new ArrayList<>(builder.excludes);
}
/**
@@ -54,14 +51,6 @@ public final class ResourcePatternHints {
return this.includes;
}
/**
* Return the exclude patterns to use to identify the resources to match.
* @return the exclude patterns
*/
public List<ResourcePatternHint> getExcludes() {
return this.excludes;
}
/**
* Builder for {@link ResourcePatternHints}.
@@ -70,13 +59,11 @@ public final class ResourcePatternHints {
private final Set<ResourcePatternHint> includes = new LinkedHashSet<>();
private final Set<ResourcePatternHint> excludes = new LinkedHashSet<>();
Builder() {
}
/**
* Include resources matching the specified patterns.
* Include resources matching the specified glob patterns.
* @param reachableType the type that should be reachable for this hint to apply
* @param includes the include patterns (see {@link ResourcePatternHint} documentation)
* @return {@code this}, to facilitate method chaining
@@ -129,7 +116,7 @@ public final class ResourcePatternHints {
}
/**
* Include resources matching the specified patterns.
* Include resources matching the specified glob patterns.
* @param includes the include patterns (see {@link ResourcePatternHint} documentation)
* @return {@code this}, to facilitate method chaining
*/
@@ -137,28 +124,6 @@ public final class ResourcePatternHints {
return includes(null, includes);
}
/**
* Exclude resources matching the specified patterns.
* @param reachableType the type that should be reachable for this hint to apply
* @param excludes the exclude patterns (see {@link ResourcePatternHint} documentation)
* @return {@code this}, to facilitate method chaining
*/
public Builder excludes(@Nullable TypeReference reachableType, String... excludes) {
List<ResourcePatternHint> newExcludes = Arrays.stream(excludes)
.map(include -> new ResourcePatternHint(include, reachableType)).toList();
this.excludes.addAll(newExcludes);
return this;
}
/**
* Exclude resources matching the specified patterns.
* @param excludes the exclude patterns (see {@link ResourcePatternHint} documentation)
* @return {@code this}, to facilitate method chaining
*/
public Builder excludes(String... excludes) {
return excludes(null, excludes);
}
/**
* Create {@link ResourcePatternHints} based on the state of this
* builder.

View File

@@ -39,49 +39,55 @@ class ResourcePatternHintTests {
@Test
void rootDirectory() {
ResourcePatternHint hint = new ResourcePatternHint("/", null);
assertThat(hint.toRegex().asMatchPredicate())
.accepts("/")
.rejects("/com/example", "/file.txt");
assertThat(hint.matches("/")).isTrue();
assertThat(hint.matches("/com/example")).isFalse();
assertThat(hint.matches("/file.txt")).isFalse();
}
@Test
void fileAtRoot() {
ResourcePatternHint hint = new ResourcePatternHint("file.properties", null);
assertThat(hint.toRegex().asMatchPredicate())
.accepts("file.properties")
.rejects("com/example/file.properties", "file.prop", "another-file.properties");
assertThat(hint.matches("file.properties")).isTrue();
assertThat(hint.matches("com/example/file.properties")).isFalse();
assertThat(hint.matches("file.prop")).isFalse();
assertThat(hint.matches("another-file.properties")).isFalse();
}
@Test
void fileInDirectory() {
ResourcePatternHint hint = new ResourcePatternHint("com/example/file.properties", null);
assertThat(hint.toRegex().asMatchPredicate())
.accepts("com/example/file.properties")
.rejects("file.properties", "com/file.properties", "com/example/another-file.properties");
assertThat(hint.matches("com/example/file.properties")).isTrue();
assertThat(hint.matches("file.properties")).isFalse();
assertThat(hint.matches("com/file.properties")).isFalse();
assertThat(hint.matches("com/example/another-file.properties")).isFalse();
}
@Test
void extension() {
ResourcePatternHint hint = new ResourcePatternHint("*.properties", null);
assertThat(hint.toRegex().asMatchPredicate())
.accepts("file.properties", "com/example/file.properties")
.rejects("file.prop", "com/example/file.prop");
ResourcePatternHint hint = new ResourcePatternHint("**/*.properties", null);
assertThat(hint.matches("file.properties")).isTrue();
assertThat(hint.matches("com/example/file.properties")).isTrue();
assertThat(hint.matches("file.prop")).isFalse();
assertThat(hint.matches("com/example/file.prop")).isFalse();
}
@Test
void extensionInDirectoryAtAnyDepth() {
ResourcePatternHint hint = new ResourcePatternHint("com/example/*.properties", null);
assertThat(hint.toRegex().asMatchPredicate())
.accepts("com/example/file.properties", "com/example/another/file.properties")
.rejects("file.properties", "com/file.properties");
assertThat(hint.matches("com/example/file.properties")).isTrue();
assertThat(hint.matches("com/example/another/file.properties")).isFalse();
assertThat(hint.matches("com/file.properties")).isFalse();
assertThat(hint.matches("file.properties")).isFalse();
}
@Test
void anyFileInDirectoryAtAnyDepth() {
ResourcePatternHint hint = new ResourcePatternHint("com/example/*", null);
assertThat(hint.toRegex().asMatchPredicate())
.accepts("com/example/file.properties", "com/example/another/file.properties", "com/example/another")
.rejects("file.properties", "com/file.properties");
ResourcePatternHint hint = new ResourcePatternHint("com/example/**", null);
assertThat(hint.matches("com/example/file.properties")).isTrue();
assertThat(hint.matches("com/example/another/file.properties")).isTrue();
assertThat(hint.matches("com/example/another")).isTrue();
assertThat(hint.matches("file.properties")).isFalse();
assertThat(hint.matches("com/file.properties")).isFalse();
}
}