4 changed files with 487 additions and 143 deletions
@ -0,0 +1,297 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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