diff --git a/build.sbt b/build.sbt index 469b74a..d4ba1e3 100644 --- a/build.sbt +++ b/build.sbt @@ -26,10 +26,6 @@ libraryDependencies += "org.antlr" % "antlr4" % "4.7.1" % Test libraryDependencies += "org.antlr" % "antlr4-runtime" % "4.7.1" % Test libraryDependencies += "org.tukaani" % "xz" % "1.8" % Test -artifactName := { (sv: ScalaVersion, module: ModuleID, artifact: Artifact) => - artifact.name + "-" + module.revision + "." + artifact.extension -} - enablePlugins(Antlr4Plugin) antlr4Version in Antlr4 := "4.7.1" antlr4PackageName in Antlr4 := Some("net.woggioni.worth.antlr") diff --git a/src/main/java/net/woggioni/worth/serialization/ValueDumper.java b/src/main/java/net/woggioni/worth/serialization/ValueDumper.java index c3ee3d0..ab44bb4 100644 --- a/src/main/java/net/woggioni/worth/serialization/ValueDumper.java +++ b/src/main/java/net/woggioni/worth/serialization/ValueDumper.java @@ -2,8 +2,11 @@ package net.woggioni.worth.serialization; import lombok.RequiredArgsConstructor; import net.woggioni.worth.exception.NotImplementedException; -import net.woggioni.worth.value.ArrayValue; -import net.woggioni.worth.value.ObjectValue; +import net.woggioni.worth.traversal.TraversalContext; +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.Value; @@ -11,9 +14,9 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; -import java.util.ArrayDeque; -import java.util.Iterator; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; public abstract class ValueDumper implements Dumper { @@ -25,6 +28,47 @@ public abstract class ValueDumper implements Dumper { public final Value value; } + protected Map getIdMap(Value root) { + Map 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 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 { private final Iterator iterator = ((ArrayValue) value).iterator(); @@ -110,4 +154,8 @@ public abstract class ValueDumper implements Dumper { protected abstract void booleanValue(boolean value); protected abstract void nullValue(); + + protected abstract void valueId(int id); + + protected abstract void valueReference(int id); } diff --git a/src/main/java/net/woggioni/worth/serialization/ValueParser.java b/src/main/java/net/woggioni/worth/serialization/ValueParser.java index 52d61f7..fa19a33 100644 --- a/src/main/java/net/woggioni/worth/serialization/ValueParser.java +++ b/src/main/java/net/woggioni/worth/serialization/ValueParser.java @@ -3,6 +3,7 @@ package net.woggioni.worth.serialization; import lombok.RequiredArgsConstructor; import net.woggioni.worth.exception.MaxDepthExceededException; import net.woggioni.worth.exception.NotImplementedException; +import net.woggioni.worth.exception.ParseException; import net.woggioni.worth.utils.WorthUtils; import net.woggioni.worth.value.*; import net.woggioni.worth.xface.Parser; @@ -11,8 +12,12 @@ import net.woggioni.worth.xface.Value; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.lang.reflect.Array; import java.nio.charset.Charset; 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; @@ -27,9 +32,6 @@ public class ValueParser implements Parser { } protected static class ArrayStackLevel extends StackLevel { - public ArrayStackLevel() { - super(new ArrayValue(), -1); - } public ArrayStackLevel(long expectedSize) { super(new ArrayValue(), expectedSize); } @@ -38,16 +40,13 @@ public class ValueParser implements Parser { protected static class ObjectStackLevel extends StackLevel { public String currentKey; - public ObjectStackLevel(Value.Configuration cfg) { - super(ObjectValue.newInstance(cfg), -1); - } - public ObjectStackLevel(Value.Configuration cfg, long expectedSize) { super(ObjectValue.newInstance(cfg), expectedSize); } } protected ArrayDeque stack; + protected Map idMap; private void add2Last(Value value) { StackLevel last = stack.getFirst(); @@ -67,17 +66,20 @@ public class ValueParser implements Parser { protected ValueParser(Value.Configuration cfg) { this.cfg = cfg; + if (cfg.serializeReferences) { + idMap = new HashMap<>(); + } stack = new ArrayDeque<>() { @Override public void push(StackLevel stackLevel) { - if(size() == cfg.maxDepth) { + if (size() == cfg.maxDepth) { 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); } }; - stack.push(new ArrayStackLevel()); + stack.push(new ArrayStackLevel(-1)); } @Override @@ -95,29 +97,40 @@ public class ValueParser implements Parser { return parse(new InputStreamReader(stream, encoding)); } - protected void beginObject() { - stack.push(new ObjectStackLevel(cfg)); + protected Value beginObject() { + return beginObject(-1); } - protected void beginObject(long size) { - stack.push(new ObjectStackLevel(cfg, size)); + protected Value beginObject(long size) { + ObjectStackLevel osl = new ObjectStackLevel(cfg, size); + stack.push(osl); + return osl.value; } - 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() { - stack.push(new ArrayStackLevel()); + protected Value beginArray() { + return beginArray(-1); } - protected void beginArray(long size) { - stack.push(new ArrayStackLevel(size)); + protected Value beginArray(long size) { + ArrayStackLevel ale = new ArrayStackLevel(size); + stack.push(ale); + return ale.value; } 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) { @@ -144,4 +157,20 @@ public class ValueParser implements Parser { protected void nullValue() { 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 error(Function constructor, String fmt, Object... args) { + throw new NotImplementedException("Method not implemented"); + } } diff --git a/src/main/java/net/woggioni/worth/serialization/binary/JBONDumper.java b/src/main/java/net/woggioni/worth/serialization/binary/JBONDumper.java index 5dfdd58..84877af 100644 --- a/src/main/java/net/woggioni/worth/serialization/binary/JBONDumper.java +++ b/src/main/java/net/woggioni/worth/serialization/binary/JBONDumper.java @@ -4,6 +4,7 @@ import lombok.SneakyThrows; import net.woggioni.worth.exception.NotImplementedException; import net.woggioni.worth.serialization.ValueDumper; import net.woggioni.worth.serialization.json.JSONDumper; +import net.woggioni.worth.traversal.ValueIdentity; import net.woggioni.worth.utils.Leb128; import net.woggioni.worth.utils.WorthUtils; import net.woggioni.worth.value.ArrayValue; @@ -13,7 +14,9 @@ import net.woggioni.worth.xface.Value; import java.io.OutputStream; import java.io.Writer; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; public class JBONDumper extends ValueDumper { @@ -44,8 +47,18 @@ public class JBONDumper extends ValueDumper { @Override @SneakyThrows public void dump(Value value, OutputStream outputStream) { + Map ids; + Set dumpedId; + if(cfg.serializeReferences) { + ids = getIdMap(value); + dumpedId = new HashSet<>(); + } else { + ids = null; + dumpedId = null; + } this.os = outputStream; final Consumer handle_value = (v) -> { + Integer id; switch (v.type()) { case NULL: nullValue(); @@ -64,13 +77,33 @@ public class JBONDumper extends ValueDumper { break; case ARRAY: ArrayValue arrayValue = WorthUtils.dynamicCast(v, ArrayValue.class); - stack.push(new ArrayStackLevel(arrayValue)); - beginArray(arrayValue.size()); + 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)); + beginArray(arrayValue.size()); + } break; case OBJECT: ObjectValue objectValue = WorthUtils.dynamicCast(v, ObjectValue.class); - stack.push(new ObjectStackLevel(objectValue)); - beginObject(objectValue.size()); + 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))); + beginObject(objectValue.size()); + } break; } }; @@ -191,4 +224,18 @@ public class JBONDumper extends ValueDumper { protected void nullValue() { 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); + } } diff --git a/src/main/java/net/woggioni/worth/serialization/binary/JBONParser.java b/src/main/java/net/woggioni/worth/serialization/binary/JBONParser.java index 7c3537b..02e60cb 100644 --- a/src/main/java/net/woggioni/worth/serialization/binary/JBONParser.java +++ b/src/main/java/net/woggioni/worth/serialization/binary/JBONParser.java @@ -18,7 +18,8 @@ public class JBONParser extends ValueParser { private int cursor = 0; - private T error(Function constructor, String fmt, Object... args) { + @Override + protected T error(Function constructor, String fmt, Object... args) { return constructor.apply( String.format("Error at position %d: %s", cursor, String.format(fmt, args))); @@ -38,6 +39,7 @@ public class JBONParser extends ValueParser { stream.read(); try { + Integer currentId = null; Leb128.Leb128Decoder decoder = new Leb128.Leb128Decoder(stream); ObjectStackLevel osl; while (true) { @@ -52,7 +54,11 @@ public class JBONParser extends ValueParser { if(c == -1) { 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(); } else if(c == BinaryMarker.True.value) { booleanValue(true); @@ -75,17 +81,26 @@ public class JBONParser extends ValueParser { stream.read(buffer); String text = new String(buffer); stringValue(text); - } else if(c == BinaryMarker.EmptyArray.value) { - beginArray(0); - } else if(c > BinaryMarker.EmptyArray.value && c < BinaryMarker.LargeArray.value) { - beginArray(c - BinaryMarker.EmptyArray.value); - } else if(c == BinaryMarker.LargeArray.value) { - long size = decoder.decode(); - beginArray(size); - } else if(c == BinaryMarker.EmptyObject.value) { - beginObject(0); - } else if(c > BinaryMarker.EmptyObject.value && c < BinaryMarker.LargeObject.value) { - beginObject(c - BinaryMarker.EmptyObject.value); + } else if(c >= BinaryMarker.EmptyArray.value && c <= BinaryMarker.LargeArray.value) { + long size; + if(c == BinaryMarker.LargeArray.value) { + size = decoder.decode(); + } else { + size = c - BinaryMarker.EmptyArray.value; + } + Value newArray = beginArray(size); + if(currentId != null) valueId(currentId, newArray); + currentId = null; + } 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) { long size = decoder.decode(); beginObject(size); diff --git a/src/main/java/net/woggioni/worth/serialization/json/JSONDumper.java b/src/main/java/net/woggioni/worth/serialization/json/JSONDumper.java index a208dda..e1b13f5 100644 --- a/src/main/java/net/woggioni/worth/serialization/json/JSONDumper.java +++ b/src/main/java/net/woggioni/worth/serialization/json/JSONDumper.java @@ -2,17 +2,21 @@ package net.woggioni.worth.serialization.json; import lombok.SneakyThrows; import net.woggioni.worth.serialization.ValueDumper; +import net.woggioni.worth.traversal.ValueIdentity; import net.woggioni.worth.utils.WorthUtils; -import net.woggioni.worth.value.ArrayValue; -import net.woggioni.worth.value.ObjectValue; +import net.woggioni.worth.value.*; import net.woggioni.worth.xface.Dumper; import net.woggioni.worth.xface.Value; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; public class JSONDumper extends ValueDumper { @@ -73,8 +77,18 @@ public class JSONDumper extends ValueDumper { @Override @SneakyThrows public void dump(Value value, Writer writer) { + Map ids; + Set dumpedId; + if(cfg.serializeReferences) { + ids = getIdMap(value); + dumpedId = new HashSet<>(); + } else { + ids = null; + dumpedId = null; + } this.writer = writer; final Consumer handle_value = (v) -> { + Integer id; switch (v.type()) { case NULL: nullValue(); @@ -93,13 +107,33 @@ public class JSONDumper extends ValueDumper { break; case ARRAY: ArrayValue arrayValue = WorthUtils.dynamicCast(v, ArrayValue.class); - stack.push(new ArrayStackLevel(arrayValue)); - beginArray(arrayValue.size()); + 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)); + beginArray(arrayValue.size()); + } break; case OBJECT: ObjectValue objectValue = WorthUtils.dynamicCast(v, ObjectValue.class); - stack.push(new ObjectStackLevel(WorthUtils.dynamicCast(v, ObjectValue.class))); - beginObject(objectValue.size()); + 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))); + beginObject(objectValue.size()); + } break; } }; @@ -197,4 +231,16 @@ public class JSONDumper extends ValueDumper { protected void nullValue() { 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); + } } diff --git a/src/main/java/net/woggioni/worth/serialization/json/JSONParser.java b/src/main/java/net/woggioni/worth/serialization/json/JSONParser.java index 5b42899..7df1cc5 100644 --- a/src/main/java/net/woggioni/worth/serialization/json/JSONParser.java +++ b/src/main/java/net/woggioni/worth/serialization/json/JSONParser.java @@ -48,7 +48,7 @@ public class JSONParser extends ValueParser { return result; } - private final void parseNumber(LookAheadTextInputStream stream) { + private final String parseNumber(LookAheadTextInputStream stream) { StringBuilder sb = new StringBuilder(); while (true) { int b = stream.getCurrentByte(); @@ -61,12 +61,31 @@ public class JSONParser extends ValueParser { } stream.read(); } - String text = sb.toString(); - if (text.indexOf('.') > 0) { - floatValue(Double.valueOf(text)); - } else { - integerValue(Long.valueOf(text)); + return sb.toString(); + } + + 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) { @@ -131,10 +150,10 @@ public class JSONParser extends ValueParser { } } - private T error(Function constructor, String fmt, Object... args) { - return constructor.apply( - String.format("Error at line %d column %d: %s", - currentLine, currentColumn, String.format(fmt, args))); + @Override + protected T error(Function constructor, String fmt, Object ...args) { + return constructor.apply(String.format("Error at line %d column %d: %s", + currentLine, currentColumn, String.format(fmt, args))); } public static Parser newInstance() { @@ -175,22 +194,34 @@ public class JSONParser extends ValueParser { }; try { + Integer currentId = null; while (true) { int c = stream.getCurrentByte(); if (c == -1) { break; } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + } else if (c == '(') { + currentId = parseId(stream); } else if (c == '{') { - beginObject(); + Value newObject = beginObject(); + if(currentId != null) valueId(currentId, newObject); + currentId = null; } else if (c == '}') { endObject(); } else if (c == '[') { - beginArray(); + Value newArray = beginArray(); + if(currentId != null) valueId(currentId, newArray); + currentId = null; } else if (c == ']') { endArray(); } else if (isDecimal(c)) { try { - parseNumber(stream); + String text = parseNumber(stream); + if (text.indexOf('.') > 0) { + floatValue(Double.valueOf(text)); + } else { + integerValue(Long.valueOf(text)); + } continue; } catch (NumberFormatException nfe) { throw error(ParseException::new, nfe.getMessage()); @@ -212,6 +243,11 @@ public class JSONParser extends ValueParser { } else if (c == 'n') { consumeExpected(stream, "null", "Unrecognized null value"); nullValue(); + } else if (idMap != null && c == '$') { + stream.read(); + String text = parseNumber(stream); + valueReference(Integer.valueOf(text)); + continue; } stream.read(); } diff --git a/src/main/java/net/woggioni/worth/traversal/ValueIdentity.java b/src/main/java/net/woggioni/worth/traversal/ValueIdentity.java new file mode 100644 index 0000000..d34b1a6 --- /dev/null +++ b/src/main/java/net/woggioni/worth/traversal/ValueIdentity.java @@ -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; + } + } +} diff --git a/src/main/java/net/woggioni/worth/traversal/ValueVisitor.java b/src/main/java/net/woggioni/worth/traversal/ValueVisitor.java index 35fd165..93e687c 100644 --- a/src/main/java/net/woggioni/worth/traversal/ValueVisitor.java +++ b/src/main/java/net/woggioni/worth/traversal/ValueVisitor.java @@ -1,14 +1,16 @@ package net.woggioni.worth.traversal; import net.woggioni.worth.value.*; +import net.woggioni.worth.xface.Value; public interface ValueVisitor { - void visit(ObjectValue value, TraversalContext ctx); - void visit(ArrayValue value, TraversalContext ctx); - void visit(BooleanValue value, TraversalContext ctx); - void visit(StringValue value, TraversalContext ctx); - void visit(IntegerValue value, TraversalContext ctx); - void visit(FloatValue value, TraversalContext ctx); - void visit(NullValue value, TraversalContext ctx); + default void visit(ObjectValue value, TraversalContext ctx) {} + default void visit(ArrayValue value, TraversalContext ctx) {} + default void visit(BooleanValue value, TraversalContext ctx) {} + default void visit(StringValue value, TraversalContext ctx) {} + default void visit(IntegerValue value, TraversalContext ctx) {} + default void visit(FloatValue value, TraversalContext ctx) {} + default void visit(NullValue value, TraversalContext ctx) {} + default boolean filter(Value value, TraversalContext ctx) { return true; } } \ No newline at end of file diff --git a/src/main/java/net/woggioni/worth/traversal/ValueWalker.java b/src/main/java/net/woggioni/worth/traversal/ValueWalker.java index 4736a9a..514604f 100644 --- a/src/main/java/net/woggioni/worth/traversal/ValueWalker.java +++ b/src/main/java/net/woggioni/worth/traversal/ValueWalker.java @@ -108,28 +108,30 @@ public class ValueWalker { stack.add(ase); while(true) { Value currentValue = stack.get(stack.size() - 1).next(); - if((av = dynamicCast(currentValue, ArrayValue.class)) != null) { - ase = new ArrayStackElement(av); - stack.add(ase); - } else if((ov = dynamicCast(currentValue, ObjectValue.class)) != null) { - ObjectStackElement ose = new ObjectStackElement(ov); - stack.add(ose); - } else { - IntegerValue iv; - BooleanValue bv; - NullValue nv; - FloatValue fv; - StringValue sv; - if((iv = dynamicCast(currentValue, IntegerValue.class)) != null) { - visitor.visit(iv, ctx); - } else if((fv = dynamicCast(currentValue, FloatValue.class)) != null) { - visitor.visit(fv, ctx); - } else if((bv = dynamicCast(currentValue, BooleanValue.class)) != null) { - visitor.visit(bv, ctx); - } else if ((sv = dynamicCast(currentValue, StringValue.class)) != null) { - visitor.visit(sv, ctx); - } else if ((nv = dynamicCast(currentValue, NullValue.class)) != null) { - visitor.visit(nv, ctx); + if(visitor.filter(currentValue, ctx)) { + if ((av = dynamicCast(currentValue, ArrayValue.class)) != null) { + ase = new ArrayStackElement(av); + stack.add(ase); + } else if ((ov = dynamicCast(currentValue, ObjectValue.class)) != null) { + ObjectStackElement ose = new ObjectStackElement(ov); + stack.add(ose); + } else { + IntegerValue iv; + BooleanValue bv; + NullValue nv; + FloatValue fv; + StringValue sv; + if ((iv = dynamicCast(currentValue, IntegerValue.class)) != null) { + visitor.visit(iv, ctx); + } else if ((fv = dynamicCast(currentValue, FloatValue.class)) != null) { + visitor.visit(fv, ctx); + } else if ((bv = dynamicCast(currentValue, BooleanValue.class)) != null) { + visitor.visit(bv, ctx); + } else if ((sv = dynamicCast(currentValue, StringValue.class)) != null) { + visitor.visit(sv, ctx); + } else if ((nv = dynamicCast(currentValue, NullValue.class)) != null) { + visitor.visit(nv, ctx); + } } } while(true) { diff --git a/src/main/java/net/woggioni/worth/utils/WorthUtils.java b/src/main/java/net/woggioni/worth/utils/WorthUtils.java index a005007..37da852 100644 --- a/src/main/java/net/woggioni/worth/utils/WorthUtils.java +++ b/src/main/java/net/woggioni/worth/utils/WorthUtils.java @@ -5,6 +5,8 @@ import net.woggioni.worth.xface.Value; import java.io.*; import java.lang.reflect.Constructor; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -27,20 +29,27 @@ public class WorthUtils { } public static void writeObject2File(String fileName, Object o) { - writeObject2File(new File(fileName), o); + writeObject2File(Paths.get(fileName), o); } @SneakyThrows - public static void writeObject2File(File file, Object o) { - try (Writer writer = new OutputStreamWriter(new FileOutputStream(file.getPath()))) { + public static void writeObject2File(Path file, Object o) { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file.toString()))) { writer.write(o.toString()); } } @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(); - 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]; while (true) { int read = reader.read(buffer); diff --git a/src/main/java/net/woggioni/worth/xface/Value.java b/src/main/java/net/woggioni/worth/xface/Value.java index 5a71625..c88a49c 100644 --- a/src/main/java/net/woggioni/worth/xface/Value.java +++ b/src/main/java/net/woggioni/worth/xface/Value.java @@ -100,6 +100,11 @@ public interface Value { @Builder.Default public final int maxDepth = 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(); diff --git a/src/test/java/net/woggioni/worth/serialization/ReferenceTest.java b/src/test/java/net/woggioni/worth/serialization/ReferenceTest.java new file mode 100644 index 0000000..fd22d0f --- /dev/null +++ b/src/test/java/net/woggioni/worth/serialization/ReferenceTest.java @@ -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 dumperConstructor, + Function 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); + } +} diff --git a/src/test/java/net/woggioni/worth/serialization/json/JSONTest.java b/src/test/java/net/woggioni/worth/serialization/json/JSONTest.java index f7c4aef..e6d57e9 100644 --- a/src/test/java/net/woggioni/worth/serialization/json/JSONTest.java +++ b/src/test/java/net/woggioni/worth/serialization/json/JSONTest.java @@ -8,7 +8,9 @@ import net.woggioni.worth.buffer.LookAheadTextInputStream; import net.woggioni.worth.exception.NotImplementedException; import net.woggioni.worth.utils.WorthUtils; import net.woggioni.worth.value.ArrayValue; +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; @@ -20,6 +22,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.util.Map; +import java.util.Objects; public class JSONTest {