added reference serialization and deserialization in both json (text) and jbon (binary) format
This commit is contained in:
@@ -26,10 +26,6 @@ libraryDependencies += "org.antlr" % "antlr4" % "4.7.1" % Test
|
|||||||
libraryDependencies += "org.antlr" % "antlr4-runtime" % "4.7.1" % Test
|
libraryDependencies += "org.antlr" % "antlr4-runtime" % "4.7.1" % Test
|
||||||
libraryDependencies += "org.tukaani" % "xz" % "1.8" % Test
|
libraryDependencies += "org.tukaani" % "xz" % "1.8" % Test
|
||||||
|
|
||||||
artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) =>
|
|
||||||
artifact.name + "-" + module.revision + "." + artifact.extension
|
|
||||||
}
|
|
||||||
|
|
||||||
enablePlugins(Antlr4Plugin)
|
enablePlugins(Antlr4Plugin)
|
||||||
antlr4Version in Antlr4 := "4.7.1"
|
antlr4Version in Antlr4 := "4.7.1"
|
||||||
antlr4PackageName in Antlr4 := Some("net.woggioni.worth.antlr")
|
antlr4PackageName in Antlr4 := Some("net.woggioni.worth.antlr")
|
||||||
|
@@ -2,8 +2,11 @@ package net.woggioni.worth.serialization;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import net.woggioni.worth.exception.NotImplementedException;
|
import net.woggioni.worth.exception.NotImplementedException;
|
||||||
import net.woggioni.worth.value.ArrayValue;
|
import net.woggioni.worth.traversal.TraversalContext;
|
||||||
import net.woggioni.worth.value.ObjectValue;
|
import net.woggioni.worth.traversal.ValueIdentity;
|
||||||
|
import net.woggioni.worth.traversal.ValueVisitor;
|
||||||
|
import net.woggioni.worth.traversal.ValueWalker;
|
||||||
|
import net.woggioni.worth.value.*;
|
||||||
import net.woggioni.worth.xface.Dumper;
|
import net.woggioni.worth.xface.Dumper;
|
||||||
import net.woggioni.worth.xface.Value;
|
import net.woggioni.worth.xface.Value;
|
||||||
|
|
||||||
@@ -11,9 +14,9 @@ import java.io.OutputStream;
|
|||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayDeque;
|
import java.util.*;
|
||||||
import java.util.Iterator;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Map;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public abstract class ValueDumper implements Dumper {
|
public abstract class ValueDumper implements Dumper {
|
||||||
|
|
||||||
@@ -25,6 +28,47 @@ public abstract class ValueDumper implements Dumper {
|
|||||||
public final Value value;
|
public final Value value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Map<ValueIdentity, Integer> getIdMap(Value root) {
|
||||||
|
Map<ValueIdentity, Integer> occurrencies = new HashMap<>();
|
||||||
|
ValueVisitor visitor = new ValueVisitor(){
|
||||||
|
|
||||||
|
private void visit(Value v) {
|
||||||
|
ValueIdentity identity = new ValueIdentity(v);
|
||||||
|
Integer i = occurrencies.getOrDefault(identity, 0);
|
||||||
|
occurrencies.put(identity, ++i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean filter(Value value, TraversalContext ctx) {
|
||||||
|
if(value.type() == Value.Type.ARRAY || value.type() == Value.Type.OBJECT) {
|
||||||
|
ValueIdentity identity = new ValueIdentity(value);
|
||||||
|
Integer i = occurrencies.getOrDefault(identity, 0);
|
||||||
|
occurrencies.put(identity, ++i);
|
||||||
|
return i == 1;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ObjectValue value, TraversalContext ctx) {
|
||||||
|
visit(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ArrayValue value, TraversalContext ctx) {
|
||||||
|
visit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ValueWalker.walk(root, visitor);
|
||||||
|
Map<ValueIdentity, Integer> result = new HashMap<>();
|
||||||
|
int i = 0;
|
||||||
|
for(ValueIdentity identity : occurrencies.keySet()) {
|
||||||
|
result.put(identity, i++);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
protected static class ArrayStackLevel extends StackLevel implements Iterator<Value> {
|
protected static class ArrayStackLevel extends StackLevel implements Iterator<Value> {
|
||||||
private final Iterator<Value> iterator = ((ArrayValue) value).iterator();
|
private final Iterator<Value> iterator = ((ArrayValue) value).iterator();
|
||||||
|
|
||||||
@@ -110,4 +154,8 @@ public abstract class ValueDumper implements Dumper {
|
|||||||
protected abstract void booleanValue(boolean value);
|
protected abstract void booleanValue(boolean value);
|
||||||
|
|
||||||
protected abstract void nullValue();
|
protected abstract void nullValue();
|
||||||
|
|
||||||
|
protected abstract void valueId(int id);
|
||||||
|
|
||||||
|
protected abstract void valueReference(int id);
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package net.woggioni.worth.serialization;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import net.woggioni.worth.exception.MaxDepthExceededException;
|
import net.woggioni.worth.exception.MaxDepthExceededException;
|
||||||
import net.woggioni.worth.exception.NotImplementedException;
|
import net.woggioni.worth.exception.NotImplementedException;
|
||||||
|
import net.woggioni.worth.exception.ParseException;
|
||||||
import net.woggioni.worth.utils.WorthUtils;
|
import net.woggioni.worth.utils.WorthUtils;
|
||||||
import net.woggioni.worth.value.*;
|
import net.woggioni.worth.value.*;
|
||||||
import net.woggioni.worth.xface.Parser;
|
import net.woggioni.worth.xface.Parser;
|
||||||
@@ -11,8 +12,12 @@ import net.woggioni.worth.xface.Value;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static net.woggioni.worth.utils.WorthUtils.newThrowable;
|
import static net.woggioni.worth.utils.WorthUtils.newThrowable;
|
||||||
|
|
||||||
@@ -27,9 +32,6 @@ public class ValueParser implements Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected static class ArrayStackLevel extends StackLevel {
|
protected static class ArrayStackLevel extends StackLevel {
|
||||||
public ArrayStackLevel() {
|
|
||||||
super(new ArrayValue(), -1);
|
|
||||||
}
|
|
||||||
public ArrayStackLevel(long expectedSize) {
|
public ArrayStackLevel(long expectedSize) {
|
||||||
super(new ArrayValue(), expectedSize);
|
super(new ArrayValue(), expectedSize);
|
||||||
}
|
}
|
||||||
@@ -38,16 +40,13 @@ public class ValueParser implements Parser {
|
|||||||
protected static class ObjectStackLevel extends StackLevel {
|
protected static class ObjectStackLevel extends StackLevel {
|
||||||
public String currentKey;
|
public String currentKey;
|
||||||
|
|
||||||
public ObjectStackLevel(Value.Configuration cfg) {
|
|
||||||
super(ObjectValue.newInstance(cfg), -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectStackLevel(Value.Configuration cfg, long expectedSize) {
|
public ObjectStackLevel(Value.Configuration cfg, long expectedSize) {
|
||||||
super(ObjectValue.newInstance(cfg), expectedSize);
|
super(ObjectValue.newInstance(cfg), expectedSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ArrayDeque<StackLevel> stack;
|
protected ArrayDeque<StackLevel> stack;
|
||||||
|
protected Map<Integer, Value> idMap;
|
||||||
|
|
||||||
private void add2Last(Value value) {
|
private void add2Last(Value value) {
|
||||||
StackLevel last = stack.getFirst();
|
StackLevel last = stack.getFirst();
|
||||||
@@ -67,17 +66,20 @@ public class ValueParser implements Parser {
|
|||||||
|
|
||||||
protected ValueParser(Value.Configuration cfg) {
|
protected ValueParser(Value.Configuration cfg) {
|
||||||
this.cfg = cfg;
|
this.cfg = cfg;
|
||||||
|
if (cfg.serializeReferences) {
|
||||||
|
idMap = new HashMap<>();
|
||||||
|
}
|
||||||
stack = new ArrayDeque<>() {
|
stack = new ArrayDeque<>() {
|
||||||
@Override
|
@Override
|
||||||
public void push(StackLevel stackLevel) {
|
public void push(StackLevel stackLevel) {
|
||||||
if(size() == cfg.maxDepth) {
|
if (size() == cfg.maxDepth) {
|
||||||
throw newThrowable(MaxDepthExceededException.class,
|
throw newThrowable(MaxDepthExceededException.class,
|
||||||
"Objects is too deep, max allowed depth is %d", cfg.maxDepth);
|
"Objects is too deep, max allowed depth is %d", cfg.maxDepth);
|
||||||
}
|
}
|
||||||
super.push(stackLevel);
|
super.push(stackLevel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
stack.push(new ArrayStackLevel());
|
stack.push(new ArrayStackLevel(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -95,29 +97,40 @@ public class ValueParser implements Parser {
|
|||||||
return parse(new InputStreamReader(stream, encoding));
|
return parse(new InputStreamReader(stream, encoding));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void beginObject() {
|
protected Value beginObject() {
|
||||||
stack.push(new ObjectStackLevel(cfg));
|
return beginObject(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void beginObject(long size) {
|
protected Value beginObject(long size) {
|
||||||
stack.push(new ObjectStackLevel(cfg, size));
|
ObjectStackLevel osl = new ObjectStackLevel(cfg, size);
|
||||||
|
stack.push(osl);
|
||||||
|
return osl.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void endObject() {
|
protected void endObject() {
|
||||||
add2Last(stack.pop().value);
|
ValueParser.StackLevel sl = stack.pop();
|
||||||
|
if(!(sl instanceof ValueParser.ObjectStackLevel)) {
|
||||||
|
error(ParseException::new, "Unexpected object terminator");
|
||||||
|
}
|
||||||
|
add2Last(sl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void beginArray() {
|
protected Value beginArray() {
|
||||||
stack.push(new ArrayStackLevel());
|
return beginArray(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void beginArray(long size) {
|
protected Value beginArray(long size) {
|
||||||
stack.push(new ArrayStackLevel(size));
|
ArrayStackLevel ale = new ArrayStackLevel(size);
|
||||||
|
stack.push(ale);
|
||||||
|
return ale.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void endArray() {
|
protected void endArray() {
|
||||||
add2Last(stack.pop().value);
|
ValueParser.StackLevel sl = stack.pop();
|
||||||
|
if(!(sl instanceof ValueParser.ArrayStackLevel)) {
|
||||||
|
error(ParseException::new, "Unexpected array terminator");
|
||||||
|
}
|
||||||
|
add2Last(sl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void objectKey(String key) {
|
protected void objectKey(String key) {
|
||||||
@@ -144,4 +157,20 @@ public class ValueParser implements Parser {
|
|||||||
protected void nullValue() {
|
protected void nullValue() {
|
||||||
add2Last(Value.Null);
|
add2Last(Value.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void valueId(int id, Value value) {
|
||||||
|
idMap.put(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void valueReference(int id) {
|
||||||
|
Value referencedValue = idMap.get(id);
|
||||||
|
if (referencedValue == null) {
|
||||||
|
error(ParseException::new, "got invalid id '%d'", id);
|
||||||
|
}
|
||||||
|
add2Last(referencedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T extends RuntimeException> T error(Function<String, T> constructor, String fmt, Object... args) {
|
||||||
|
throw new NotImplementedException("Method not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import lombok.SneakyThrows;
|
|||||||
import net.woggioni.worth.exception.NotImplementedException;
|
import net.woggioni.worth.exception.NotImplementedException;
|
||||||
import net.woggioni.worth.serialization.ValueDumper;
|
import net.woggioni.worth.serialization.ValueDumper;
|
||||||
import net.woggioni.worth.serialization.json.JSONDumper;
|
import net.woggioni.worth.serialization.json.JSONDumper;
|
||||||
|
import net.woggioni.worth.traversal.ValueIdentity;
|
||||||
import net.woggioni.worth.utils.Leb128;
|
import net.woggioni.worth.utils.Leb128;
|
||||||
import net.woggioni.worth.utils.WorthUtils;
|
import net.woggioni.worth.utils.WorthUtils;
|
||||||
import net.woggioni.worth.value.ArrayValue;
|
import net.woggioni.worth.value.ArrayValue;
|
||||||
@@ -13,7 +14,9 @@ import net.woggioni.worth.xface.Value;
|
|||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class JBONDumper extends ValueDumper {
|
public class JBONDumper extends ValueDumper {
|
||||||
@@ -44,8 +47,18 @@ public class JBONDumper extends ValueDumper {
|
|||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void dump(Value value, OutputStream outputStream) {
|
public void dump(Value value, OutputStream outputStream) {
|
||||||
|
Map<ValueIdentity, Integer> ids;
|
||||||
|
Set<Integer> dumpedId;
|
||||||
|
if(cfg.serializeReferences) {
|
||||||
|
ids = getIdMap(value);
|
||||||
|
dumpedId = new HashSet<>();
|
||||||
|
} else {
|
||||||
|
ids = null;
|
||||||
|
dumpedId = null;
|
||||||
|
}
|
||||||
this.os = outputStream;
|
this.os = outputStream;
|
||||||
final Consumer<Value> handle_value = (v) -> {
|
final Consumer<Value> handle_value = (v) -> {
|
||||||
|
Integer id;
|
||||||
switch (v.type()) {
|
switch (v.type()) {
|
||||||
case NULL:
|
case NULL:
|
||||||
nullValue();
|
nullValue();
|
||||||
@@ -64,13 +77,33 @@ public class JBONDumper extends ValueDumper {
|
|||||||
break;
|
break;
|
||||||
case ARRAY:
|
case ARRAY:
|
||||||
ArrayValue arrayValue = WorthUtils.dynamicCast(v, ArrayValue.class);
|
ArrayValue arrayValue = WorthUtils.dynamicCast(v, ArrayValue.class);
|
||||||
|
if(ids != null && (id = ids.get(new ValueIdentity(arrayValue))) != null) {
|
||||||
|
if(dumpedId.add(id)) {
|
||||||
|
stack.push(new ArrayStackLevel(arrayValue));
|
||||||
|
valueId(id);
|
||||||
|
beginArray(arrayValue.size());
|
||||||
|
} else {
|
||||||
|
valueReference(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
stack.push(new ArrayStackLevel(arrayValue));
|
stack.push(new ArrayStackLevel(arrayValue));
|
||||||
beginArray(arrayValue.size());
|
beginArray(arrayValue.size());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case OBJECT:
|
case OBJECT:
|
||||||
ObjectValue objectValue = WorthUtils.dynamicCast(v, ObjectValue.class);
|
ObjectValue objectValue = WorthUtils.dynamicCast(v, ObjectValue.class);
|
||||||
stack.push(new ObjectStackLevel(objectValue));
|
if(ids != null && (id = ids.get(new ValueIdentity(objectValue))) != null) {
|
||||||
|
if(dumpedId.add(id)) {
|
||||||
|
stack.push(new ObjectStackLevel(WorthUtils.dynamicCast(v, ObjectValue.class)));
|
||||||
|
valueId(id);
|
||||||
beginObject(objectValue.size());
|
beginObject(objectValue.size());
|
||||||
|
} else {
|
||||||
|
valueReference(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stack.push(new ObjectStackLevel(WorthUtils.dynamicCast(v, ObjectValue.class)));
|
||||||
|
beginObject(objectValue.size());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -191,4 +224,18 @@ public class JBONDumper extends ValueDumper {
|
|||||||
protected void nullValue() {
|
protected void nullValue() {
|
||||||
os.write(BinaryMarker.Null.value);
|
os.write(BinaryMarker.Null.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void valueReference(int id) {
|
||||||
|
os.write(BinaryMarker.Reference.value);
|
||||||
|
Leb128.encode(os, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void valueId(int id) {
|
||||||
|
os.write(BinaryMarker.Id.value);
|
||||||
|
Leb128.encode(os, id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,8 @@ public class JBONParser extends ValueParser {
|
|||||||
private int cursor = 0;
|
private int cursor = 0;
|
||||||
|
|
||||||
|
|
||||||
private <T extends RuntimeException> T error(Function<String, T> constructor, String fmt, Object... args) {
|
@Override
|
||||||
|
protected <T extends RuntimeException> T error(Function<String, T> constructor, String fmt, Object... args) {
|
||||||
return constructor.apply(
|
return constructor.apply(
|
||||||
String.format("Error at position %d: %s",
|
String.format("Error at position %d: %s",
|
||||||
cursor, String.format(fmt, args)));
|
cursor, String.format(fmt, args)));
|
||||||
@@ -38,6 +39,7 @@ public class JBONParser extends ValueParser {
|
|||||||
stream.read();
|
stream.read();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Integer currentId = null;
|
||||||
Leb128.Leb128Decoder decoder = new Leb128.Leb128Decoder(stream);
|
Leb128.Leb128Decoder decoder = new Leb128.Leb128Decoder(stream);
|
||||||
ObjectStackLevel osl;
|
ObjectStackLevel osl;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -52,7 +54,11 @@ public class JBONParser extends ValueParser {
|
|||||||
if(c == -1) {
|
if(c == -1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(c == BinaryMarker.Null.value) {
|
if(idMap != null && c == BinaryMarker.Id.value) {
|
||||||
|
currentId = (int) decoder.decode();
|
||||||
|
} else if(idMap != null && c == BinaryMarker.Reference.value) {
|
||||||
|
valueReference((int) decoder.decode());
|
||||||
|
} else if(c == BinaryMarker.Null.value) {
|
||||||
nullValue();
|
nullValue();
|
||||||
} else if(c == BinaryMarker.True.value) {
|
} else if(c == BinaryMarker.True.value) {
|
||||||
booleanValue(true);
|
booleanValue(true);
|
||||||
@@ -75,17 +81,26 @@ public class JBONParser extends ValueParser {
|
|||||||
stream.read(buffer);
|
stream.read(buffer);
|
||||||
String text = new String(buffer);
|
String text = new String(buffer);
|
||||||
stringValue(text);
|
stringValue(text);
|
||||||
} else if(c == BinaryMarker.EmptyArray.value) {
|
} else if(c >= BinaryMarker.EmptyArray.value && c <= BinaryMarker.LargeArray.value) {
|
||||||
beginArray(0);
|
long size;
|
||||||
} else if(c > BinaryMarker.EmptyArray.value && c < BinaryMarker.LargeArray.value) {
|
if(c == BinaryMarker.LargeArray.value) {
|
||||||
beginArray(c - BinaryMarker.EmptyArray.value);
|
size = decoder.decode();
|
||||||
} else if(c == BinaryMarker.LargeArray.value) {
|
} else {
|
||||||
long size = decoder.decode();
|
size = c - BinaryMarker.EmptyArray.value;
|
||||||
beginArray(size);
|
}
|
||||||
} else if(c == BinaryMarker.EmptyObject.value) {
|
Value newArray = beginArray(size);
|
||||||
beginObject(0);
|
if(currentId != null) valueId(currentId, newArray);
|
||||||
} else if(c > BinaryMarker.EmptyObject.value && c < BinaryMarker.LargeObject.value) {
|
currentId = null;
|
||||||
beginObject(c - BinaryMarker.EmptyObject.value);
|
} else if(c >= BinaryMarker.EmptyObject.value && c <= BinaryMarker.LargeObject.value) {
|
||||||
|
long size;
|
||||||
|
if(c == BinaryMarker.LargeObject.value) {
|
||||||
|
size = decoder.decode();
|
||||||
|
} else {
|
||||||
|
size = c - BinaryMarker.EmptyObject.value;
|
||||||
|
}
|
||||||
|
Value newObject = beginObject(size);
|
||||||
|
if(currentId != null) valueId(currentId, newObject);
|
||||||
|
currentId = null;
|
||||||
} else if(c == BinaryMarker.LargeObject.value) {
|
} else if(c == BinaryMarker.LargeObject.value) {
|
||||||
long size = decoder.decode();
|
long size = decoder.decode();
|
||||||
beginObject(size);
|
beginObject(size);
|
||||||
|
@@ -2,17 +2,21 @@ package net.woggioni.worth.serialization.json;
|
|||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import net.woggioni.worth.serialization.ValueDumper;
|
import net.woggioni.worth.serialization.ValueDumper;
|
||||||
|
import net.woggioni.worth.traversal.ValueIdentity;
|
||||||
import net.woggioni.worth.utils.WorthUtils;
|
import net.woggioni.worth.utils.WorthUtils;
|
||||||
import net.woggioni.worth.value.ArrayValue;
|
import net.woggioni.worth.value.*;
|
||||||
import net.woggioni.worth.value.ObjectValue;
|
|
||||||
import net.woggioni.worth.xface.Dumper;
|
import net.woggioni.worth.xface.Dumper;
|
||||||
import net.woggioni.worth.xface.Value;
|
import net.woggioni.worth.xface.Value;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class JSONDumper extends ValueDumper {
|
public class JSONDumper extends ValueDumper {
|
||||||
|
|
||||||
@@ -73,8 +77,18 @@ public class JSONDumper extends ValueDumper {
|
|||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void dump(Value value, Writer writer) {
|
public void dump(Value value, Writer writer) {
|
||||||
|
Map<ValueIdentity, Integer> ids;
|
||||||
|
Set<Integer> dumpedId;
|
||||||
|
if(cfg.serializeReferences) {
|
||||||
|
ids = getIdMap(value);
|
||||||
|
dumpedId = new HashSet<>();
|
||||||
|
} else {
|
||||||
|
ids = null;
|
||||||
|
dumpedId = null;
|
||||||
|
}
|
||||||
this.writer = writer;
|
this.writer = writer;
|
||||||
final Consumer<Value> handle_value = (v) -> {
|
final Consumer<Value> handle_value = (v) -> {
|
||||||
|
Integer id;
|
||||||
switch (v.type()) {
|
switch (v.type()) {
|
||||||
case NULL:
|
case NULL:
|
||||||
nullValue();
|
nullValue();
|
||||||
@@ -93,13 +107,33 @@ public class JSONDumper extends ValueDumper {
|
|||||||
break;
|
break;
|
||||||
case ARRAY:
|
case ARRAY:
|
||||||
ArrayValue arrayValue = WorthUtils.dynamicCast(v, ArrayValue.class);
|
ArrayValue arrayValue = WorthUtils.dynamicCast(v, ArrayValue.class);
|
||||||
|
if(ids != null && (id = ids.get(new ValueIdentity(arrayValue))) != null) {
|
||||||
|
if(dumpedId.add(id)) {
|
||||||
|
stack.push(new ArrayStackLevel(arrayValue));
|
||||||
|
valueId(id);
|
||||||
|
beginArray(arrayValue.size());
|
||||||
|
} else {
|
||||||
|
valueReference(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
stack.push(new ArrayStackLevel(arrayValue));
|
stack.push(new ArrayStackLevel(arrayValue));
|
||||||
beginArray(arrayValue.size());
|
beginArray(arrayValue.size());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case OBJECT:
|
case OBJECT:
|
||||||
ObjectValue objectValue = WorthUtils.dynamicCast(v, ObjectValue.class);
|
ObjectValue objectValue = WorthUtils.dynamicCast(v, ObjectValue.class);
|
||||||
|
if(ids != null && (id = ids.get(new ValueIdentity(objectValue))) != null) {
|
||||||
|
if(dumpedId.add(id)) {
|
||||||
|
stack.push(new ObjectStackLevel(WorthUtils.dynamicCast(v, ObjectValue.class)));
|
||||||
|
valueId(id);
|
||||||
|
beginObject(objectValue.size());
|
||||||
|
} else {
|
||||||
|
valueReference(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
stack.push(new ObjectStackLevel(WorthUtils.dynamicCast(v, ObjectValue.class)));
|
stack.push(new ObjectStackLevel(WorthUtils.dynamicCast(v, ObjectValue.class)));
|
||||||
beginObject(objectValue.size());
|
beginObject(objectValue.size());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -197,4 +231,16 @@ public class JSONDumper extends ValueDumper {
|
|||||||
protected void nullValue() {
|
protected void nullValue() {
|
||||||
this.writer.write("null");
|
this.writer.write("null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void valueId(int id) {
|
||||||
|
this.writer.write("(" + id + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void valueReference(int id) {
|
||||||
|
this.writer.write("$" + id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ public class JSONParser extends ValueParser {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final void parseNumber(LookAheadTextInputStream stream) {
|
private final String parseNumber(LookAheadTextInputStream stream) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
while (true) {
|
while (true) {
|
||||||
int b = stream.getCurrentByte();
|
int b = stream.getCurrentByte();
|
||||||
@@ -61,12 +61,31 @@ public class JSONParser extends ValueParser {
|
|||||||
}
|
}
|
||||||
stream.read();
|
stream.read();
|
||||||
}
|
}
|
||||||
String text = sb.toString();
|
return sb.toString();
|
||||||
if (text.indexOf('.') > 0) {
|
|
||||||
floatValue(Double.valueOf(text));
|
|
||||||
} else {
|
|
||||||
integerValue(Long.valueOf(text));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final int parseId(LookAheadTextInputStream stream) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean digitsStarted = false;
|
||||||
|
boolean digitsEnded = false;
|
||||||
|
while (true) {
|
||||||
|
int b = stream.getCurrentByte();
|
||||||
|
if(b == '(' ) {
|
||||||
|
} else if(Character.isWhitespace(b)) {
|
||||||
|
if(digitsStarted) digitsEnded = true;
|
||||||
|
} else if (b < 0 || b == ')') {
|
||||||
|
break;
|
||||||
|
} else if (isDecimal(b)) {
|
||||||
|
if(digitsEnded) {
|
||||||
|
error(ParseException::new, "error parsing id");
|
||||||
|
} else {
|
||||||
|
digitsStarted = true;
|
||||||
|
sb.appendCodePoint(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.read();
|
||||||
|
}
|
||||||
|
return Integer.valueOf(sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readString(LookAheadTextInputStream stream) {
|
private String readString(LookAheadTextInputStream stream) {
|
||||||
@@ -131,9 +150,9 @@ public class JSONParser extends ValueParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T extends RuntimeException> T error(Function<String, T> constructor, String fmt, Object... args) {
|
@Override
|
||||||
return constructor.apply(
|
protected <T extends RuntimeException> T error(Function<String, T> constructor, String fmt, Object ...args) {
|
||||||
String.format("Error at line %d column %d: %s",
|
return constructor.apply(String.format("Error at line %d column %d: %s",
|
||||||
currentLine, currentColumn, String.format(fmt, args)));
|
currentLine, currentColumn, String.format(fmt, args)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,22 +194,34 @@ public class JSONParser extends ValueParser {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Integer currentId = null;
|
||||||
while (true) {
|
while (true) {
|
||||||
int c = stream.getCurrentByte();
|
int c = stream.getCurrentByte();
|
||||||
if (c == -1) {
|
if (c == -1) {
|
||||||
break;
|
break;
|
||||||
} else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
} else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
||||||
|
} else if (c == '(') {
|
||||||
|
currentId = parseId(stream);
|
||||||
} else if (c == '{') {
|
} else if (c == '{') {
|
||||||
beginObject();
|
Value newObject = beginObject();
|
||||||
|
if(currentId != null) valueId(currentId, newObject);
|
||||||
|
currentId = null;
|
||||||
} else if (c == '}') {
|
} else if (c == '}') {
|
||||||
endObject();
|
endObject();
|
||||||
} else if (c == '[') {
|
} else if (c == '[') {
|
||||||
beginArray();
|
Value newArray = beginArray();
|
||||||
|
if(currentId != null) valueId(currentId, newArray);
|
||||||
|
currentId = null;
|
||||||
} else if (c == ']') {
|
} else if (c == ']') {
|
||||||
endArray();
|
endArray();
|
||||||
} else if (isDecimal(c)) {
|
} else if (isDecimal(c)) {
|
||||||
try {
|
try {
|
||||||
parseNumber(stream);
|
String text = parseNumber(stream);
|
||||||
|
if (text.indexOf('.') > 0) {
|
||||||
|
floatValue(Double.valueOf(text));
|
||||||
|
} else {
|
||||||
|
integerValue(Long.valueOf(text));
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
} catch (NumberFormatException nfe) {
|
} catch (NumberFormatException nfe) {
|
||||||
throw error(ParseException::new, nfe.getMessage());
|
throw error(ParseException::new, nfe.getMessage());
|
||||||
@@ -212,6 +243,11 @@ public class JSONParser extends ValueParser {
|
|||||||
} else if (c == 'n') {
|
} else if (c == 'n') {
|
||||||
consumeExpected(stream, "null", "Unrecognized null value");
|
consumeExpected(stream, "null", "Unrecognized null value");
|
||||||
nullValue();
|
nullValue();
|
||||||
|
} else if (idMap != null && c == '$') {
|
||||||
|
stream.read();
|
||||||
|
String text = parseNumber(stream);
|
||||||
|
valueReference(Integer.valueOf(text));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
stream.read();
|
stream.read();
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,26 @@
|
|||||||
|
package net.woggioni.worth.traversal;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.woggioni.worth.xface.Value;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ValueIdentity {
|
||||||
|
|
||||||
|
private final Value value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return System.identityHashCode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if(other == null) {
|
||||||
|
return false;
|
||||||
|
} else if(!(other instanceof ValueIdentity)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return value == ((ValueIdentity) other).value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,14 +1,16 @@
|
|||||||
package net.woggioni.worth.traversal;
|
package net.woggioni.worth.traversal;
|
||||||
|
|
||||||
import net.woggioni.worth.value.*;
|
import net.woggioni.worth.value.*;
|
||||||
|
import net.woggioni.worth.xface.Value;
|
||||||
|
|
||||||
public interface ValueVisitor {
|
public interface ValueVisitor {
|
||||||
|
|
||||||
void visit(ObjectValue value, TraversalContext ctx);
|
default void visit(ObjectValue value, TraversalContext ctx) {}
|
||||||
void visit(ArrayValue value, TraversalContext ctx);
|
default void visit(ArrayValue value, TraversalContext ctx) {}
|
||||||
void visit(BooleanValue value, TraversalContext ctx);
|
default void visit(BooleanValue value, TraversalContext ctx) {}
|
||||||
void visit(StringValue value, TraversalContext ctx);
|
default void visit(StringValue value, TraversalContext ctx) {}
|
||||||
void visit(IntegerValue value, TraversalContext ctx);
|
default void visit(IntegerValue value, TraversalContext ctx) {}
|
||||||
void visit(FloatValue value, TraversalContext ctx);
|
default void visit(FloatValue value, TraversalContext ctx) {}
|
||||||
void visit(NullValue value, TraversalContext ctx);
|
default void visit(NullValue value, TraversalContext ctx) {}
|
||||||
|
default boolean filter(Value value, TraversalContext ctx) { return true; }
|
||||||
}
|
}
|
@@ -108,10 +108,11 @@ public class ValueWalker {
|
|||||||
stack.add(ase);
|
stack.add(ase);
|
||||||
while(true) {
|
while(true) {
|
||||||
Value currentValue = stack.get(stack.size() - 1).next();
|
Value currentValue = stack.get(stack.size() - 1).next();
|
||||||
if((av = dynamicCast(currentValue, ArrayValue.class)) != null) {
|
if(visitor.filter(currentValue, ctx)) {
|
||||||
|
if ((av = dynamicCast(currentValue, ArrayValue.class)) != null) {
|
||||||
ase = new ArrayStackElement(av);
|
ase = new ArrayStackElement(av);
|
||||||
stack.add(ase);
|
stack.add(ase);
|
||||||
} else if((ov = dynamicCast(currentValue, ObjectValue.class)) != null) {
|
} else if ((ov = dynamicCast(currentValue, ObjectValue.class)) != null) {
|
||||||
ObjectStackElement ose = new ObjectStackElement(ov);
|
ObjectStackElement ose = new ObjectStackElement(ov);
|
||||||
stack.add(ose);
|
stack.add(ose);
|
||||||
} else {
|
} else {
|
||||||
@@ -120,11 +121,11 @@ public class ValueWalker {
|
|||||||
NullValue nv;
|
NullValue nv;
|
||||||
FloatValue fv;
|
FloatValue fv;
|
||||||
StringValue sv;
|
StringValue sv;
|
||||||
if((iv = dynamicCast(currentValue, IntegerValue.class)) != null) {
|
if ((iv = dynamicCast(currentValue, IntegerValue.class)) != null) {
|
||||||
visitor.visit(iv, ctx);
|
visitor.visit(iv, ctx);
|
||||||
} else if((fv = dynamicCast(currentValue, FloatValue.class)) != null) {
|
} else if ((fv = dynamicCast(currentValue, FloatValue.class)) != null) {
|
||||||
visitor.visit(fv, ctx);
|
visitor.visit(fv, ctx);
|
||||||
} else if((bv = dynamicCast(currentValue, BooleanValue.class)) != null) {
|
} else if ((bv = dynamicCast(currentValue, BooleanValue.class)) != null) {
|
||||||
visitor.visit(bv, ctx);
|
visitor.visit(bv, ctx);
|
||||||
} else if ((sv = dynamicCast(currentValue, StringValue.class)) != null) {
|
} else if ((sv = dynamicCast(currentValue, StringValue.class)) != null) {
|
||||||
visitor.visit(sv, ctx);
|
visitor.visit(sv, ctx);
|
||||||
@@ -132,6 +133,7 @@ public class ValueWalker {
|
|||||||
visitor.visit(nv, ctx);
|
visitor.visit(nv, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
while(true) {
|
while(true) {
|
||||||
if(stack.size() == 1) return;
|
if(stack.size() == 1) return;
|
||||||
int lastIndex = stack.size() - 1;
|
int lastIndex = stack.size() - 1;
|
||||||
|
@@ -5,6 +5,8 @@ import net.woggioni.worth.xface.Value;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@@ -27,20 +29,27 @@ public class WorthUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void writeObject2File(String fileName, Object o) {
|
public static void writeObject2File(String fileName, Object o) {
|
||||||
writeObject2File(new File(fileName), o);
|
writeObject2File(Paths.get(fileName), o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public static void writeObject2File(File file, Object o) {
|
public static void writeObject2File(Path file, Object o) {
|
||||||
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file.getPath()))) {
|
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file.toString()))) {
|
||||||
writer.write(o.toString());
|
writer.write(o.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public static String readFile2String(File file) {
|
public static void writeBytes2File(Path file, byte[] bytes) {
|
||||||
|
try (OutputStream os = new FileOutputStream(file.toString())) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public static String readFile2String(Path file) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
try (Reader reader = new InputStreamReader(new BufferedInputStream(new FileInputStream(file.getPath())))) {
|
try (Reader reader = new InputStreamReader(new BufferedInputStream(new FileInputStream(file.toString())))) {
|
||||||
char[] buffer = new char[1024];
|
char[] buffer = new char[1024];
|
||||||
while (true) {
|
while (true) {
|
||||||
int read = reader.read(buffer);
|
int read = reader.read(buffer);
|
||||||
|
@@ -100,6 +100,11 @@ public interface Value {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
public final int maxDepth =
|
public final int maxDepth =
|
||||||
Integer.parseInt(System.getProperty(Value.class.getName() + ".maxDepth", "1048576"));
|
Integer.parseInt(System.getProperty(Value.class.getName() + ".maxDepth", "1048576"));
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
public final boolean serializeReferences =
|
||||||
|
Boolean.parseBoolean(System.getProperty(
|
||||||
|
Value.class.getName() + ".serializeReferences", "false"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Configuration configuration = Configuration.builder().build();
|
Configuration configuration = Configuration.builder().build();
|
||||||
|
@@ -0,0 +1,62 @@
|
|||||||
|
package net.woggioni.worth.serialization;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.woggioni.worth.serialization.binary.JBONDumper;
|
||||||
|
import net.woggioni.worth.serialization.binary.JBONParser;
|
||||||
|
import net.woggioni.worth.serialization.json.JSONDumper;
|
||||||
|
import net.woggioni.worth.serialization.json.JSONParser;
|
||||||
|
import net.woggioni.worth.utils.WorthUtils;
|
||||||
|
import net.woggioni.worth.value.IntegerValue;
|
||||||
|
import net.woggioni.worth.value.ObjectValue;
|
||||||
|
import net.woggioni.worth.xface.Dumper;
|
||||||
|
import net.woggioni.worth.xface.Parser;
|
||||||
|
import net.woggioni.worth.xface.Value;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class ReferenceTest {
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void common(Function<Value.Configuration, Dumper> dumperConstructor,
|
||||||
|
Function<Value.Configuration, Parser> parserConstructor) {
|
||||||
|
Value.Configuration cfg = Value.Configuration.builder()
|
||||||
|
.serializeReferences(true)
|
||||||
|
.objectValueImplementation(ObjectValue.Implementation.HashMap)
|
||||||
|
.build();
|
||||||
|
Value value = ObjectValue.newInstance(cfg);
|
||||||
|
value.put("child", value);
|
||||||
|
value.put("id", new IntegerValue(25));
|
||||||
|
|
||||||
|
byte[] bytes;
|
||||||
|
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
Dumper dumper = dumperConstructor.apply(cfg);
|
||||||
|
dumper.dump(value, baos);
|
||||||
|
bytes = baos.toByteArray();
|
||||||
|
}
|
||||||
|
WorthUtils.writeBytes2File(Paths.get("/tmp/ciao.jbon"), bytes);
|
||||||
|
Value reparsedValue;
|
||||||
|
try(ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) {
|
||||||
|
Parser parser = parserConstructor.apply(cfg);
|
||||||
|
reparsedValue = parser.parse(bais);
|
||||||
|
}
|
||||||
|
Assert.assertEquals(reparsedValue, reparsedValue.get("child"));
|
||||||
|
Assert.assertEquals(value.get("id"), reparsedValue.get("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SneakyThrows
|
||||||
|
public void json() {
|
||||||
|
common(JSONDumper::new, JSONParser::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SneakyThrows
|
||||||
|
public void jbon() {
|
||||||
|
common(JBONDumper::new, JBONParser::new);
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,9 @@ import net.woggioni.worth.buffer.LookAheadTextInputStream;
|
|||||||
import net.woggioni.worth.exception.NotImplementedException;
|
import net.woggioni.worth.exception.NotImplementedException;
|
||||||
import net.woggioni.worth.utils.WorthUtils;
|
import net.woggioni.worth.utils.WorthUtils;
|
||||||
import net.woggioni.worth.value.ArrayValue;
|
import net.woggioni.worth.value.ArrayValue;
|
||||||
|
import net.woggioni.worth.value.IntegerValue;
|
||||||
import net.woggioni.worth.value.ObjectValue;
|
import net.woggioni.worth.value.ObjectValue;
|
||||||
|
import net.woggioni.worth.xface.Dumper;
|
||||||
import net.woggioni.worth.xface.Parser;
|
import net.woggioni.worth.xface.Parser;
|
||||||
import net.woggioni.worth.xface.Value;
|
import net.woggioni.worth.xface.Value;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
@@ -20,6 +22,7 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class JSONTest {
|
public class JSONTest {
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user