diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle
index 446a86f8d7..2a4f18258e 100644
--- a/spring-core/spring-core.gradle
+++ b/spring-core/spring-core.gradle
@@ -67,6 +67,7 @@ dependencies {
testImplementation("org.xmlunit:xmlunit-matchers")
testImplementation("io.projectreactor:reactor-test")
testImplementation("io.projectreactor.tools:blockhound")
+ testImplementation("org.skyscreamer:jsonassert")
testFixturesImplementation("com.google.code.findbugs:jsr305")
testFixturesImplementation("org.junit.platform:junit-platform-launcher")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationGenerator.java b/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationGenerator.java
new file mode 100644
index 0000000000..8f2f6de3e8
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationGenerator.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.springframework.aot.hint.JavaSerializationHints;
+import org.springframework.aot.hint.ProxyHints;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.ResourceHints;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.lang.Nullable;
+
+/**
+ * Generate the GraalVM native configuration files from runtime hints.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ * @see Native Image Build Configuration
+ */
+public class FileNativeConfigurationGenerator implements NativeConfigurationGenerator {
+
+ private final Path basePath;
+
+ private final String groupId;
+
+ private final String artifactId;
+
+ public FileNativeConfigurationGenerator(Path basePath) {
+ this(basePath, null, null);
+ }
+
+ public FileNativeConfigurationGenerator(Path basePath, @Nullable String groupId, @Nullable String artifactId) {
+ this.basePath = basePath;
+ if ((groupId == null && artifactId != null) || (groupId != null && artifactId == null)) {
+ throw new IllegalArgumentException("groupId and artifactId must be both null or both non-null");
+ }
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ }
+
+ @Override
+ public void generate(RuntimeHints hints) {
+ try {
+ if (hints.javaSerialization().types().findAny().isPresent()) {
+ generateFile(hints.javaSerialization());
+ }
+ if (hints.proxies().jdkProxies().findAny().isPresent()) {
+ generateFile(hints.proxies());
+ }
+ if (hints.reflection().typeHints().findAny().isPresent()) {
+ generateFile(hints.reflection());
+ }
+ if (hints.resources().resourcePatterns().findAny().isPresent() ||
+ hints.resources().resourceBundles().findAny().isPresent()) {
+ generateFile(hints.resources());
+ }
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException("Unexpected I/O error while writing the native configuration", ex);
+ }
+ }
+
+ /**
+ * Generate the Java serialization native configuration file.
+ */
+ private void generateFile(JavaSerializationHints hints) throws IOException {
+ JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
+ File file = createIfNecessary("serialization-config.json");
+ FileWriter writer = new FileWriter(file);
+ writer.write(serializer.serialize(hints));
+ writer.close();
+ }
+
+ /**
+ * Generate the proxy native configuration file.
+ */
+ private void generateFile(ProxyHints hints) throws IOException {
+ ProxyHintsSerializer serializer = new ProxyHintsSerializer();
+ File file = createIfNecessary("proxy-config.json");
+ FileWriter writer = new FileWriter(file);
+ writer.write(serializer.serialize(hints));
+ writer.close();
+ }
+
+ /**
+ * Generate the reflection native configuration file.
+ */
+ private void generateFile(ReflectionHints hints) throws IOException {
+ ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
+ File file = createIfNecessary("reflect-config.json");
+ FileWriter writer = new FileWriter(file);
+ writer.write(serializer.serialize(hints));
+ writer.close();
+ }
+
+ /**
+ * Generate the resource native configuration file.
+ */
+ private void generateFile(ResourceHints hints) throws IOException {
+ ResourceHintsSerializer serializer = new ResourceHintsSerializer();
+ File file = createIfNecessary("resource-config.json");
+ FileWriter writer = new FileWriter(file);
+ writer.write(serializer.serialize(hints));
+ writer.close();
+ }
+
+ private File createIfNecessary(String filename) throws IOException {
+ Path outputDirectory = this.basePath.resolve("META-INF").resolve("native-image");
+ if (this.groupId != null && this.artifactId != null) {
+ outputDirectory = outputDirectory.resolve(this.groupId).resolve(this.artifactId);
+ }
+ outputDirectory.toFile().mkdirs();
+ File file = outputDirectory.resolve(filename).toFile();
+ file.createNewFile();
+ return file;
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java
new file mode 100644
index 0000000000..8c4549164d
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.util.Iterator;
+
+import org.springframework.aot.hint.JavaSerializationHints;
+import org.springframework.aot.hint.TypeReference;
+
+/**
+ * Serialize a {@link JavaSerializationHints} to the JSON file expected by GraalVM {@code native-image} compiler,
+ * typically named {@code serialization-config.json}.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ * @see Native Image Build Configuration
+ */
+class JavaSerializationHintsSerializer {
+
+ public String serialize(JavaSerializationHints hints) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[\n");
+ Iterator typeIterator = hints.types().iterator();
+ while (typeIterator.hasNext()) {
+ TypeReference type = typeIterator.next();
+ String name = JsonUtils.escape(type.getCanonicalName());
+ builder.append("{ \"name\": \"").append(name).append("\" }");
+ if (typeIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("\n]\n");
+ return builder.toString();
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java b/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java
new file mode 100644
index 0000000000..15ed1b58e6
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+/**
+ * Utility class for JSON.
+ *
+ * @author Sebastien Deleuze
+ */
+abstract class JsonUtils {
+
+ /**
+ * Escape a JSON String.
+ */
+ static String escape(String input) {
+ StringBuilder builder = new StringBuilder();
+ input.chars().forEach(c -> {
+ switch (c) {
+ case '"':
+ builder.append("\\\"");
+ break;
+ case '\\':
+ builder.append("\\\\");
+ break;
+ case '/':
+ builder.append("\\/");
+ break;
+ case '\b':
+ builder.append("\\b");
+ break;
+ case '\f':
+ builder.append("\\f");
+ break;
+ case '\n':
+ builder.append("\\n");
+ break;
+ case '\r':
+ builder.append("\\r");
+ break;
+ case '\t':
+ builder.append("\\t");
+ break;
+ default:
+ if (c <= 0x1F) {
+ builder.append(String.format("\\u%04x", c));
+ }
+ else {
+ builder.append((char) c);
+ }
+ break;
+ }
+ });
+ return builder.toString();
+ }
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationGenerator.java b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationGenerator.java
new file mode 100644
index 0000000000..841794108f
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationGenerator.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import org.springframework.aot.hint.RuntimeHints;
+
+/**
+ * Generate GraalVM native configuration.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ * @see Native Image Build Configuration
+ */
+public interface NativeConfigurationGenerator {
+
+ /**
+ * Generate the GraalVM native configuration from the provided hints.
+ * @param hints the hints to serialize
+ */
+ void generate(RuntimeHints hints);
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java
new file mode 100644
index 0000000000..835890e065
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.util.Iterator;
+
+import org.springframework.aot.hint.JdkProxyHint;
+import org.springframework.aot.hint.ProxyHints;
+import org.springframework.aot.hint.TypeReference;
+
+/**
+ * Serialize {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON file expected by GraalVM
+ * {@code native-image} compiler, typically named {@code proxy-config.json}.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ * @see Dynamic Proxy in Native Image
+ * @see Native Image Build Configuration
+ */
+class ProxyHintsSerializer {
+
+ public String serialize(ProxyHints hints) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[\n");
+ Iterator hintIterator = hints.jdkProxies().iterator();
+ while (hintIterator.hasNext()) {
+ builder.append("{ \"interfaces\": [ ");
+ JdkProxyHint hint = hintIterator.next();
+ Iterator interfaceIterator = hint.getProxiedInterfaces().iterator();
+ while (interfaceIterator.hasNext()) {
+ String name = JsonUtils.escape(interfaceIterator.next().getCanonicalName());
+ builder.append("\"").append(name).append("\"");
+ if (interfaceIterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ builder.append(" ] }");
+ if (hintIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("\n]\n");
+ return builder.toString();
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java
new file mode 100644
index 0000000000..47fc5951ce
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.springframework.aot.hint.ExecutableHint;
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.FieldHint;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.TypeHint;
+import org.springframework.aot.hint.TypeReference;
+
+/**
+ * Serialize {@link ReflectionHints} to the JSON file expected by GraalVM {@code native-image} compiler,
+ * typically named {@code reflect-config.json}.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ * @see Reflection Use in Native Images
+ * @see Native Image Build Configuration
+ */
+@SuppressWarnings("serial")
+class ReflectionHintsSerializer {
+
+ public String serialize(ReflectionHints hints) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[\n");
+ Iterator hintIterator = hints.typeHints().iterator();
+ while (hintIterator.hasNext()) {
+ TypeHint hint = hintIterator.next();
+ String name = JsonUtils.escape(hint.getType().getCanonicalName());
+ builder.append("{\n\"name\": \"").append(name).append("\"");
+ serializeCondition(hint, builder);
+ serializeMembers(hint, builder);
+ serializeFields(hint, builder);
+ serializeExecutables(hint, builder);
+ builder.append(" }");
+ if (hintIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("\n]");
+ return builder.toString();
+ }
+
+ private void serializeCondition(TypeHint hint, StringBuilder builder) {
+ if (hint.getReachableType() != null) {
+ String name = JsonUtils.escape(hint.getReachableType().getCanonicalName());
+ builder.append(",\n\"condition\": { \"typeReachable\": \"").append(name).append("\" }");
+ }
+ }
+
+ private void serializeFields(TypeHint hint, StringBuilder builder) {
+ Iterator fieldIterator = hint.fields().iterator();
+ if (fieldIterator.hasNext()) {
+ builder.append(",\n\"fields\": [\n");
+ while (fieldIterator.hasNext()) {
+ FieldHint fieldHint = fieldIterator.next();
+ String name = JsonUtils.escape(fieldHint.getName());
+ builder.append("{ \"name\": \"").append(name).append("\"");
+ if (fieldHint.isAllowWrite()) {
+ builder.append(", \"allowWrite\": ").append(fieldHint.isAllowWrite());
+ }
+ if (fieldHint.isAllowUnsafeAccess()) {
+ builder.append(", \"allowUnsafeAccess\": ").append(fieldHint.isAllowUnsafeAccess());
+ }
+ builder.append(" }");
+ if (fieldIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("\n]");
+ }
+ }
+
+ private void serializeExecutables(TypeHint hint, StringBuilder builder) {
+ List executables = Stream.concat(hint.constructors(), hint.methods()).toList();
+ Iterator methodIterator = executables.stream().filter(h -> h.getModes().contains(ExecutableMode.INVOKE) || h.getModes().isEmpty()).iterator();
+ Iterator queriedMethodIterator = executables.stream().filter(h -> h.getModes().contains(ExecutableMode.INTROSPECT)).iterator();
+ if (methodIterator.hasNext()) {
+ builder.append(",\n");
+ serializeMethods("methods", methodIterator, builder);
+ if (queriedMethodIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ if (queriedMethodIterator.hasNext()) {
+ serializeMethods("queriedMethods", queriedMethodIterator, builder);
+ }
+ }
+
+ private void serializeMethods(String fieldName, Iterator methodIterator, StringBuilder builder) {
+ builder.append("\"").append(JsonUtils.escape(fieldName)).append("\": [\n");
+ while (methodIterator.hasNext()) {
+ ExecutableHint hint = methodIterator.next();
+ String name = JsonUtils.escape(hint.getName());
+ builder.append("{\n\"name\": \"").append(name).append("\", ").append("\"parameterTypes\": [ ");
+ Iterator parameterIterator = hint.getParameterTypes().iterator();
+ while (parameterIterator.hasNext()) {
+ String parameterName = JsonUtils.escape(parameterIterator.next().getCanonicalName());
+ builder.append("\"").append(parameterName).append("\"");
+ if (parameterIterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ builder.append(" ] }\n");
+ if (methodIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("]\n");
+ }
+
+ private void serializeMembers(TypeHint hint, StringBuilder builder) {
+ Iterator categoryIterator = hint.getMemberCategories().iterator();
+ if (categoryIterator.hasNext()) {
+ builder.append(",\n");
+ while (categoryIterator.hasNext()) {
+ switch (categoryIterator.next()) {
+ case PUBLIC_FIELDS -> builder.append("\"allPublicFields\": true");
+ case DECLARED_FIELDS -> builder.append("\"allDeclaredFields\": true");
+ case INTROSPECT_PUBLIC_CONSTRUCTORS -> builder.append("\"queryAllPublicConstructors\": true");
+ case INTROSPECT_DECLARED_CONSTRUCTORS -> builder.append("\"queryAllDeclaredConstructors\": true");
+ case INVOKE_PUBLIC_CONSTRUCTORS -> builder.append("\"allPublicConstructors\": true");
+ case INVOKE_DECLARED_CONSTRUCTORS -> builder.append("\"allDeclaredConstructors\": true");
+ case INTROSPECT_PUBLIC_METHODS -> builder.append("\"queryAllPublicMethods\": true");
+ case INTROSPECT_DECLARED_METHODS -> builder.append("\"queryAllDeclaredMethods\": true");
+ case INVOKE_PUBLIC_METHODS -> builder.append("\"allPublicMethods\": true");
+ case INVOKE_DECLARED_METHODS -> builder.append("\"allDeclaredMethods\": true");
+ case PUBLIC_CLASSES -> builder.append("\"allPublicClasses\": true");
+ case DECLARED_CLASSES -> builder.append("\"allDeclaredClasses\": true");
+ }
+ if (categoryIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ }
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java
new file mode 100644
index 0000000000..d0b9f9a6f5
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.springframework.aot.hint.ResourceBundleHint;
+import org.springframework.aot.hint.ResourceHints;
+import org.springframework.aot.hint.ResourcePatternHint;
+
+/**
+ * Serialize a {@link ResourceHints} to the JSON file expected by GraalVM {@code native-image} compiler,
+ * typically named {@code resource-config.json}.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ * @see Accessing Resources in Native Images
+ * @see Native Image Build Configuration
+ */
+class ResourceHintsSerializer {
+
+ public String serialize(ResourceHints hints) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("{\n\"resources\" : {\n");
+ serializeInclude(hints, builder);
+ serializeExclude(hints, builder);
+ builder.append("},\n");
+ serializeBundles(hints, builder);
+ builder.append("}\n");
+ return builder.toString();
+ }
+
+ private void serializeInclude(ResourceHints hints, StringBuilder builder) {
+ builder.append("\"includes\" : [\n");
+ Iterator patternIterator = hints.resourcePatterns().iterator();
+ while (patternIterator.hasNext()) {
+ ResourcePatternHint hint = patternIterator.next();
+ Iterator includeIterator = hint.getIncludes().iterator();
+ while (includeIterator.hasNext()) {
+ String pattern = JsonUtils.escape(patternToRegexp(includeIterator.next()));
+ builder.append("{ \"pattern\": \"").append(pattern).append("\" }");
+ if (includeIterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ if (patternIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("\n],\n");
+ }
+
+ private void serializeExclude(ResourceHints hints, StringBuilder builder) {
+ builder.append("\"excludes\" : [\n");
+ Iterator patternIterator = hints.resourcePatterns().iterator();
+ while (patternIterator.hasNext()) {
+ ResourcePatternHint hint = patternIterator.next();
+ Iterator excludeIterator = hint.getExcludes().iterator();
+ while (excludeIterator.hasNext()) {
+ String pattern = JsonUtils.escape(patternToRegexp(excludeIterator.next()));
+ builder.append("{ \"pattern\": \"").append(pattern).append("\" }");
+ if (excludeIterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ if (patternIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("\n]\n");
+ }
+
+ private void serializeBundles(ResourceHints hints, StringBuilder builder) {
+ builder.append("\"bundles\" : [\n");
+ Iterator bundleIterator = hints.resourceBundles().iterator();
+ while (bundleIterator.hasNext()) {
+ String baseName = JsonUtils.escape(bundleIterator.next().getBaseName());
+ builder.append("{ \"name\": \"").append(baseName).append("\" }");
+ if (bundleIterator.hasNext()) {
+ builder.append(",\n");
+ }
+ }
+ builder.append("]\n");
+ }
+
+ private String patternToRegexp(String pattern) {
+ return Arrays.stream(pattern.split("\\*")).map(Pattern::quote).collect(Collectors.joining(".*"));
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java b/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java
new file mode 100644
index 0000000000..4cbc506572
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Support for generating GraalVM native configuration from runtime hints.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.aot.nativex;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java
new file mode 100644
index 0000000000..8374b76a50
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.JavaSerializationHints;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ProxyHints;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.ResourceHints;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.core.codec.StringDecoder;
+import org.springframework.util.MimeType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link FileNativeConfigurationGenerator}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class FileNativeConfigurationGeneratorTests {
+
+ @TempDir
+ static Path tempDir;
+
+ @Test
+ void emptyConfig() {
+ Path empty = tempDir.resolve("empty");
+ FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(empty);
+ generator.generate(new RuntimeHints());
+ assertThat(empty.toFile().listFiles()).isNull();
+ }
+
+ @Test
+ void serializationConfig() throws IOException, JSONException {
+ FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
+ RuntimeHints hints = new RuntimeHints();
+ JavaSerializationHints serializationHints = hints.javaSerialization();
+ serializationHints.registerType(Integer.class);
+ serializationHints.registerType(Long.class);
+ generator.generate(hints);
+ assertEquals("""
+ [
+ { "name" : "java.lang.Integer" },
+ { "name" : "java.lang.Long" }
+ ]""", "serialization-config.json");
+ }
+
+ @Test
+ void proxyConfig() throws IOException, JSONException {
+ FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
+ RuntimeHints hints = new RuntimeHints();
+ ProxyHints proxyHints = hints.proxies();
+ proxyHints.registerJdkProxy(Function.class);
+ proxyHints.registerJdkProxy(Function.class, Consumer.class);
+ generator.generate(hints);
+ assertEquals("""
+ [
+ { "interfaces" : [ "java.util.function.Function" ] },
+ { "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] }
+ ]""", "proxy-config.json");
+ }
+
+ @Test
+ void reflectionConfig() throws IOException, JSONException {
+ FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
+ RuntimeHints hints = new RuntimeHints();
+ ReflectionHints reflectionHints = hints.reflection();
+ reflectionHints.registerType(StringDecoder.class, builder -> {
+ builder
+ .onReachableType(TypeReference.of(String.class))
+ .withMembers(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS,
+ MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS,
+ MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS,
+ MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES)
+ .withField("DEFAULT_CHARSET", fieldBuilder -> {})
+ .withField("defaultCharset", fieldBuilder -> {
+ fieldBuilder.allowWrite(true);
+ fieldBuilder.allowUnsafeAccess(true);
+ })
+ .withConstructor(List.of(TypeReference.of(List.class), TypeReference.of(boolean.class), TypeReference.of(MimeType.class)), constructorHint ->
+ constructorHint.withMode(ExecutableMode.INTROSPECT))
+ .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ctorBuilder -> {})
+ .withMethod("getDefaultCharset", Collections.emptyList(), constructorHint ->
+ constructorHint.withMode(ExecutableMode.INTROSPECT));
+ });
+ generator.generate(hints);
+ assertEquals("""
+ [
+ {
+ "name" : "org.springframework.core.codec.StringDecoder",
+ "condition" : { "typeReachable" : "java.lang.String" },
+ "allPublicFields" : true,
+ "allDeclaredFields" : true,
+ "queryAllPublicConstructors" : true,
+ "queryAllDeclaredConstructors" : true,
+ "allPublicConstructors" : true,
+ "allDeclaredConstructors" : true,
+ "queryAllPublicMethods" : true,
+ "queryAllDeclaredMethods" : true,
+ "allPublicMethods" : true,
+ "allDeclaredMethods" : true,
+ "allPublicClasses" : true,
+ "allDeclaredClasses" : true,
+ "fields" : [
+ { "name" : "DEFAULT_CHARSET" },
+ { "name" : "defaultCharset", "allowWrite" = true, "allowUnsafeAccess" = true }
+ ],
+ "methods" : [
+ { "name" : "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
+ ],
+ "queriedMethods" : [
+ { "name" : "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
+ { "name" : "getDefaultCharset" }
+ ]
+ }
+ ]""", "reflect-config.json");
+ }
+
+ @Test
+ void resourceConfig() throws IOException, JSONException {
+ FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
+ RuntimeHints hints = new RuntimeHints();
+ ResourceHints resourceHints = hints.resources();
+ resourceHints.registerPattern("com/example/test.properties");
+ resourceHints.registerPattern("com/example/another.properties");
+ generator.generate(hints);
+ assertEquals("""
+ {
+ "resources": {
+ "includes" : [
+ {"pattern" : "\\\\Qcom/example/test.properties\\\\E"},
+ {"pattern" : "\\\\Qcom/example/another.properties\\\\E"}
+ ]
+ }
+ }""", "resource-config.json");
+ }
+
+ @Test
+ void namespace() {
+ String groupId = "foo.bar";
+ String artifactId = "baz";
+ String filename = "resource-config.json";
+ FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir, groupId, artifactId);
+ RuntimeHints hints = new RuntimeHints();
+ ResourceHints resourceHints = hints.resources();
+ resourceHints.registerPattern("com/example/test.properties");
+ generator.generate(hints);
+ Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename);
+ assertThat(jsonFile.toFile().exists()).isTrue();
+ }
+
+ 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));
+ JSONAssert.assertEquals(expectedString, content, JSONCompareMode.LENIENT);
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java
new file mode 100644
index 0000000000..fc494b7c12
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.aot.hint.JavaSerializationHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.core.env.Environment;
+
+/**
+ * Tests for {@link JavaSerializationHintsSerializer}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class JavaSerializationHintsSerializerTests {
+
+ private final JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
+
+ @Test
+ void empty() throws JSONException {
+ JavaSerializationHints hints = new JavaSerializationHints();
+ assertEquals("[]", hints);
+ }
+
+ @Test
+ void one() throws JSONException {
+ JavaSerializationHints hints = new JavaSerializationHints().registerType(TypeReference.of(String.class));
+ assertEquals("""
+ [
+ { "name" : "java.lang.String" }
+ ]""", hints);
+ }
+
+ @Test
+ void two() throws JSONException {
+ JavaSerializationHints hints = new JavaSerializationHints()
+ .registerType(TypeReference.of(String.class))
+ .registerType(TypeReference.of(Environment.class));
+ assertEquals("""
+ [
+ { "name" : "java.lang.String" },
+ { "name" : "org.springframework.core.env.Environment" }
+ ]""", hints);
+ }
+
+ private void assertEquals(String expectedString, JavaSerializationHints hints) throws JSONException {
+ JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java
new file mode 100644
index 0000000000..1773b8c35c
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link JsonUtils}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class JsonUtilsTests {
+
+ @Test
+ void unescaped() {
+ assertThat(JsonUtils.escape("azerty")).isEqualTo("azerty");
+ }
+
+ @Test
+ void escapeDoubleQuote() {
+ assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
+ }
+
+ @Test
+ void escapeBackslash() {
+ assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
+ }
+
+ @Test
+ void escapeBackspace() {
+ assertThat(JsonUtils.escape("foo\bbar")).isEqualTo("foo\\bbar");
+ }
+
+ @Test
+ void escapeFormfeed() {
+ assertThat(JsonUtils.escape("foo\fbar")).isEqualTo("foo\\fbar");
+ }
+
+ @Test
+ void escapeNewline() {
+ assertThat(JsonUtils.escape("foo\nbar")).isEqualTo("foo\\nbar");
+ }
+
+ @Test
+ void escapeCarriageReturn() {
+ assertThat(JsonUtils.escape("foo\rbar")).isEqualTo("foo\\rbar");
+ }
+
+ @Test
+ void escapeTab() {
+ assertThat(JsonUtils.escape("foo\tbar")).isEqualTo("foo\\tbar");
+ }
+
+ @Test
+ void escapeUnicode() {
+ assertThat(JsonUtils.escape("foo\u001Fbar")).isEqualTo("foo\\u001fbar");
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java
new file mode 100644
index 0000000000..1f60446e75
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.aot.hint.ProxyHints;
+
+/**
+ * Tests for {@link ProxyHintsSerializer}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class ProxyHintsSerializerTests {
+
+ private final ProxyHintsSerializer serializer = new ProxyHintsSerializer();
+
+ @Test
+ void empty() throws JSONException {
+ ProxyHints hints = new ProxyHints();
+ assertEquals("[]", hints);
+ }
+
+ @Test
+ void one() throws JSONException {
+ ProxyHints hints = new ProxyHints();
+ hints.registerJdkProxy(Function.class);
+ assertEquals("""
+ [
+ { "interfaces" : [ "java.util.function.Function" ] }
+ ]""", hints);
+ }
+
+ @Test
+ void two() throws JSONException {
+ ProxyHints hints = new ProxyHints();
+ hints.registerJdkProxy(Function.class);
+ hints.registerJdkProxy(Function.class, Consumer.class);
+ assertEquals("""
+ [
+ { "interfaces" : [ "java.util.function.Function" ] },
+ { "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] }
+ ]""", hints);
+ }
+
+ private void assertEquals(String expectedString, ProxyHints hints) throws JSONException {
+
+ JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java
new file mode 100644
index 0000000000..00f887add2
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.List;
+
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.core.codec.StringDecoder;
+import org.springframework.util.MimeType;
+
+/**
+ * Tests for {@link ReflectionHintsSerializer}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class ReflectionHintsSerializerTests {
+
+ private final ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
+
+ @Test
+ void empty() throws JSONException {
+ ReflectionHints hints = new ReflectionHints();
+ assertEquals("[]", hints);
+ }
+
+ @Test
+ void one() throws JSONException {
+ ReflectionHints hints = new ReflectionHints();
+ hints.registerType(StringDecoder.class, builder -> {
+ builder
+ .onReachableType(TypeReference.of(String.class))
+ .withMembers(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS,
+ MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS,
+ MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS,
+ MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES)
+ .withField("DEFAULT_CHARSET", fieldBuilder -> {})
+ .withField("defaultCharset", fieldBuilder -> {
+ fieldBuilder.allowWrite(true);
+ fieldBuilder.allowUnsafeAccess(true);
+ })
+ .withConstructor(List.of(TypeReference.of(List.class), TypeReference.of(boolean.class), TypeReference.of(MimeType.class)), constructorHint ->
+ constructorHint.withMode(ExecutableMode.INTROSPECT))
+ .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ctorBuilder -> {})
+ .withMethod("getDefaultCharset", Collections.emptyList(), constructorHint ->
+ constructorHint.withMode(ExecutableMode.INTROSPECT));
+ });
+ assertEquals("""
+ [
+ {
+ "name" : "org.springframework.core.codec.StringDecoder",
+ "condition" : { "typeReachable" : "java.lang.String" },
+ "allPublicFields" : true,
+ "allDeclaredFields" : true,
+ "queryAllPublicConstructors" : true,
+ "queryAllDeclaredConstructors" : true,
+ "allPublicConstructors" : true,
+ "allDeclaredConstructors" : true,
+ "queryAllPublicMethods" : true,
+ "queryAllDeclaredMethods" : true,
+ "allPublicMethods" : true,
+ "allDeclaredMethods" : true,
+ "allPublicClasses" : true,
+ "allDeclaredClasses" : true,
+ "fields" : [
+ { "name" : "DEFAULT_CHARSET" },
+ { "name" : "defaultCharset", "allowWrite" = true, "allowUnsafeAccess" = true }
+ ],
+ "methods" : [
+ { "name" : "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
+ ],
+ "queriedMethods" : [
+ { "name" : "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
+ { "name" : "getDefaultCharset" }
+ ]
+ }
+ ]""", hints);
+ }
+
+ @Test
+ void two() throws JSONException {
+ ReflectionHints hints = new ReflectionHints();
+ hints.registerType(Integer.class, builder -> {
+ });
+ hints.registerType(Long.class, builder -> {
+ });
+
+ assertEquals("""
+ [
+ { "name" : "java.lang.Integer" },
+ { "name" : "java.lang.Long" }
+ ]""", hints);
+ }
+
+ private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException {
+ JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java
new file mode 100644
index 0000000000..e64163558c
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.io.IOException;
+
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.aot.hint.ResourceHints;
+
+/**
+ * Tests for {@link ResourceHintsSerializer}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class ResourceHintsSerializerTests {
+
+ private final ResourceHintsSerializer serializer = new ResourceHintsSerializer();
+
+ @Test
+ void empty() throws IOException, JSONException {
+ ResourceHints hints = new ResourceHints();
+ assertEquals("{}", hints);
+ }
+
+ @Test
+ void registerExactMatch() throws JSONException {
+ ResourceHints hints = new ResourceHints();
+ hints.registerPattern("com/example/test.properties");
+ hints.registerPattern("com/example/another.properties");
+ assertEquals("""
+ {
+ "resources": {
+ "includes": [
+ { "pattern" : "\\\\Qcom/example/test.properties\\\\E"},
+ { "pattern" : "\\\\Qcom/example/another.properties\\\\E"}
+ ]
+ }
+ }""", hints);
+ }
+
+ @Test
+ void registerPattern() throws JSONException {
+ ResourceHints hints = new ResourceHints();
+ hints.registerPattern("com/example/*.properties");
+ assertEquals("""
+ {
+ "resources": {
+ "includes" : [
+ { "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}
+ ]
+ }
+ }""", hints);
+ }
+
+ @Test
+ void registerPatternWithIncludesAndExcludes() throws JSONException {
+ ResourceHints hints = new ResourceHints();
+ hints.registerPattern("com/example/*.properties", hint -> hint.excludes("com/example/to-ignore.properties"));
+ hints.registerPattern("org/example/*.properties", hint -> hint.excludes("org/example/to-ignore.properties"));
+ assertEquals("""
+ {
+ "resources": {
+ "includes": [
+ { "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
+ { "pattern" : "\\\\Qorg/example/\\\\E.*\\\\Q.properties\\\\E"}
+ ],
+ "excludes": [
+ { "pattern" : "\\\\Qcom/example/to-ignore.properties\\\\E"},
+ { "pattern" : "\\\\Qorg/example/to-ignore.properties\\\\E"}
+ ]
+ }
+ }""", hints);
+ }
+
+ @Test
+ void registerType() throws JSONException {
+ ResourceHints hints = new ResourceHints();
+ hints.registerType(String.class);
+ assertEquals("""
+ {
+ "resources": {
+ "includes" : [
+ { "pattern" : "\\\\Qjava/lang/String.class\\\\E"}
+ ]
+ }
+ }""", hints);
+ }
+
+ @Test
+ void registerResourceBundle() throws JSONException {
+ ResourceHints hints = new ResourceHints();
+ hints.registerResourceBundle("com.example.message");
+ hints.registerResourceBundle("com.example.message2");
+ assertEquals("""
+ {
+ "bundles": [
+ { "name" : "com.example.message"},
+ { "name" : "com.example.message2"}
+ ]
+ }""", hints);
+ }
+
+ private void assertEquals(String expectedString, ResourceHints hints) throws JSONException {
+ JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
+ }
+
+}