diff --git a/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoder.java b/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoder.java index 4b347b8..ee47c57 100644 --- a/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoder.java +++ b/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoder.java @@ -30,6 +30,7 @@ public class JSONCoder { public static JSONCoder get() { return getGlobal(); } public static String stringify(Object obj) { return get().encode(obj); } + public static String stringify(Object obj, int indentFact) { return encode(obj, JSONCoderOption.ofIndentFactor(indentFact)); } public static T parse(String str, Class cls) { return get().decode(str, cls); } @SuppressWarnings("unchecked") diff --git a/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoderOption.java b/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoderOption.java index a8290fc..c8d0b12 100644 --- a/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoderOption.java +++ b/JSONCoder/src/main/java/org/jsonex/jsoncoder/JSONCoderOption.java @@ -22,6 +22,7 @@ import org.jsonex.jsoncoder.coder.*; import org.jsonex.jsoncoder.fieldTransformer.FieldTransformer; import org.jsonex.jsoncoder.fieldTransformer.FieldTransformer.FieldInfo; +import org.jsonex.treedoc.TDNode; import org.jsonex.treedoc.json.TDJSONOption; import org.slf4j.Logger; @@ -137,7 +138,7 @@ private static SimpleDateFormat buildDateFormat(String format, TimeZone timeZone @Getter private final List> equalsWrapper = new ArrayList<>(); // JSON coder config - @Getter @Setter private TDJSONOption jsonOption = new TDJSONOption(); + @Getter @Setter private TDJSONOption jsonOption = TDJSONOption.ofDefaultRootType(TDNode.Type.SIMPLE); public enum LogLevel { OFF { public void log(Logger log, String msg, Throwable e) { /* Noop */ }}, diff --git a/JSONEX.md b/JSONEX.md index e0d48fe..6e94b49 100644 --- a/JSONEX.md +++ b/JSONEX.md @@ -75,12 +75,13 @@ line2`, FeatureJSONexJSON optional json top level braces - object - a:1 + a:1,b:2 ```json { - "a":1 + "a": 1, + "b": 2 } ``` @@ -95,6 +96,20 @@ line2`, ``` + + optional json top level braces - array of objects + {a:1},{b:2},c + + +```json +[ + {"a": 1}, + {"b": 2}, + "c" +] +``` + + @@ -130,6 +145,42 @@ line2`, ``` + + Optional Quotes + {a: value1, b: value2} + + +```json +{"a": "value1", "b": "value2"} +``` + + + + Commas are allowed for last element + {a: 1, b: 2, } + + +```json +{"a": 1, "b": 2} +``` + + + + Multi-line value +
{
+    a: 
+      `abc 
+       line2`, 
+    b: 2 
+  }
+ + +```json +{"a": "abc\nline2", "b": 2} +``` + + + diff --git a/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONOption.java b/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONOption.java index 24571a6..ed2d615 100644 --- a/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONOption.java +++ b/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONOption.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; +import org.jsonex.core.type.Nullable; import org.jsonex.treedoc.TDNode; import java.net.URI; @@ -43,8 +44,11 @@ public enum TextType {OPERATOR, KEY, STRING, NON_STRING} URI uri; // Used for JSONParser - /** In case there's no enclosed '[' of '{' on the root level, the default type. */ - TDNode.Type defaultRootType = TDNode.Type.SIMPLE; + /** + * In case there's no enclosed '[' of '{' on the root level, the default type. + * By default, it will try to interpreter as a single value (either map, if there's ":", or a simple value.) + */ + @Nullable TDNode.Type defaultRootType; // Used for JSONWriter int indentFactor; @@ -113,7 +117,7 @@ public TDJSONOption setDeliminatorArray(String start, String end) { @Setter(AccessLevel.NONE) @Getter(AccessLevel.NONE) Collection _termKeyStrs; public void buildTerms() { - _termValue = "\n\r" + deliminatorObjectStart; // support tree with a type in the form of "type{attr1:val1}" + _termValue = "\n\r" + deliminatorKey + deliminatorObjectStart; // support tree with a type in the form of "type{attr1:val1}", key1:key2:type{att1:val1} _termKey = deliminatorObjectStart + deliminatorObjectEnd + deliminatorArrayStart; _termValueStrs = new ArrayList<>(); _termKeyStrs = new ArrayList<>(); @@ -129,7 +133,7 @@ public void buildTerms() { else _termKeyStrs.add(deliminatorKey); - _termValueInMap = _termValue + deliminatorObjectEnd; + _termValueInMap = _termValue + deliminatorObjectEnd + deliminatorArrayEnd; // It's possible object end is omitted for path compression. e.g [a:b:c] _termValueInArray = _termValue + deliminatorArrayEnd; } } diff --git a/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONParser.java b/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONParser.java index dce8003..a8db2b4 100644 --- a/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONParser.java +++ b/treedoc/src/main/java/org/jsonex/treedoc/json/TDJSONParser.java @@ -41,7 +41,7 @@ public TDNode parseAll(CharSource src, TDJSONOption opt) { TreeDoc doc = TreeDoc.ofArray(); int docId = 0; while(src.skipSpacesAndReturnsAndCommas()) - TDJSONParser.get().parse(src, new TDJSONOption().setDocId(docId++), doc.getRoot().createChild()); + TDJSONParser.get().parse(src, TDJSONOption.ofDefaultRootType(TDNode.Type.MAP).setDocId(docId++), doc.getRoot().createChild()); return doc.getRoot(); } @@ -64,7 +64,7 @@ public TDNode parse(CharSource src, TDJSONOption opt, TDNode node, boolean isRoo if (contains(opt.deliminatorArrayStart, c)) return parseArray(src, opt, node, true); - if (isRoot) { + if (isRoot && opt.defaultRootType != null) { switch (opt.defaultRootType) { case MAP: return parseMap(src, opt, node, false); @@ -82,17 +82,29 @@ public TDNode parse(CharSource src, TDJSONOption opt, TDNode node, boolean isRoo return node.setValue(sb.toString()); } + if (isRoot && opt.defaultRootType == TDNode.Type.SIMPLE) { + return node.setValue(ClassUtil.toSimpleObject(src.readUntil("\r\n"))); + } + String term = opt._termValue; if (node.getParent() != null) // parent.type can either be ARRAY or MAP. term = node.getParent().getType() == TDNode.Type.ARRAY ? opt._termValueInArray : opt._termValueInMap; String str = src.readUntil(term, opt._termValueStrs).trim(); - node.setValue(ClassUtil.toSimpleObject(str)); + if (!src.isEof() && contains(opt.deliminatorKey, src.peek())) { // it's a path compression such as: a:b:c,d:e -> {a: {b: c}} + src.skip(); + node.setType(TDNode.Type.MAP); + parse(src, opt, node.createChild(str), false); + return node; + } + if (!src.isEof() && contains(opt.deliminatorObjectStart, src.peek())) { // A value with type in the form of `type{attr1:val1:attr2:val2} - node.createChild(opt.KEY_TYPE).setValue(node.getValue()); + node.createChild(opt.KEY_TYPE).setValue(str); return parseMap(src, opt, node, true); } + // A simple value + node.setValue(ClassUtil.toSimpleObject(str)); return node; } finally { node.setEnd(src.getBookmark()); diff --git a/treedoc/src/test/java/org/jsonex/treedoc/json/TDJsonParserTest.java b/treedoc/src/test/java/org/jsonex/treedoc/json/TDJsonParserTest.java index 2edf79c..a0d08aa 100644 --- a/treedoc/src/test/java/org/jsonex/treedoc/json/TDJsonParserTest.java +++ b/treedoc/src/test/java/org/jsonex/treedoc/json/TDJsonParserTest.java @@ -181,12 +181,13 @@ public class TDJsonParserTest { } private final static String EXPECTED_STREAM_MERGE_RESULT = - "[{a: 1, obj: {$id: '1_0'}, ref: {$ref: '#1_0'}}, {b: 2, obj: {$id: '1_1'}, ref: {$ref: '#1_1'}}, 'a:1', 'b:2']"; + "[{a: 1, obj: {$id: '1_0'}, ref: {$ref: '#1_0'}}, {b: 2, obj: {$id: '1_1'}, ref: {$ref: '#1_1'}}, {a: 1, b: 2}]"; @Test public void testStream() { ReaderCharSource reader = new ReaderCharSource(loadResource(this.getClass(), "stream.json")); List nodes = new ArrayList<>(); - while(reader.skipSpacesAndReturnsAndCommas()) - nodes.add(TDJSONParser.get().parse(reader)); + while(reader.skipSpacesAndReturnsAndCommas()) { + nodes.add(TDJSONParser.get().parse(reader, TDJSONOption.ofDefaultRootType(TDNode.Type.MAP))); + } TDNode node = TreeDoc.merge(nodes).getRoot(); log.info("testStream=" + node.toString()); assertEquals("1", node.getChild(1).getKey()); @@ -215,7 +216,7 @@ public class TDJsonParserTest { .setDeliminatorArray("<", ">"); TDNode node = TDJSONParser.get().parse(str, opt); assertEquals("{a: 'va', c: {d: 23, strs: ['a', 'b']}}", node.toString()); - assertEquals("(a=\"va\";c=(d=23;strs=<\"a\";\"b\">))", TDJSONWriter.get().writeAsString(node, opt.setAlwaysQuoteName(false))); + assertEquals("(a=`va`;c=(d=23;strs=<`a`;`b`>))", TDJSONWriter.get().writeAsString(node, opt.setAlwaysQuoteName(false).setQuoteChar('`'))); } private static void parseWithException(String str, String expectedError) { @@ -250,9 +251,24 @@ private static void parseWithException(String str, String expectedError) { @Test public void testParseObjectToString() { TestCls test = new TestCls("va", new TestCls1(23, new String[]{"a", "b"})); String str = test.toString(); // TDJsonParserTest.TestCls(a=va, c=TDJsonParserTest.TestCls1(d=23, strs=[a, b])) - log.info("testParseObjectToString: str=" + str); - TDNode node = TDJSONParser.get().parse(str, new TDJSONOption().setDeliminatorObject("(", ")").setDeliminatorKey("=") ); - assertEquals("{$type:\"TDJsonParserTest.TestCls\",a:\"va\",c:{$type:\"TDJsonParserTest.TestCls1\",d:23,strs:[\"a\",\"b\"]}}", TDJSONWriter.get().writeAsString(node, new TDJSONOption().setAlwaysQuoteName(false))); + TDJSONOption opt = new TDJSONOption().setDeliminatorObject("(", ")").setDeliminatorKey("="); + testParse(str, opt, "{$type:\"TDJsonParserTest.TestCls\",a:\"va\",c:{$type:\"TDJsonParserTest.TestCls1\",d:23,strs:[\"a\",\"b\"]}}"); + } + + @Test public void testParsePathCompression() { + TDJSONOption opt = new TDJSONOption(); + testParse("a:b:123", opt, "{a:{b:123}}"); + testParse("[h:i, j:k]", opt, "[{h:'i'},{j:'k'}]"); + testParse("a:b:{e:123, f:g:[h:i, j:k]}", opt, "{a:{b:{e:123,f:{g:[{h:'i'},{j:'k'}]}}}}"); + testParse("a:b:123,c:d:456", opt, "{a:{b:123}}"); // Default, read a single map + testParse("a:b:123,c:d:456", opt.setDefaultRootType(TDNode.Type.MAP), "{a:{b:123},c:{d:456}}"); + testParse("a:b:123,c:d:456", opt.setDefaultRootType(TDNode.Type.ARRAY), "[{a:{b:123}},{c:{d:456}}]"); + testParse("a:b:123,a:d:456", opt.setDefaultRootType(TDNode.Type.MAP), "{a:[{b:123},{d:456}]}"); + } + + private void testParse(String str, TDJSONOption opt, String expectedJson) { + TDNode node = TDJSONParser.get().parse(str, opt); + assertEquals(expectedJson, TDJSONWriter.get().writeAsString(node, new TDJSONOption().setAlwaysQuoteName(false).setQuoteChar('\''))); } @Data