From 1cf112bfa4ba7f75a3db7a0f9ea4716d9434692f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Apr 2022 15:42:35 +0200 Subject: [PATCH 1/5] Harmonize json format in tests --- ...FileNativeConfigurationGeneratorTests.java | 58 +++++++++---------- ...JavaSerializationHintsSerializerTests.java | 6 +- .../nativex/ProxyHintsSerializerTests.java | 6 +- .../ReflectionHintsSerializerTests.java | 48 +++++++-------- .../nativex/ResourceHintsSerializerTests.java | 30 +++++----- 5 files changed, 73 insertions(+), 75 deletions(-) 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 index 8374b76a50d..626e33aa02d 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java @@ -72,8 +72,8 @@ public class FileNativeConfigurationGeneratorTests { generator.generate(hints); assertEquals(""" [ - { "name" : "java.lang.Integer" }, - { "name" : "java.lang.Long" } + { "name": "java.lang.Integer" }, + { "name": "java.lang.Long" } ]""", "serialization-config.json"); } @@ -87,8 +87,8 @@ public class FileNativeConfigurationGeneratorTests { generator.generate(hints); assertEquals(""" [ - { "interfaces" : [ "java.util.function.Function" ] }, - { "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] } + { "interfaces": [ "java.util.function.Function" ] }, + { "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] } ]""", "proxy-config.json"); } @@ -121,30 +121,30 @@ public class FileNativeConfigurationGeneratorTests { 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 } + "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" ] } + "methods": [ + { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } ], - "queriedMethods" : [ - { "name" : "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name" : "getDefaultCharset" } + "queriedMethods": [ + { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, + { "name": "getDefaultCharset" } ] } ]""", "reflect-config.json"); @@ -161,9 +161,9 @@ public class FileNativeConfigurationGeneratorTests { assertEquals(""" { "resources": { - "includes" : [ - {"pattern" : "\\\\Qcom/example/test.properties\\\\E"}, - {"pattern" : "\\\\Qcom/example/another.properties\\\\E"} + "includes": [ + {"pattern": "\\\\Qcom/example/test.properties\\\\E"}, + {"pattern": "\\\\Qcom/example/another.properties\\\\E"} ] } }""", "resource-config.json"); 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 index fc494b7c12b..b0ea9bd2f57 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java @@ -45,7 +45,7 @@ public class JavaSerializationHintsSerializerTests { JavaSerializationHints hints = new JavaSerializationHints().registerType(TypeReference.of(String.class)); assertEquals(""" [ - { "name" : "java.lang.String" } + { "name": "java.lang.String" } ]""", hints); } @@ -56,8 +56,8 @@ public class JavaSerializationHintsSerializerTests { .registerType(TypeReference.of(Environment.class)); assertEquals(""" [ - { "name" : "java.lang.String" }, - { "name" : "org.springframework.core.env.Environment" } + { "name": "java.lang.String" }, + { "name": "org.springframework.core.env.Environment" } ]""", hints); } 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 index 1f60446e755..e31b4e9b29c 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java @@ -47,7 +47,7 @@ public class ProxyHintsSerializerTests { hints.registerJdkProxy(Function.class); assertEquals(""" [ - { "interfaces" : [ "java.util.function.Function" ] } + { "interfaces": [ "java.util.function.Function" ] } ]""", hints); } @@ -58,8 +58,8 @@ public class ProxyHintsSerializerTests { hints.registerJdkProxy(Function.class, Consumer.class); assertEquals(""" [ - { "interfaces" : [ "java.util.function.Function" ] }, - { "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] } + { "interfaces": [ "java.util.function.Function" ] }, + { "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] } ]""", hints); } 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 index 859cd1a5396..508b64a06f6 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java @@ -73,30 +73,30 @@ public class ReflectionHintsSerializerTests { 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 } + "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" ] } + "methods": [ + { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } ], - "queriedMethods" : [ - { "name" : "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name" : "getDefaultCharset" } + "queriedMethods": [ + { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, + { "name": "getDefaultCharset" } ] } ]""", hints); @@ -112,8 +112,8 @@ public class ReflectionHintsSerializerTests { assertEquals(""" [ - { "name" : "java.lang.Integer" }, - { "name" : "java.lang.Long" } + { "name": "java.lang.Integer" }, + { "name": "java.lang.Long" } ]""", hints); } 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 index e64163558c8..075f870ae40 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java @@ -16,8 +16,6 @@ package org.springframework.aot.nativex; -import java.io.IOException; - import org.json.JSONException; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; @@ -35,13 +33,13 @@ public class ResourceHintsSerializerTests { private final ResourceHintsSerializer serializer = new ResourceHintsSerializer(); @Test - void empty() throws IOException, JSONException { + void empty() throws JSONException { ResourceHints hints = new ResourceHints(); assertEquals("{}", hints); } @Test - void registerExactMatch() throws JSONException { + void registerExactMatch() throws JSONException { ResourceHints hints = new ResourceHints(); hints.registerPattern("com/example/test.properties"); hints.registerPattern("com/example/another.properties"); @@ -49,8 +47,8 @@ public class ResourceHintsSerializerTests { { "resources": { "includes": [ - { "pattern" : "\\\\Qcom/example/test.properties\\\\E"}, - { "pattern" : "\\\\Qcom/example/another.properties\\\\E"} + { "pattern": "\\\\Qcom/example/test.properties\\\\E"}, + { "pattern": "\\\\Qcom/example/another.properties\\\\E"} ] } }""", hints); @@ -63,8 +61,8 @@ public class ResourceHintsSerializerTests { assertEquals(""" { "resources": { - "includes" : [ - { "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"} + "includes": [ + { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"} ] } }""", hints); @@ -79,12 +77,12 @@ public class ResourceHintsSerializerTests { { "resources": { "includes": [ - { "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, - { "pattern" : "\\\\Qorg/example/\\\\E.*\\\\Q.properties\\\\E"} + { "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"} + { "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"}, + { "pattern": "\\\\Qorg/example/to-ignore.properties\\\\E"} ] } }""", hints); @@ -97,8 +95,8 @@ public class ResourceHintsSerializerTests { assertEquals(""" { "resources": { - "includes" : [ - { "pattern" : "\\\\Qjava/lang/String.class\\\\E"} + "includes": [ + { "pattern": "\\\\Qjava/lang/String.class\\\\E"} ] } }""", hints); @@ -112,8 +110,8 @@ public class ResourceHintsSerializerTests { assertEquals(""" { "bundles": [ - { "name" : "com.example.message"}, - { "name" : "com.example.message2"} + { "name": "com.example.message"}, + { "name": "com.example.message2"} ] }""", hints); } From f24369b49c7f48a4ca4cc0f56dee4a85b2b57dcf Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Apr 2022 15:43:26 +0200 Subject: [PATCH 2/5] Introduce basic json writer for native configuration processing --- .../aot/nativex/BasicJsonWriter.java | 297 ++++++++++++++++++ .../aot/nativex/JsonUtils.java | 69 ---- .../aot/nativex/BasicJsonWriterTests.java | 190 +++++++++++ .../aot/nativex/JsonUtilsTests.java | 74 ----- 4 files changed, 487 insertions(+), 143 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java delete mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java delete mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java new file mode 100644 index 00000000000..4fa022252ae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java @@ -0,0 +1,297 @@ +/* + * 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.io.Writer; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Very basic json writer for the purposes of translating runtime hints to native + * configuration. + * + * @author Stephane Nicoll + */ +class BasicJsonWriter { + + private final IndentingWriter writer; + + /** + * Create a new instance with the specified indent value. + * @param writer the writer to use + * @param singleIndent the value of one indent + */ + public BasicJsonWriter(Writer writer, String singleIndent) { + this.writer = new IndentingWriter(writer, singleIndent); + } + + /** + * Create a new instance using two whitespaces for the indent. + * @param writer the writer to use + */ + public BasicJsonWriter(Writer writer) { + this(writer, " "); + } + + + /** + * Write an object with the specified attributes. Each attribute is + * written according to its value type: + *
    + *
  • Map: write the value as a nested object
  • + *
  • List: write the value as a nested array
  • + *
  • Otherwise, write a single value
  • + *
+ * @param attributes the attributes of the object + */ + public void writeObject(Map attributes) { + writeObject(attributes, true); + } + + /** + * Write an array with the specified items. Each item in the + * list is written either as a nested object or as an attribute + * depending on its type. + * @param items the items to write + * @see #writeObject(Map) + */ + public void writeArray(List items) { + writeArray(items, true); + } + + private void writeObject(Map attributes, boolean newLine) { + if (attributes.isEmpty()) { + this.writer.print("{ }"); + } + else { + this.writer.println("{").indented(writeAll(attributes.entrySet().iterator(), + entry -> writeAttribute(entry.getKey(), entry.getValue()))).print("}"); + } + if (newLine) { + this.writer.println(); + } + } + + private void writeArray(List items, boolean newLine) { + if (items.isEmpty()) { + this.writer.print("[ ]"); + } + else { + this.writer.println("[") + .indented(writeAll(items.iterator(), this::writeValue)).print("]"); + } + if (newLine) { + this.writer.println(); + } + } + + private Runnable writeAll(Iterator it, Consumer writer) { + return () -> { + while (it.hasNext()) { + writer.accept(it.next()); + if (it.hasNext()) { + this.writer.println(","); + } + else { + this.writer.println(); + } + } + }; + } + + private void writeAttribute(String name, Object value) { + this.writer.print(quote(name) + ": "); + writeValue(value); + } + + @SuppressWarnings("unchecked") + private void writeValue(Object value) { + if (value instanceof Map map) { + writeObject((Map) map, false); + } + else if (value instanceof List list) { + writeArray(list, false); + } + else if (value instanceof CharSequence string) { + this.writer.print(quote(escape(string))); + } + else if (value instanceof Boolean flag) { + this.writer.print(Boolean.toString(flag)); + } + else { + throw new IllegalStateException("unsupported type: " + value.getClass()); + } + } + + private String quote(String name) { + return "\"" + name + "\""; + } + + private static String escape(CharSequence 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(); + } + + + static class IndentingWriter extends Writer { + + private final Writer out; + + private final String singleIndent; + + private int level = 0; + + private String currentIndent = ""; + + private boolean prependIndent = false; + + IndentingWriter(Writer out, String singleIndent) { + this.out = out; + this.singleIndent = singleIndent; + } + + /** + * Write the specified text. + * @param string the content to write + */ + public IndentingWriter print(String string) { + write(string.toCharArray(), 0, string.length()); + return this; + } + + /** + * Write the specified text and append a new line. + * @param string the content to write + */ + public IndentingWriter println(String string) { + write(string.toCharArray(), 0, string.length()); + return println(); + } + + /** + * Write a new line. + */ + public IndentingWriter println() { + String separator = System.lineSeparator(); + try { + this.out.write(separator.toCharArray(), 0, separator.length()); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + this.prependIndent = true; + return this; + } + + /** + * Increase the indentation level and execute the {@link Runnable}. Decrease the + * indentation level on completion. + * @param runnable the code to execute withing an extra indentation level + */ + public IndentingWriter indented(Runnable runnable) { + indent(); + runnable.run(); + return outdent(); + } + + /** + * Increase the indentation level. + */ + private IndentingWriter indent() { + this.level++; + return refreshIndent(); + } + + /** + * Decrease the indentation level. + */ + private IndentingWriter outdent() { + this.level--; + return refreshIndent(); + } + + private IndentingWriter refreshIndent() { + this.currentIndent = this.singleIndent.repeat(Math.max(0, this.level)); + return this; + } + + @Override + public void write(char[] chars, int offset, int length) { + try { + if (this.prependIndent) { + this.out.write(this.currentIndent.toCharArray(), 0, this.currentIndent.length()); + this.prependIndent = false; + } + this.out.write(chars, offset, length); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void flush() throws IOException { + this.out.flush(); + } + + @Override + public void close() throws IOException { + this.out.close(); + } + + } + +} 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 deleted file mode 100644 index 15ed1b58e6d..00000000000 --- a/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java new file mode 100644 index 00000000000..4545c9caa9c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java @@ -0,0 +1,190 @@ +/* + * 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.StringWriter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BasicJsonWriter}. + * + * @author Stephane Nicoll + */ +class BasicJsonWriterTests { + + private final StringWriter out = new StringWriter(); + + private final BasicJsonWriter json = new BasicJsonWriter(out, "\t"); + + @Test + void writeObject() { + Map attributes = orderedMap("test", "value"); + attributes.put("another", true); + this.json.writeObject(attributes); + assertThat(out.toString()).isEqualTo(""" + { + "test": "value", + "another": true + } + """); + } + + @Test + void writeObjectWitNestedObject() { + Map attributes = orderedMap("test", "value"); + attributes.put("nested", orderedMap("enabled", false)); + this.json.writeObject(attributes); + assertThat(out.toString()).isEqualTo(""" + { + "test": "value", + "nested": { + "enabled": false + } + } + """); + } + + @Test + void writeObjectWitNestedArrayOfString() { + Map attributes = orderedMap("test", "value"); + attributes.put("nested", List.of("test", "value", "another")); + this.json.writeObject(attributes); + assertThat(out.toString()).isEqualTo(""" + { + "test": "value", + "nested": [ + "test", + "value", + "another" + ] + } + """); + } + + @Test + void writeObjectWitNestedArrayOfObject() { + Map attributes = orderedMap("test", "value"); + LinkedHashMap secondNested = orderedMap("name", "second"); + secondNested.put("enabled", false); + attributes.put("nested", List.of(orderedMap("name", "first"), secondNested, orderedMap("name", "third"))); + this.json.writeObject(attributes); + assertThat(out.toString()).isEqualTo(""" + { + "test": "value", + "nested": [ + { + "name": "first" + }, + { + "name": "second", + "enabled": false + }, + { + "name": "third" + } + ] + } + """); + } + + @Test + void writeObjectWithNestedEmptyArray() { + Map attributes = orderedMap("test", "value"); + attributes.put("nested", Collections.emptyList()); + this.json.writeObject(attributes); + assertThat(out.toString()).isEqualTo(""" + { + "test": "value", + "nested": [ ] + } + """); + } + + @Test + void writeObjectWithNestedEmptyObject() { + Map attributes = orderedMap("test", "value"); + attributes.put("nested", Collections.emptyMap()); + this.json.writeObject(attributes); + assertThat(out.toString()).isEqualTo(""" + { + "test": "value", + "nested": { } + } + """); + } + + @Test + void writeWithEscapeDoubleQuote() { + assertEscapedValue("foo\"bar", "foo\\\"bar"); + } + + @Test + void writeWithEscapeBackslash() { + assertEscapedValue("foo\"bar", "foo\\\"bar"); + } + + @Test + void writeWithEscapeBackspace() { + assertEscapedValue("foo\bbar", "foo\\bbar"); + } + + @Test + void writeWithEscapeFormFeed() { + assertEscapedValue("foo\fbar", "foo\\fbar"); + } + + @Test + void writeWithEscapeNewline() { + assertEscapedValue("foo\nbar", "foo\\nbar"); + } + + @Test + void writeWithEscapeCarriageReturn() { + assertEscapedValue("foo\rbar", "foo\\rbar"); + } + + @Test + void writeWithEscapeTab() { + assertEscapedValue("foo\tbar", "foo\\tbar"); + } + + @Test + void writeWithEscapeUnicode() { + assertEscapedValue("foo\u001Fbar", "foo\\u001fbar"); + } + + void assertEscapedValue(String value, String expectedEscapedValue) { + Map attributes = new LinkedHashMap<>(); + attributes.put("test", value); + this.json.writeObject(attributes); + assertThat(out.toString()).contains("\"test\": \"" + expectedEscapedValue + "\""); + } + + private static LinkedHashMap orderedMap(String key, Object value) { + LinkedHashMap map = new LinkedHashMap<>(); + map.put(key, value); + return map; + } + +} 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 deleted file mode 100644 index 1773b8c35c0..00000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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"); - } -} From 4e9306fa1b68171bb58d9190acf50a14a9fa6dc2 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Apr 2022 15:46:57 +0200 Subject: [PATCH 3/5] Adapt to BasicJsonWriter --- .../JavaSerializationHintsSerializer.java | 33 ++-- .../aot/nativex/ProxyHintsSerializer.java | 42 ++--- .../nativex/ReflectionHintsSerializer.java | 171 ++++++++---------- .../aot/nativex/ResourceHintsSerializer.java | 100 +++++----- 4 files changed, 150 insertions(+), 196 deletions(-) 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); + } + } + } From f65136dd684711bbd4c1b2bb3b3493f966b48983 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Apr 2022 16:17:42 +0200 Subject: [PATCH 4/5] Harmonize the use of Writer rather than Generator --- .../FileNativeConfigurationGenerator.java | 136 ------------------ .../FileNativeConfigurationWriter.java | 86 +++++++++++ ...java => JavaSerializationHintsWriter.java} | 12 +- .../nativex/NativeConfigurationGenerator.java | 36 ----- .../nativex/NativeConfigurationWriter.java | 85 +++++++++++ ...sSerializer.java => ProxyHintsWriter.java} | 12 +- ...alizer.java => ReflectionHintsWriter.java} | 12 +- ...rializer.java => ResourceHintsWriter.java} | 12 +- ...> FileNativeConfigurationWriterTests.java} | 28 ++-- ...=> JavaSerializationHintsWriterTests.java} | 13 +- ...rTests.java => ProxyHintsWriterTests.java} | 13 +- ...s.java => ReflectionHintsWriterTests.java} | 13 +- ...sts.java => ResourceHintsWriterTests.java} | 13 +- 13 files changed, 236 insertions(+), 235 deletions(-) delete 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/FileNativeConfigurationWriter.java rename spring-core/src/main/java/org/springframework/aot/nativex/{JavaSerializationHintsSerializer.java => JavaSerializationHintsWriter.java} (81%) delete 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/NativeConfigurationWriter.java rename spring-core/src/main/java/org/springframework/aot/nativex/{ProxyHintsSerializer.java => ProxyHintsWriter.java} (84%) rename spring-core/src/main/java/org/springframework/aot/nativex/{ReflectionHintsSerializer.java => ReflectionHintsWriter.java} (94%) rename spring-core/src/main/java/org/springframework/aot/nativex/{ResourceHintsSerializer.java => ResourceHintsWriter.java} (91%) rename spring-core/src/test/java/org/springframework/aot/nativex/{FileNativeConfigurationGeneratorTests.java => FileNativeConfigurationWriterTests.java} (88%) rename spring-core/src/test/java/org/springframework/aot/nativex/{JavaSerializationHintsSerializerTests.java => JavaSerializationHintsWriterTests.java} (83%) rename spring-core/src/test/java/org/springframework/aot/nativex/{ProxyHintsSerializerTests.java => ProxyHintsWriterTests.java} (84%) rename spring-core/src/test/java/org/springframework/aot/nativex/{ReflectionHintsSerializerTests.java => ReflectionHintsWriterTests.java} (94%) rename spring-core/src/test/java/org/springframework/aot/nativex/{ResourceHintsSerializerTests.java => ResourceHintsWriterTests.java} (90%) 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 deleted file mode 100644 index 8f2f6de3e8e..00000000000 --- a/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationGenerator.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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/FileNativeConfigurationWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationWriter.java new file mode 100644 index 00000000000..ae35c5ac634 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationWriter.java @@ -0,0 +1,86 @@ +/* + * 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.io.Writer; +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.springframework.lang.Nullable; + +/** + * A {@link NativeConfigurationWriter} implementation that writes the + * configuration to disk. + * + * @author Sebastien Deleuze + * @author Stephane Nicoll + * @since 6.0 + * @see Native Image Build Configuration + */ +public class FileNativeConfigurationWriter extends NativeConfigurationWriter { + + private final Path basePath; + + private final String groupId; + + private final String artifactId; + + public FileNativeConfigurationWriter(Path basePath) { + this(basePath, null, null); + } + + public FileNativeConfigurationWriter(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 + protected void writeTo(String fileName, Consumer writer) { + try { + File file = createIfNecessary(fileName); + try (FileWriter out = new FileWriter(file)) { + writer.accept(createJsonWriter(out)); + } + } + catch (IOException ex) { + throw new IllegalStateException("Failed to write native configuration for " + fileName, ex); + } + } + + 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; + } + + private BasicJsonWriter createJsonWriter(Writer out) { + return new BasicJsonWriter(out); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsWriter.java similarity index 81% rename from spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java rename to spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsWriter.java index 1082e1dce68..cebea99397c 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/JavaSerializationHintsWriter.java @@ -16,7 +16,6 @@ package org.springframework.aot.nativex; -import java.io.StringWriter; import java.util.LinkedHashMap; import java.util.Map; @@ -24,7 +23,7 @@ import org.springframework.aot.hint.JavaSerializationHints; import org.springframework.aot.hint.TypeReference; /** - * Serialize a {@link JavaSerializationHints} to the JSON output expected by the + * Write a {@link JavaSerializationHints} to the JSON output expected by the * GraalVM {@code native-image} compiler, typically named * {@code serialization-config.json}. * @@ -33,13 +32,12 @@ import org.springframework.aot.hint.TypeReference; * @since 6.0 * @see Native Image Build Configuration */ -class JavaSerializationHintsSerializer { +class JavaSerializationHintsWriter { - public String serialize(JavaSerializationHints hints) { - StringWriter sw = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(sw, " "); + public static final JavaSerializationHintsWriter INSTANCE = new JavaSerializationHintsWriter(); + + public void write(BasicJsonWriter writer, JavaSerializationHints hints) { writer.writeArray(hints.types().map(this::toAttributes).toList()); - return sw.toString(); } private Map toAttributes(TypeReference typeReference) { 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 deleted file mode 100644 index 841794108fb..00000000000 --- a/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationGenerator.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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/NativeConfigurationWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java new file mode 100644 index 00000000000..143f708119f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java @@ -0,0 +1,85 @@ +/* + * 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 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; + +/** + * Write {@link RuntimeHints} as GraalVM native configuration. + * + * @author Sebastien Deleuze + * @author Stephane Nicoll + * @since 6.0 + * @see Native Image Build Configuration + */ +public abstract class NativeConfigurationWriter { + + /** + * Write the GraalVM native configuration from the provided hints. + * @param hints the hints to handle + */ + public void write(RuntimeHints hints) { + if (hints.javaSerialization().types().findAny().isPresent()) { + writeJavaSerializationHints(hints.javaSerialization()); + } + if (hints.proxies().jdkProxies().findAny().isPresent()) { + writeProxyHints(hints.proxies()); + } + if (hints.reflection().typeHints().findAny().isPresent()) { + writeReflectionHints(hints.reflection()); + } + if (hints.resources().resourcePatterns().findAny().isPresent() || + hints.resources().resourceBundles().findAny().isPresent()) { + writeResourceHints(hints.resources()); + } + } + + /** + * Write the specified GraalVM native configuration file, using the + * provided {@link BasicJsonWriter}. + * @param fileName the name of the file + * @param writer a consumer for the writer to use + */ + protected abstract void writeTo(String fileName, Consumer writer); + + private void writeJavaSerializationHints(JavaSerializationHints hints) { + writeTo("serialization-config.json", writer -> + JavaSerializationHintsWriter.INSTANCE.write(writer, hints)); + } + + private void writeProxyHints(ProxyHints hints) { + writeTo("proxy-config.json", writer -> + ProxyHintsWriter.INSTANCE.write(writer, hints)); + } + + private void writeReflectionHints(ReflectionHints hints) { + writeTo("reflect-config.json", writer -> + ReflectionHintsWriter.INSTANCE.write(writer, hints)); + } + + private void writeResourceHints(ResourceHints hints) { + writeTo("resource-config.json", writer -> + ResourceHintsWriter.INSTANCE.write(writer, 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/ProxyHintsWriter.java similarity index 84% rename from spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java index 36f51912d12..9def944c0dd 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java @@ -16,7 +16,6 @@ package org.springframework.aot.nativex; -import java.io.StringWriter; import java.util.LinkedHashMap; import java.util.Map; @@ -25,7 +24,7 @@ import org.springframework.aot.hint.ProxyHints; import org.springframework.aot.hint.TypeReference; /** - * Serialize {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON + * Write {@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}. * @@ -35,13 +34,12 @@ import org.springframework.aot.hint.TypeReference; * @see Dynamic Proxy in Native Image * @see Native Image Build Configuration */ -class ProxyHintsSerializer { +class ProxyHintsWriter { - public String serialize(ProxyHints hints) { - StringWriter sw = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(sw, " "); + public static final ProxyHintsWriter INSTANCE = new ProxyHintsWriter(); + + public void write(BasicJsonWriter writer, ProxyHints hints) { writer.writeArray(hints.jdkProxies().map(this::toAttributes).toList()); - return sw.toString(); } private Map toAttributes(JdkProxyHint hint) { diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java similarity index 94% rename from spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java index e3f1dfc30cd..09c8dd09d14 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java @@ -16,7 +16,6 @@ package org.springframework.aot.nativex; -import java.io.StringWriter; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -34,7 +33,7 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; /** - * Serialize {@link ReflectionHints} to the JSON output expected by the GraalV + * Write {@link ReflectionHints} to the JSON output expected by the GraalVM * {@code native-image} compiler, typically named {@code reflect-config.json}. * * @author Sebastien Deleuze @@ -43,13 +42,12 @@ import org.springframework.lang.Nullable; * @see Reflection Use in Native Images * @see Native Image Build Configuration */ -class ReflectionHintsSerializer { +class ReflectionHintsWriter { - public String serialize(ReflectionHints hints) { - StringWriter sw = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(sw, " "); + public static final ReflectionHintsWriter INSTANCE = new ReflectionHintsWriter(); + + public void write(BasicJsonWriter writer, ReflectionHints hints) { writer.writeArray(hints.typeHints().map(this::toAttributes).toList()); - return sw.toString(); } private Map toAttributes(TypeHint hint) { diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java similarity index 91% rename from spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java index 9000d8cb39e..0b5f76a2787 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsSerializer.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java @@ -16,7 +16,6 @@ package org.springframework.aot.nativex; -import java.io.StringWriter; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; @@ -32,7 +31,7 @@ import org.springframework.aot.hint.ResourcePatternHint; import org.springframework.lang.Nullable; /** - * Serialize a {@link ResourceHints} to the JSON output expected by the GraalVM + * Write a {@link ResourceHints} to the JSON output expected by the GraalVM * {@code native-image} compiler, typically named {@code resource-config.json}. * * @author Sebastien Deleuze @@ -41,16 +40,15 @@ import org.springframework.lang.Nullable; * @see Accessing Resources in Native Images * @see Native Image Build Configuration */ -class ResourceHintsSerializer { +class ResourceHintsWriter { - public String serialize(ResourceHints hints) { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + public static final ResourceHintsWriter INSTANCE = new ResourceHintsWriter(); + + public void write(BasicJsonWriter writer, ResourceHints hints) { Map attributes = new LinkedHashMap<>(); attributes.put("resources", toAttributes(hints)); handleResourceBundles(attributes, hints.resourceBundles()); writer.writeObject(attributes); - return out.toString(); } private Map toAttributes(ResourceHints hint) { diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java similarity index 88% rename from spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java rename to spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java index 626e33aa02d..58d03b9b96b 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationGeneratorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java @@ -45,11 +45,11 @@ import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link FileNativeConfigurationGenerator}. + * Tests for {@link FileNativeConfigurationWriter}. * * @author Sebastien Deleuze */ -public class FileNativeConfigurationGeneratorTests { +public class FileNativeConfigurationWriterTests { @TempDir static Path tempDir; @@ -57,19 +57,19 @@ public class FileNativeConfigurationGeneratorTests { @Test void emptyConfig() { Path empty = tempDir.resolve("empty"); - FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(empty); - generator.generate(new RuntimeHints()); + FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(empty); + generator.write(new RuntimeHints()); assertThat(empty.toFile().listFiles()).isNull(); } @Test void serializationConfig() throws IOException, JSONException { - FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir); + FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir); RuntimeHints hints = new RuntimeHints(); JavaSerializationHints serializationHints = hints.javaSerialization(); serializationHints.registerType(Integer.class); serializationHints.registerType(Long.class); - generator.generate(hints); + generator.write(hints); assertEquals(""" [ { "name": "java.lang.Integer" }, @@ -79,12 +79,12 @@ public class FileNativeConfigurationGeneratorTests { @Test void proxyConfig() throws IOException, JSONException { - FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir); + FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir); RuntimeHints hints = new RuntimeHints(); ProxyHints proxyHints = hints.proxies(); proxyHints.registerJdkProxy(Function.class); proxyHints.registerJdkProxy(Function.class, Consumer.class); - generator.generate(hints); + generator.write(hints); assertEquals(""" [ { "interfaces": [ "java.util.function.Function" ] }, @@ -94,7 +94,7 @@ public class FileNativeConfigurationGeneratorTests { @Test void reflectionConfig() throws IOException, JSONException { - FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir); + FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir); RuntimeHints hints = new RuntimeHints(); ReflectionHints reflectionHints = hints.reflection(); reflectionHints.registerType(StringDecoder.class, builder -> { @@ -117,7 +117,7 @@ public class FileNativeConfigurationGeneratorTests { .withMethod("getDefaultCharset", Collections.emptyList(), constructorHint -> constructorHint.withMode(ExecutableMode.INTROSPECT)); }); - generator.generate(hints); + generator.write(hints); assertEquals(""" [ { @@ -152,12 +152,12 @@ public class FileNativeConfigurationGeneratorTests { @Test void resourceConfig() throws IOException, JSONException { - FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir); + FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir); RuntimeHints hints = new RuntimeHints(); ResourceHints resourceHints = hints.resources(); resourceHints.registerPattern("com/example/test.properties"); resourceHints.registerPattern("com/example/another.properties"); - generator.generate(hints); + generator.write(hints); assertEquals(""" { "resources": { @@ -174,11 +174,11 @@ public class FileNativeConfigurationGeneratorTests { String groupId = "foo.bar"; String artifactId = "baz"; String filename = "resource-config.json"; - FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir, groupId, artifactId); + FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir, groupId, artifactId); RuntimeHints hints = new RuntimeHints(); ResourceHints resourceHints = hints.resources(); resourceHints.registerPattern("com/example/test.properties"); - generator.generate(hints); + generator.write(hints); Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename); assertThat(jsonFile.toFile().exists()).isTrue(); } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java similarity index 83% rename from spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java rename to spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java index b0ea9bd2f57..9783e5a3aa6 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java @@ -16,6 +16,8 @@ package org.springframework.aot.nativex; +import java.io.StringWriter; + import org.json.JSONException; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; @@ -26,13 +28,11 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.core.env.Environment; /** - * Tests for {@link JavaSerializationHintsSerializer}. + * Tests for {@link JavaSerializationHintsWriter}. * * @author Sebastien Deleuze */ -public class JavaSerializationHintsSerializerTests { - - private final JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer(); +public class JavaSerializationHintsWriterTests { @Test void empty() throws JSONException { @@ -62,7 +62,10 @@ public class JavaSerializationHintsSerializerTests { } private void assertEquals(String expectedString, JavaSerializationHints hints) throws JSONException { - JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT); + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + JavaSerializationHintsWriter.INSTANCE.write(writer, hints); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.LENIENT); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java similarity index 84% rename from spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java rename to spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java index e31b4e9b29c..4bb7e73f009 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java @@ -16,6 +16,7 @@ package org.springframework.aot.nativex; +import java.io.StringWriter; import java.util.function.Consumer; import java.util.function.Function; @@ -27,13 +28,11 @@ import org.skyscreamer.jsonassert.JSONCompareMode; import org.springframework.aot.hint.ProxyHints; /** - * Tests for {@link ProxyHintsSerializer}. + * Tests for {@link ProxyHintsWriter}. * * @author Sebastien Deleuze */ -public class ProxyHintsSerializerTests { - - private final ProxyHintsSerializer serializer = new ProxyHintsSerializer(); +public class ProxyHintsWriterTests { @Test void empty() throws JSONException { @@ -64,8 +63,10 @@ public class ProxyHintsSerializerTests { } private void assertEquals(String expectedString, ProxyHints hints) throws JSONException { - - JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT); + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + ProxyHintsWriter.INSTANCE.write(writer, hints); + JSONAssert.assertEquals(expectedString, out.toString(), 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/ReflectionHintsWriterTests.java similarity index 94% rename from spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java rename to spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java index 508b64a06f6..0514ea9c587 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java @@ -16,6 +16,7 @@ package org.springframework.aot.nativex; +import java.io.StringWriter; import java.nio.charset.Charset; import java.util.Collections; import java.util.List; @@ -33,13 +34,11 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.util.MimeType; /** - * Tests for {@link ReflectionHintsSerializer}. + * Tests for {@link ReflectionHintsWriter}. * * @author Sebastien Deleuze */ -public class ReflectionHintsSerializerTests { - - private final ReflectionHintsSerializer serializer = new ReflectionHintsSerializer(); +public class ReflectionHintsWriterTests { @Test void empty() throws JSONException { @@ -158,6 +157,7 @@ public class ReflectionHintsSerializerTests { ] """, hints); } + @Test void methodAndQueriedMethods() throws JSONException { ReflectionHints hints = new ReflectionHints(); @@ -188,7 +188,10 @@ public class ReflectionHintsSerializerTests { } private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException { - JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT); + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + ReflectionHintsWriter.INSTANCE.write(writer, hints); + JSONAssert.assertEquals(expectedString, out.toString(), 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/ResourceHintsWriterTests.java similarity index 90% rename from spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java rename to spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java index 075f870ae40..c789d392017 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsSerializerTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java @@ -16,6 +16,8 @@ package org.springframework.aot.nativex; +import java.io.StringWriter; + import org.json.JSONException; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; @@ -24,13 +26,11 @@ import org.skyscreamer.jsonassert.JSONCompareMode; import org.springframework.aot.hint.ResourceHints; /** - * Tests for {@link ResourceHintsSerializer}. + * Tests for {@link ResourceHintsWriter}. * * @author Sebastien Deleuze */ -public class ResourceHintsSerializerTests { - - private final ResourceHintsSerializer serializer = new ResourceHintsSerializer(); +public class ResourceHintsWriterTests { @Test void empty() throws JSONException { @@ -117,7 +117,10 @@ public class ResourceHintsSerializerTests { } private void assertEquals(String expectedString, ResourceHints hints) throws JSONException { - JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT); + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + ResourceHintsWriter.INSTANCE.write(writer, hints); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.LENIENT); } } From 10dc10dbf959028ccc6019deb901210b9887b07f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Apr 2022 16:36:04 +0200 Subject: [PATCH 5/5] Make JSON assertions more strict --- .../aot/nativex/ResourceHintsWriter.java | 14 ++++++++++++-- .../FileNativeConfigurationWriterTests.java | 4 ++-- .../nativex/JavaSerializationHintsWriterTests.java | 2 +- .../aot/nativex/ProxyHintsWriterTests.java | 2 +- .../aot/nativex/ReflectionHintsWriterTests.java | 4 ++-- .../aot/nativex/ResourceHintsWriterTests.java | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java index 0b5f76a2787..266ffc26bed 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java @@ -46,7 +46,7 @@ class ResourceHintsWriter { public void write(BasicJsonWriter writer, ResourceHints hints) { Map attributes = new LinkedHashMap<>(); - attributes.put("resources", toAttributes(hints)); + addIfNotEmpty(attributes, "resources", toAttributes(hints)); handleResourceBundles(attributes, hints.resourceBundles()); writer.writeObject(attributes); } @@ -81,7 +81,17 @@ class ResourceHintsWriter { } private void addIfNotEmpty(Map attributes, String name, @Nullable Object value) { - if (value != null && (value instanceof Collection collection && !collection.isEmpty())) { + if (value instanceof Collection collection) { + if (!collection.isEmpty()) { + attributes.put(name, value); + } + } + else if (value instanceof Map map) { + if (!map.isEmpty()) { + attributes.put(name, value); + } + } + else if (value != null) { attributes.put(name, value); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java index 58d03b9b96b..c4308ca9f5a 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java @@ -144,7 +144,7 @@ public class FileNativeConfigurationWriterTests { ], "queriedMethods": [ { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name": "getDefaultCharset" } + { "name": "getDefaultCharset", "parameterTypes": [ ] } ] } ]""", "reflect-config.json"); @@ -186,7 +186,7 @@ public class FileNativeConfigurationWriterTests { 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); + JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java index 9783e5a3aa6..4b44a93bab9 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/JavaSerializationHintsWriterTests.java @@ -65,7 +65,7 @@ public class JavaSerializationHintsWriterTests { StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); JavaSerializationHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.LENIENT); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java index 4bb7e73f009..53040a0069a 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java @@ -66,7 +66,7 @@ public class ProxyHintsWriterTests { StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); ProxyHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.LENIENT); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java index 0514ea9c587..6c51ca6506d 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java @@ -95,7 +95,7 @@ public class ReflectionHintsWriterTests { ], "queriedMethods": [ { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name": "getDefaultCharset" } + { "name": "getDefaultCharset", "parameterTypes": [ ] } ] } ]""", hints); @@ -191,7 +191,7 @@ public class ReflectionHintsWriterTests { StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); ReflectionHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.LENIENT); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java index c789d392017..ecd68b17776 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java @@ -120,7 +120,7 @@ public class ResourceHintsWriterTests { StringWriter out = new StringWriter(); BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); ResourceHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.LENIENT); + JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE); } }