added reference serialization and deserialization in both json (text) and jbon (binary) format

This commit is contained in:
2019-07-26 12:42:35 +01:00
parent 89fb8ac1b7
commit ff9b26ed93
14 changed files with 426 additions and 100 deletions

View File

@@ -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")

View File

@@ -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);
} }

View File

@@ -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");
}
} }

View File

@@ -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);
stack.push(new ArrayStackLevel(arrayValue)); if(ids != null && (id = ids.get(new ValueIdentity(arrayValue))) != null) {
beginArray(arrayValue.size()); if(dumpedId.add(id)) {
stack.push(new ArrayStackLevel(arrayValue));
valueId(id);
beginArray(arrayValue.size());
} else {
valueReference(id);
}
} else {
stack.push(new ArrayStackLevel(arrayValue));
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) {
beginObject(objectValue.size()); 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)));
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);
}
} }

View File

@@ -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);

View File

@@ -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);
stack.push(new ArrayStackLevel(arrayValue)); if(ids != null && (id = ids.get(new ValueIdentity(arrayValue))) != null) {
beginArray(arrayValue.size()); if(dumpedId.add(id)) {
stack.push(new ArrayStackLevel(arrayValue));
valueId(id);
beginArray(arrayValue.size());
} else {
valueReference(id);
}
} else {
stack.push(new ArrayStackLevel(arrayValue));
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(WorthUtils.dynamicCast(v, ObjectValue.class))); if(ids != null && (id = ids.get(new ValueIdentity(objectValue))) != null) {
beginObject(objectValue.size()); 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)));
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);
}
} }

View File

@@ -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 { private final int parseId(LookAheadTextInputStream stream) {
integerValue(Long.valueOf(text)); 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,10 +150,10 @@ 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)));
} }
public static Parser newInstance() { public static Parser newInstance() {
@@ -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();
} }

View File

@@ -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;
}
}
}

View File

@@ -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; }
} }

View File

@@ -108,28 +108,30 @@ 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)) {
ase = new ArrayStackElement(av); if ((av = dynamicCast(currentValue, ArrayValue.class)) != null) {
stack.add(ase); ase = new ArrayStackElement(av);
} else if((ov = dynamicCast(currentValue, ObjectValue.class)) != null) { stack.add(ase);
ObjectStackElement ose = new ObjectStackElement(ov); } else if ((ov = dynamicCast(currentValue, ObjectValue.class)) != null) {
stack.add(ose); ObjectStackElement ose = new ObjectStackElement(ov);
} else { stack.add(ose);
IntegerValue iv; } else {
BooleanValue bv; IntegerValue iv;
NullValue nv; BooleanValue bv;
FloatValue fv; NullValue nv;
StringValue sv; FloatValue fv;
if((iv = dynamicCast(currentValue, IntegerValue.class)) != null) { StringValue sv;
visitor.visit(iv, ctx); if ((iv = dynamicCast(currentValue, IntegerValue.class)) != null) {
} else if((fv = dynamicCast(currentValue, FloatValue.class)) != null) { visitor.visit(iv, ctx);
visitor.visit(fv, ctx); } else if ((fv = dynamicCast(currentValue, FloatValue.class)) != null) {
} else if((bv = dynamicCast(currentValue, BooleanValue.class)) != null) { visitor.visit(fv, ctx);
visitor.visit(bv, ctx); } else if ((bv = dynamicCast(currentValue, BooleanValue.class)) != null) {
} else if ((sv = dynamicCast(currentValue, StringValue.class)) != null) { visitor.visit(bv, ctx);
visitor.visit(sv, ctx); } else if ((sv = dynamicCast(currentValue, StringValue.class)) != null) {
} else if ((nv = dynamicCast(currentValue, NullValue.class)) != null) { visitor.visit(sv, ctx);
visitor.visit(nv, ctx); } else if ((nv = dynamicCast(currentValue, NullValue.class)) != null) {
visitor.visit(nv, ctx);
}
} }
} }
while(true) { while(true) {

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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 {