fix JSON construction with maps/collections, more convenience for converting strings

This commit is contained in:
Jörg Prante 2021-10-16 19:00:41 +02:00
parent b5491330a5
commit 9759d5c020
6 changed files with 264 additions and 61 deletions

View file

@ -16,7 +16,7 @@ public interface Builder {
Builder buildMap(Map<String, Object> map) throws IOException;
Builder buildCollection(Collection<Object> collection) throws IOException;
Builder buildCollection(Collection<?> collection) throws IOException;
Builder buildKey(CharSequence key) throws IOException;
@ -24,7 +24,13 @@ public interface Builder {
Builder buildNull() throws IOException;
default Builder buildIfNotNull(CharSequence key, Object value) throws IOException {
default Builder field(CharSequence key, Object value) throws IOException {
buildKey(key);
buildValue(value);
return this;
}
default Builder fieldIfNotNull(CharSequence key, Object value) throws IOException {
if (value != null){
buildKey(key);
buildValue(value);
@ -32,5 +38,25 @@ public interface Builder {
return this;
}
default Builder collection(CharSequence key, Collection<?> value) throws IOException {
buildKey(key);
buildValue(value);
return this;
}
default Builder beginMap(CharSequence key) throws IOException {
buildKey(key);
beginMap();
return this;
}
default Builder beginCollection(CharSequence key) throws IOException {
buildKey(key);
beginCollection();
return this;
}
Builder copy(Builder builder) throws IOException;
String build();
}

View file

@ -1,13 +1,23 @@
package org.xbib.datastructures.json.tiny;
import org.xbib.datastructures.api.*;
import org.xbib.datastructures.api.Builder;
import org.xbib.datastructures.api.ByteSizeValue;
import org.xbib.datastructures.api.DataStructure;
import org.xbib.datastructures.api.Generator;
import org.xbib.datastructures.api.Node;
import org.xbib.datastructures.api.Parser;
import org.xbib.datastructures.api.TimeValue;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.Instant;
import java.util.Map;
import java.util.function.Consumer;
public class Json implements DataStructure {
private static final Json INSTANCE = new Json();
private final char separator;
private Node<?> root;
@ -25,6 +35,15 @@ public class Json implements DataStructure {
this.separator = separator;
}
@SuppressWarnings("unchecked")
public static Map<String, Object> toMap(String yaml) throws IOException {
return (Map<String, Object>) INSTANCE.createParser().parse(new StringReader(yaml)).get();
}
public static String toString(Map<String, Object> map) throws IOException {
return INSTANCE.createBuilder().buildMap(map).build();
}
@Override
public Parser createParser() {
return new StreamParser();

View file

@ -3,11 +3,8 @@ package org.xbib.datastructures.json.tiny;
import org.xbib.datastructures.api.Builder;
import org.xbib.datastructures.api.ByteSizeValue;
import org.xbib.datastructures.api.TimeValue;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.time.Instant;
import java.util.Collection;
import java.util.Map;
@ -15,31 +12,31 @@ import java.util.Objects;
public class JsonBuilder implements Builder {
private final Writer writer;
private final Appendable appendable;
private State state;
public JsonBuilder() {
this(new StringWriter());
this(new StringBuilder());
}
public JsonBuilder(Writer writer) {
this.writer = writer;
this.state = new State(null, 0, Structure.MAP, true);
public JsonBuilder(Appendable appendable) {
this.appendable = appendable;
this.state = new State(null, 0, Structure.DOCSTART, true);
}
public static JsonBuilder builder() {
return new JsonBuilder();
}
public static JsonBuilder builder(Writer writer) {
return new JsonBuilder(writer);
public static JsonBuilder builder(Appendable appendable) {
return new JsonBuilder(appendable);
}
@Override
public Builder beginCollection() throws IOException {
this.state = new State(state, state.level + 1, Structure.COLLECTION, true);
writer.write('[');
appendable.append('[');
return this;
}
@ -48,7 +45,7 @@ public class JsonBuilder implements Builder {
if (state.structure != Structure.COLLECTION) {
throw new JsonException("no array to close");
}
writer.write(']');
appendable.append(']');
this.state = state != null ? state.parent : null;
return this;
}
@ -56,25 +53,30 @@ public class JsonBuilder implements Builder {
@Override
public Builder beginMap() throws IOException {
this.state = new State(state, state.level + 1, Structure.MAP, true);
writer.write('{');
appendable.append('{');
return this;
}
@Override
public Builder endMap() throws IOException {
if (state.structure != Structure.MAP) {
if (state.structure != Structure.MAP && state.structure != Structure.KEY) {
throw new JsonException("no object to close");
}
writer.write('}');
appendable.append('}');
this.state = state != null ? state.parent : null;
return this;
}
@Override
public Builder buildMap(Map<String, Object> map) throws IOException {
Objects.requireNonNull(map);
beginMap();
if (state.structure == Structure.COLLECTION) {
beginArrayValue(map);
}
boolean wrap = state.structure != Structure.MAP;
if (wrap) {
beginMap();
}
map.forEach((k, v) -> {
try {
buildKey(k);
@ -83,12 +85,17 @@ public class JsonBuilder implements Builder {
throw new UncheckedIOException(e);
}
});
endMap();
if (wrap) {
endMap();
}
if (state.structure == Structure.COLLECTION) {
endArrayValue(map);
}
return this;
}
@Override
public Builder buildCollection(Collection<Object> collection) throws IOException {
public Builder buildCollection(Collection<?> collection) throws IOException {
Objects.requireNonNull(collection);
beginCollection();
collection.forEach(v -> {
@ -105,7 +112,7 @@ public class JsonBuilder implements Builder {
@SuppressWarnings("unchecked")
@Override
public Builder buildValue(Object object) throws IOException {
if (state.structure == Structure.MAP) {
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
beginValue(object);
} else if (state.structure == Structure.COLLECTION) {
beginArrayValue(object);
@ -142,7 +149,7 @@ public class JsonBuilder implements Builder {
} else {
throw new IllegalArgumentException("unable to write object class " + object.getClass());
}
if (state.structure == Structure.MAP) {
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
endValue(object);
} else if (state.structure == Structure.COLLECTION) {
endArrayValue(object);
@ -152,42 +159,68 @@ public class JsonBuilder implements Builder {
@Override
public Builder buildKey(CharSequence string) throws IOException {
if (state.structure == Structure.MAP) {
if (state.structure == Structure.COLLECTION) {
beginArrayValue(string);
}
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
beginKey(string != null ? string.toString() : null);
}
buildString(string, true);
if (state.structure == Structure.MAP) {
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
endKey(string != null ? string.toString() : null);
}
state.structure = Structure.KEY;
return this;
}
@Override
public Builder buildNull() throws IOException {
if (state.structure == Structure.MAP) {
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
beginValue(null);
} else if (state.structure == Structure.COLLECTION) {
beginArrayValue(null);
}
buildString("null", false);
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
endValue(null);
} else if (state.structure == Structure.COLLECTION) {
endArrayValue(null);
}
return this;
}
@Override
public Builder copy(Builder builder) throws IOException {
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
beginValue(null);
} else if (state.structure == Structure.COLLECTION) {
beginArrayValue(null);
}
appendable.append(builder.build());
if (state.structure == Structure.MAP || state.structure == Structure.KEY) {
endValue(null);
}
if (state.structure == Structure.COLLECTION) {
endArrayValue(null);
}
return this;
}
@Override
public String build() {
return writer.toString();
return appendable.toString();
}
private void beginKey(String k) throws IOException {
if (state.first) {
state.first = false;
} else {
writer.write(",");
appendable.append(",");
}
}
private void endKey(String k) throws IOException {
writer.write(":");
appendable.append(":");
}
private void beginValue(Object v) {
@ -200,7 +233,7 @@ public class JsonBuilder implements Builder {
if (state.first) {
state.first = false;
} else {
writer.write(",");
appendable.append(",");
}
}
@ -220,10 +253,10 @@ public class JsonBuilder implements Builder {
}
private void buildString(CharSequence string, boolean escape) throws IOException {
writer.write(escape ? escapeString(string) : string.toString());
appendable.append(escape ? escapeString(string) : string);
}
private String escapeString(CharSequence string) {
private CharSequence escapeString(CharSequence string) {
StringBuilder sb = new StringBuilder();
sb.append('"');
int start = 0;
@ -232,17 +265,17 @@ public class JsonBuilder implements Builder {
char c = string.charAt(i);
if (c == '"' || c < 32 || c >= 127 || c == '\\') {
if (start < i) {
sb.append(string.toString(), start, i - start);
sb.append(string, start, i - start);
}
start = i;
sb.append(escapeCharacter(c));
}
}
if (start < l) {
sb.append(string.toString(), start, l - start);
sb.append(string, start, l - start);
}
sb.append('"');
return sb.toString();
return sb;
}
private static String escapeCharacter(char c) {
@ -264,7 +297,7 @@ public class JsonBuilder implements Builder {
return "\\u0000".substring(0, 6 - hex.length()) + hex;
}
private enum Structure { MAP, COLLECTION };
private enum Structure { DOCSTART, MAP, KEY, COLLECTION };
private static class State {
State parent;

View file

@ -6,6 +6,7 @@ import org.xbib.datastructures.json.tiny.StreamParser;
import java.io.IOException;
import java.io.StringReader;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -108,4 +109,108 @@ public class JsonBuilderTest {
jsonBuilder.buildMap(Map.of("a", "b"));
assertEquals("{\"a\":\"b\"}", jsonBuilder.build());
}
@Test
public void testKeyValue() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.buildKey("a");
jsonBuilder.buildValue("b");
// test comma separation
jsonBuilder.buildKey("c");
jsonBuilder.buildValue("d");
jsonBuilder.endMap();
assertEquals("{\"a\":\"b\",\"c\":\"d\"}", jsonBuilder.build());
}
@Test
public void testMapBuild() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.buildKey("map");
// buildMap is wrapped with '{' and '}'
jsonBuilder.buildMap(Map.of("a", "b"));
jsonBuilder.endMap();
assertEquals("{\"map\":{\"a\":\"b\"}}", jsonBuilder.build());
}
@Test
public void testBeginMapBuild() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.beginMap("map");
// buildMap is not wrapped with '{' and '}'
jsonBuilder.buildMap(Map.of("a", "b"));
jsonBuilder.endMap();
jsonBuilder.endMap();
assertEquals("{\"map\":{\"a\":\"b\"}}", jsonBuilder.build());
}
@Test
public void testMapOfCollections() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.beginMap("map");
jsonBuilder.collection("a", Arrays.asList("b", "c"));
// test comma separation
jsonBuilder.collection("d", Arrays.asList("e", "f"));
jsonBuilder.endMap();
jsonBuilder.endMap();
assertEquals("{\"map\":{\"a\":[\"b\",\"c\"],\"d\":[\"e\",\"f\"]}}", jsonBuilder.build());
}
@Test
public void testMapOfEmptyCollections() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.beginMap("map");
jsonBuilder.collection("a", List.of());
// test comma separation
jsonBuilder.collection("b", List.of());
jsonBuilder.endMap();
jsonBuilder.endMap();
assertEquals("{\"map\":{\"a\":[],\"b\":[]}}", jsonBuilder.build());
}
@Test
public void testCollectionOfMaps() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.beginCollection("collection");
jsonBuilder.buildMap(Map.of("a", "b"));
// test comma separation
jsonBuilder.buildMap(Map.of("c", "d"));
jsonBuilder.endCollection();
jsonBuilder.endMap();
assertEquals("{\"collection\":[{\"a\":\"b\"},{\"c\":\"d\"}]}", jsonBuilder.build());
}
@Test
public void testCollectionOfEmptyMaps() throws Exception {
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginMap();
jsonBuilder.beginCollection("collection");
jsonBuilder.buildMap(Map.of());
// test comma separation
jsonBuilder.buildMap(Map.of());
jsonBuilder.endCollection();
jsonBuilder.endMap();
assertEquals("{\"collection\":[{},{}]}", jsonBuilder.build());
}
@Test
public void testCopy() throws Exception {
JsonBuilder jsonBuilder1 = new JsonBuilder();
jsonBuilder1.buildMap(Map.of("a", "b"));
JsonBuilder jsonBuilder2 = new JsonBuilder();
jsonBuilder2.buildMap(Map.of("c", "d"));
JsonBuilder jsonBuilder = new JsonBuilder();
jsonBuilder.beginCollection();
jsonBuilder.copy(jsonBuilder1);
// test comma separation
jsonBuilder.copy(jsonBuilder2);
jsonBuilder.endCollection();
assertEquals("[{\"a\":\"b\"},{\"c\":\"d\"}]", jsonBuilder.build());
}
}

View file

@ -3,12 +3,17 @@ package org.xbib.datastructures.yaml.tiny;
import org.xbib.datastructures.api.*;
import org.xbib.datastructures.api.Builder;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.Instant;
import java.util.Map;
import java.util.function.Consumer;
public class Yaml implements DataStructure {
private static final Yaml INSTANCE = new Yaml();
;
private final char separator;
private Node<?> root;
@ -26,6 +31,15 @@ public class Yaml implements DataStructure {
this.separator = separator;
}
@SuppressWarnings("unchecked")
public static Map<String, Object> toMap(String yaml) throws IOException {
return (Map<String, Object>) INSTANCE.createParser().parse(new StringReader(yaml)).get();
}
public static String toString(Map<String, Object> map) throws IOException {
return INSTANCE.createBuilder().buildMap(map).build();
}
@Override
public Parser createParser() {
return new YamlParser();

View file

@ -4,9 +4,7 @@ import org.xbib.datastructures.api.Builder;
import org.xbib.datastructures.api.ByteSizeValue;
import org.xbib.datastructures.api.TimeValue;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.time.Instant;
import java.util.Collection;
import java.util.Map;
@ -14,22 +12,22 @@ import java.util.Objects;
public class YamlBuilder implements Builder {
public final Writer writer;
public final Appendable appendable;
private final int indent;
private State state;
public YamlBuilder() {
this(new StringWriter());
this(new StringBuilder());
}
public YamlBuilder(Writer writer) {
this(writer, 2);
public YamlBuilder(Appendable appendable) {
this(appendable, 2);
}
public YamlBuilder(Writer writer, int indent) {
this.writer = writer;
public YamlBuilder(Appendable appendable, int indent) {
this.appendable = appendable;
this.indent = indent;
this.state = new State(null, 0, Structure.MAP, false);
}
@ -38,8 +36,8 @@ public class YamlBuilder implements Builder {
return new YamlBuilder();
}
public static YamlBuilder builder(Writer writer) {
return new YamlBuilder(writer);
public static YamlBuilder builder(Appendable appendable) {
return new YamlBuilder(appendable);
}
@Override
@ -89,7 +87,7 @@ public class YamlBuilder implements Builder {
}
@Override
public Builder buildCollection(Collection<Object> collection) {
public Builder buildCollection(Collection<?> collection) {
Objects.requireNonNull(collection);
beginCollection();
collection.forEach(v -> {
@ -170,9 +168,16 @@ public class YamlBuilder implements Builder {
return this;
}
@Override
public Builder copy(Builder builder) throws IOException {
// TODO: no correct indent yet for copied yaml
buildValue(builder.build());
return this;
}
@Override
public String build() {
return writer.toString();
return appendable.toString();
}
private void buildNumber(Number number) throws IOException {
@ -188,7 +193,8 @@ public class YamlBuilder implements Builder {
}
private void buildString(CharSequence string, boolean escape) throws IOException {
String value = escape ? escapeString(string) : string.toString();
CharSequence charSequence = escape ? escapeString(string) : string;
String value = charSequence.toString();
if (!((value.startsWith("'") && value.endsWith("'")) || (value.startsWith("\"") && value.endsWith("\""))) &&
value.matches(".*[?\\-#:>|$%&{}\\[\\]]+.*|[ ]+")) {
if (value.contains("\"")) {
@ -197,7 +203,7 @@ public class YamlBuilder implements Builder {
value = "\"" + value + "\"";
}
}
writer.write(value);
appendable.append(value);
}
private void beginKey(String k) throws IOException {
@ -205,11 +211,11 @@ public class YamlBuilder implements Builder {
state.parent.item = false;
return;
}
writer.write(" ".repeat((state.level - 1) * indent));
appendable.append(" ".repeat((state.level - 1) * indent));
}
private void endKey(String k) throws IOException {
writer.write(": ");
appendable.append(": ");
}
private void beginValue(Object v) throws IOException {
@ -236,8 +242,8 @@ public class YamlBuilder implements Builder {
if (v instanceof Collection) {
return;
}
writer.write(" ".repeat((state.level - 1) * indent));
writer.write("- ");
appendable.append(" ".repeat((state.level - 1) * indent));
appendable.append("- ");
state.item = true;
}
@ -252,10 +258,10 @@ public class YamlBuilder implements Builder {
}
private void writeLn() throws IOException{
writer.write(System.lineSeparator());
appendable.append(System.lineSeparator());
}
private String escapeString(CharSequence string) {
private CharSequence escapeString(CharSequence string) {
StringBuilder sb = new StringBuilder();
int start = 0;
int l = string.length();
@ -263,16 +269,16 @@ public class YamlBuilder implements Builder {
char c = string.charAt(i);
if (c == '"' || c < 32 || c >= 127 || c == '\\') {
if (start < i) {
sb.append(string.toString(), start, i - start);
sb.append(string, start, i - start);
}
start = i;
sb.append(escapeCharacter(c));
}
}
if (start < l) {
sb.append(string.toString(), start, l - start);
sb.append(string, start, l - start);
}
return sb.toString();
return sb;
}
private static String escapeCharacter(char c) {