Introduce basic json writer for native configuration processing

This commit is contained in:
Stephane Nicoll 2022-04-11 15:43:26 +02:00
parent 1cf112bfa4
commit f24369b49c
4 changed files with 487 additions and 143 deletions

View File

@ -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:
* <ul>
* <li>Map: write the value as a nested object</li>
* <li>List: write the value as a nested array</li>
* <li>Otherwise, write a single value</li>
* </ul>
* @param attributes the attributes of the object
*/
public void writeObject(Map<String, Object> 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<String, Object> 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 <T> Runnable writeAll(Iterator<T> it, Consumer<T> 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<String, Object>) 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();
}
}
}

View File

@ -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();
}
}

View File

@ -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<String, Object> attributes = orderedMap("test", "value");
attributes.put("another", true);
this.json.writeObject(attributes);
assertThat(out.toString()).isEqualTo("""
{
"test": "value",
"another": true
}
""");
}
@Test
void writeObjectWitNestedObject() {
Map<String, Object> 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<String, Object> 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<String, Object> attributes = orderedMap("test", "value");
LinkedHashMap<String, Object> 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<String, Object> attributes = orderedMap("test", "value");
attributes.put("nested", Collections.emptyList());
this.json.writeObject(attributes);
assertThat(out.toString()).isEqualTo("""
{
"test": "value",
"nested": [ ]
}
""");
}
@Test
void writeObjectWithNestedEmptyObject() {
Map<String, Object> 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<String, Object> attributes = new LinkedHashMap<>();
attributes.put("test", value);
this.json.writeObject(attributes);
assertThat(out.toString()).contains("\"test\": \"" + expectedEscapedValue + "\"");
}
private static LinkedHashMap<String, Object> orderedMap(String key, Object value) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put(key, value);
return map;
}
}

View File

@ -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");
}
}