Introduce basic json writer for native configuration processing
This commit is contained in:
parent
1cf112bfa4
commit
f24369b49c
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue