From 77e0100f42253ab26947cf2258ee9653e2ac811d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 2 Mar 2022 10:53:17 +0100 Subject: [PATCH] Implement GraalVM native JSON configuration generation This commit implements 4 package private Json serializers for JavaSerializationHints, ProxyHints, ReflectionHints and ResourceHints to serialize GraalVM native JSON configuration as documented in https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/. It exposes the related functionality via NativeConfigurationGenerator which allows to generate the relevant files on the filesystem via the FileNativeConfigurationGenerator implementation. The generated *-config.json files have been validated working with GraalVM 22.0. Closes gh-27991 --- spring-core/spring-core.gradle | 1 + .../FileNativeConfigurationGenerator.java | 136 +++++++++++++ .../JavaSerializationHintsSerializer.java | 50 +++++ .../aot/nativex/JsonUtils.java | 69 +++++++ .../nativex/NativeConfigurationGenerator.java | 36 ++++ .../aot/nativex/ProxyHintsSerializer.java | 60 ++++++ .../nativex/ReflectionHintsSerializer.java | 158 ++++++++++++++ .../aot/nativex/ResourceHintsSerializer.java | 107 ++++++++++ .../aot/nativex/package-info.java | 9 + ...FileNativeConfigurationGeneratorTests.java | 192 ++++++++++++++++++ ...JavaSerializationHintsSerializerTests.java | 68 +++++++ .../aot/nativex/JsonUtilsTests.java | 74 +++++++ .../nativex/ProxyHintsSerializerTests.java | 71 +++++++ .../ReflectionHintsSerializerTests.java | 124 +++++++++++ .../nativex/ResourceHintsSerializerTests.java | 125 ++++++++++++ 15 files changed, 1280 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationGenerator.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationGenerator.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/package-info.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java 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); + } + +}