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 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; Builder buildKey(CharSequence key) throws IOException;
@ -24,7 +24,13 @@ public interface Builder {
Builder buildNull() throws IOException; 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){ if (value != null){
buildKey(key); buildKey(key);
buildValue(value); buildValue(value);
@ -32,5 +38,25 @@ public interface Builder {
return this; 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(); String build();
} }

View file

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

View file

@ -6,6 +6,7 @@ import org.xbib.datastructures.json.tiny.StreamParser;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -108,4 +109,108 @@ public class JsonBuilderTest {
jsonBuilder.buildMap(Map.of("a", "b")); jsonBuilder.buildMap(Map.of("a", "b"));
assertEquals("{\"a\":\"b\"}", jsonBuilder.build()); 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.*;
import org.xbib.datastructures.api.Builder; import org.xbib.datastructures.api.Builder;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import java.time.Instant; import java.time.Instant;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
public class Yaml implements DataStructure { public class Yaml implements DataStructure {
private static final Yaml INSTANCE = new Yaml();
;
private final char separator; private final char separator;
private Node<?> root; private Node<?> root;
@ -26,6 +31,15 @@ public class Yaml implements DataStructure {
this.separator = separator; 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 @Override
public Parser createParser() { public Parser createParser() {
return new YamlParser(); 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.ByteSizeValue;
import org.xbib.datastructures.api.TimeValue; import org.xbib.datastructures.api.TimeValue;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.io.Writer;
import java.time.Instant; import java.time.Instant;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@ -14,22 +12,22 @@ import java.util.Objects;
public class YamlBuilder implements Builder { public class YamlBuilder implements Builder {
public final Writer writer; public final Appendable appendable;
private final int indent; private final int indent;
private State state; private State state;
public YamlBuilder() { public YamlBuilder() {
this(new StringWriter()); this(new StringBuilder());
} }
public YamlBuilder(Writer writer) { public YamlBuilder(Appendable appendable) {
this(writer, 2); this(appendable, 2);
} }
public YamlBuilder(Writer writer, int indent) { public YamlBuilder(Appendable appendable, int indent) {
this.writer = writer; this.appendable = appendable;
this.indent = indent; this.indent = indent;
this.state = new State(null, 0, Structure.MAP, false); this.state = new State(null, 0, Structure.MAP, false);
} }
@ -38,8 +36,8 @@ public class YamlBuilder implements Builder {
return new YamlBuilder(); return new YamlBuilder();
} }
public static YamlBuilder builder(Writer writer) { public static YamlBuilder builder(Appendable appendable) {
return new YamlBuilder(writer); return new YamlBuilder(appendable);
} }
@Override @Override
@ -89,7 +87,7 @@ public class YamlBuilder implements Builder {
} }
@Override @Override
public Builder buildCollection(Collection<Object> collection) { public Builder buildCollection(Collection<?> collection) {
Objects.requireNonNull(collection); Objects.requireNonNull(collection);
beginCollection(); beginCollection();
collection.forEach(v -> { collection.forEach(v -> {
@ -170,9 +168,16 @@ public class YamlBuilder implements Builder {
return this; return this;
} }
@Override
public Builder copy(Builder builder) throws IOException {
// TODO: no correct indent yet for copied yaml
buildValue(builder.build());
return this;
}
@Override @Override
public String build() { public String build() {
return writer.toString(); return appendable.toString();
} }
private void buildNumber(Number number) throws IOException { 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 { 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("\""))) && if (!((value.startsWith("'") && value.endsWith("'")) || (value.startsWith("\"") && value.endsWith("\""))) &&
value.matches(".*[?\\-#:>|$%&{}\\[\\]]+.*|[ ]+")) { value.matches(".*[?\\-#:>|$%&{}\\[\\]]+.*|[ ]+")) {
if (value.contains("\"")) { if (value.contains("\"")) {
@ -197,7 +203,7 @@ public class YamlBuilder implements Builder {
value = "\"" + value + "\""; value = "\"" + value + "\"";
} }
} }
writer.write(value); appendable.append(value);
} }
private void beginKey(String k) throws IOException { private void beginKey(String k) throws IOException {
@ -205,11 +211,11 @@ public class YamlBuilder implements Builder {
state.parent.item = false; state.parent.item = false;
return; return;
} }
writer.write(" ".repeat((state.level - 1) * indent)); appendable.append(" ".repeat((state.level - 1) * indent));
} }
private void endKey(String k) throws IOException { private void endKey(String k) throws IOException {
writer.write(": "); appendable.append(": ");
} }
private void beginValue(Object v) throws IOException { private void beginValue(Object v) throws IOException {
@ -236,8 +242,8 @@ public class YamlBuilder implements Builder {
if (v instanceof Collection) { if (v instanceof Collection) {
return; return;
} }
writer.write(" ".repeat((state.level - 1) * indent)); appendable.append(" ".repeat((state.level - 1) * indent));
writer.write("- "); appendable.append("- ");
state.item = true; state.item = true;
} }
@ -252,10 +258,10 @@ public class YamlBuilder implements Builder {
} }
private void writeLn() throws IOException{ 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(); StringBuilder sb = new StringBuilder();
int start = 0; int start = 0;
int l = string.length(); int l = string.length();
@ -263,16 +269,16 @@ public class YamlBuilder implements Builder {
char c = string.charAt(i); char c = string.charAt(i);
if (c == '"' || c < 32 || c >= 127 || c == '\\') { if (c == '"' || c < 32 || c >= 127 || c == '\\') {
if (start < i) { if (start < i) {
sb.append(string.toString(), start, i - start); sb.append(string, start, i - start);
} }
start = i; start = i;
sb.append(escapeCharacter(c)); sb.append(escapeCharacter(c));
} }
} }
if (start < l) { 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) { private static String escapeCharacter(char c) {