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 index 8c4549164d5..1082e1dce68 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java @@ -16,35 +16,36 @@ package org.springframework.aot.nativex; -import java.util.Iterator; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.Map; 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}. + * Serialize a {@link JavaSerializationHints} to the JSON output expected by the + * GraalVM {@code native-image} compiler, typically named + * {@code serialization-config.json}. * * @author Sebastien Deleuze + * @author Stephane Nicoll * @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(); + StringWriter sw = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(sw, " "); + writer.writeArray(hints.types().map(this::toAttributes).toList()); + return sw.toString(); + } + + private Map toAttributes(TypeReference typeReference) { + LinkedHashMap attributes = new LinkedHashMap<>(); + attributes.put("name", typeReference.getCanonicalName()); + return attributes; } } 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 index 835890e065b..36f51912d12 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java @@ -16,17 +16,21 @@ package org.springframework.aot.nativex; -import java.util.Iterator; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.Map; 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}. + * Serialize {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON + * output expected by the GraalVM {@code native-image} compiler, typically named + * {@code proxy-config.json}. * * @author Sebastien Deleuze + * @author Stephane Nicoll * @since 6.0 * @see Dynamic Proxy in Native Image * @see Native Image Build Configuration @@ -34,27 +38,17 @@ import org.springframework.aot.hint.TypeReference; 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(); + StringWriter sw = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(sw, " "); + writer.writeArray(hints.jdkProxies().map(this::toAttributes).toList()); + return sw.toString(); + } + + private Map toAttributes(JdkProxyHint hint) { + Map attributes = new LinkedHashMap<>(); + attributes.put("interfaces", hint.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).toList()); + return attributes; } } 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 index 6b7661efd82..e3f1dfc30cd 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java @@ -16,8 +16,12 @@ package org.springframework.aot.nativex; -import java.util.Iterator; +import java.io.StringWriter; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.springframework.aot.hint.ExecutableHint; @@ -27,129 +31,100 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; /** - * Serialize {@link ReflectionHints} to the JSON file expected by GraalVM {@code native-image} compiler, - * typically named {@code reflect-config.json}. + * Serialize {@link ReflectionHints} to the JSON output expected by the GraalV + * {@code native-image} compiler, typically named {@code reflect-config.json}. * * @author Sebastien Deleuze + * @author Stephane Nicoll * @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(); + StringWriter sw = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(sw, " "); + writer.writeArray(hints.typeHints().map(this::toAttributes).toList()); + return sw.toString(); + } + + private Map toAttributes(TypeHint hint) { + Map attributes = new LinkedHashMap<>(); + attributes.put("name", hint.getType().getCanonicalName()); + handleCondition(attributes, hint); + handleCategories(attributes, hint.getMemberCategories()); + handleFields(attributes, hint.fields()); + handleExecutables(attributes, Stream.concat(hint.constructors(), hint.methods()).toList()); + return attributes; } - private void serializeCondition(TypeHint hint, StringBuilder builder) { + private void handleCondition(Map attributes, TypeHint hint) { if (hint.getReachableType() != null) { - String name = JsonUtils.escape(hint.getReachableType().getCanonicalName()); - builder.append(",\n\"condition\": { \"typeReachable\": \"").append(name).append("\" }"); + Map conditionAttributes = new LinkedHashMap<>(); + conditionAttributes.put("typeReachable", hint.getReachableType().getCanonicalName()); + attributes.put("condition", conditionAttributes); } } - 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 handleFields(Map attributes, Stream fields) { + addIfNotEmpty(attributes, "fields", fields.map(this::toAttributes).toList()); } - 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); + private Map toAttributes(FieldHint hint) { + Map attributes = new LinkedHashMap<>(); + attributes.put("name", hint.getName()); + if (hint.isAllowWrite()) { + attributes.put("allowWrite", hint.isAllowWrite()); } - if (queriedMethodIterator.hasNext()) { - builder.append(",\n"); - serializeMethods("queriedMethods", queriedMethodIterator, builder); + if (hint.isAllowUnsafeAccess()) { + attributes.put("allowUnsafeAccess", hint.isAllowUnsafeAccess()); } + return attributes; } - 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 handleExecutables(Map attributes, List hints) { + addIfNotEmpty(attributes, "methods", hints.stream() + .filter(h -> h.getModes().contains(ExecutableMode.INVOKE) || h.getModes().isEmpty()) + .map(this::toAttributes).toList()); + addIfNotEmpty(attributes, "queriedMethods", hints.stream() + .filter(h -> h.getModes().contains(ExecutableMode.INTROSPECT)) + .map(this::toAttributes).toList()); } - 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"); + private Map toAttributes(ExecutableHint hint) { + Map attributes = new LinkedHashMap<>(); + attributes.put("name", hint.getName()); + attributes.put("parameterTypes", hint.getParameterTypes().stream().map(TypeReference::getCanonicalName).toList()); + return attributes; + } + + private void handleCategories(Map attributes, Set categories) { + categories.forEach(category -> { + switch (category) { + case PUBLIC_FIELDS -> attributes.put("allPublicFields", true); + case DECLARED_FIELDS -> attributes.put("allDeclaredFields", true); + case INTROSPECT_PUBLIC_CONSTRUCTORS -> attributes.put("queryAllPublicConstructors", true); + case INTROSPECT_DECLARED_CONSTRUCTORS -> attributes.put("queryAllDeclaredConstructors", true); + case INVOKE_PUBLIC_CONSTRUCTORS -> attributes.put("allPublicConstructors", true); + case INVOKE_DECLARED_CONSTRUCTORS -> attributes.put("allDeclaredConstructors", true); + case INTROSPECT_PUBLIC_METHODS -> attributes.put("queryAllPublicMethods", true); + case INTROSPECT_DECLARED_METHODS -> attributes.put("queryAllDeclaredMethods", true); + case INVOKE_PUBLIC_METHODS -> attributes.put("allPublicMethods", true); + case INVOKE_DECLARED_METHODS -> attributes.put("allDeclaredMethods", true); + case PUBLIC_CLASSES -> attributes.put("allPublicClasses", true); + case DECLARED_CLASSES -> attributes.put("allDeclaredClasses", true); + } } - } + ); + } + + private void addIfNotEmpty(Map attributes, String name, @Nullable Object value) { + if (value != null && (value instanceof Collection collection && !collection.isEmpty())) { + attributes.put(name, value); } } 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 index d0b9f9a6f5c..9000d8cb39e 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java @@ -16,20 +16,27 @@ package org.springframework.aot.nativex; +import java.io.StringWriter; import java.util.Arrays; -import java.util.Iterator; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.aot.hint.ResourceBundleHint; import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.ResourcePatternHint; +import org.springframework.lang.Nullable; /** - * Serialize a {@link ResourceHints} to the JSON file expected by GraalVM {@code native-image} compiler, - * typically named {@code resource-config.json}. + * Serialize a {@link ResourceHints} to the JSON output expected by the GraalVM + * {@code native-image} compiler, typically named {@code resource-config.json}. * * @author Sebastien Deleuze + * @author Stephane Nicoll * @since 6.0 * @see Accessing Resources in Native Images * @see Native Image Build Configuration @@ -37,71 +44,48 @@ import org.springframework.aot.hint.ResourcePatternHint; 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(); + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + Map attributes = new LinkedHashMap<>(); + attributes.put("resources", toAttributes(hints)); + handleResourceBundles(attributes, hints.resourceBundles()); + writer.writeObject(attributes); + return out.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 Map toAttributes(ResourceHints hint) { + Map attributes = new LinkedHashMap<>(); + addIfNotEmpty(attributes, "includes", hint.resourcePatterns().map(ResourcePatternHint::getIncludes) + .flatMap(List::stream).distinct().map(this::toAttributes).toList()); + addIfNotEmpty(attributes, "excludes", hint.resourcePatterns().map(ResourcePatternHint::getExcludes) + .flatMap(List::stream).distinct().map(this::toAttributes).toList()); + return attributes; } - 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 handleResourceBundles(Map attributes, Stream ressourceBundles) { + addIfNotEmpty(attributes, "bundles", ressourceBundles.map(this::toAttributes).toList()); } - 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 Map toAttributes(ResourceBundleHint hint) { + Map attributes = new LinkedHashMap<>(); + attributes.put("name", hint.getBaseName()); + return attributes; + } + + private Map toAttributes(String pattern) { + Map attributes = new LinkedHashMap<>(); + attributes.put("pattern", patternToRegexp(pattern)); + return attributes; } private String patternToRegexp(String pattern) { return Arrays.stream(pattern.split("\\*")).map(Pattern::quote).collect(Collectors.joining(".*")); } + private void addIfNotEmpty(Map attributes, String name, @Nullable Object value) { + if (value != null && (value instanceof Collection collection && !collection.isEmpty())) { + attributes.put(name, value); + } + } + }