diff --git a/.gitignore b/.gitignore index ee3c2c8..6f0fef2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ gradle gradlew gradle.bat build/ +out/ hjson.iml .idea diff --git a/README.md b/README.md index 9b1b3bc..708880f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ -# hjson-java +# PersonTheCat/hjson-java + +## Notice of deprecation +This repository is being replaced with a new ecosystem: [XJS](https://github.com/exjson). The new ecosystem will primarily focus on its own syntax and data storage format, but will provide compatibility with Hjson, YAML, JSON-C, and other formats through `xjs-compat`. It is also built from the ground up to support streams, type coercion, and provides a number of other utilities which I believe will make for an extremely convenient experience. If your code is based on this fork of Hjson, I recommend migrating as soon as possible. + +# hjson-java + [![Build Status](https://img.shields.io/travis/hjson/hjson-java.svg?style=flat-square)](http://travis-ci.org/hjson/hjson-java) [![Maven Central](https://img.shields.io/maven-central/v/org.hjson/hjson.svg?style=flat-square)](http://search.maven.org/#search|ga|1|g%3A%22org.hjson%22%20a%3A%22hjson%22) [![Javadoc](https://javadoc-emblem.rhcloud.com/doc/org.hjson/hjson/badge.svg?style=flat-square&color=blue)](http://www.javadoc.io/doc/org.hjson/hjson) diff --git a/assets/comments1_result.hjson b/assets/comments1_result.hjson new file mode 100644 index 0000000..bbf564e --- /dev/null +++ b/assets/comments1_result.hjson @@ -0,0 +1,74 @@ +# Headers placed in this location will now be +# correctly saved by hjson-java! +{ + # Comments can be placed above values. + numPenguins: 417 # And beside values. + // Multiple comment styles are supported. + anotherNum: 24 + randString: "info" # Even with quotes and commas. + /* + There's a lot of info here. + Here's another line. + And another line. + */ + complicatedObject: + { + apples: 12 + bananas: 14 + /* + a fancy + interior comment + for you + */ + } + # Objects and arrays placed on a single line are + # considered "condensed" and will remain that way. + arrayOfNums: [ 1, 2, 3, 4, 5 ] + # Objects and arrays with multiple values per- + # line will continue to have multiple values + # per-line, based on the average line length. + multiLine: + [ + 1, 2, 3 + 4, 5, 6 + ] + # These should get averaged to 2. + averageLines: + [ + 1, 2 + 3, 4 + 5, 6 + ] + # This works especially well for multi-dimensional + # arrays (i.e. matrices). + matrix: + [ + [ 1, 2, 3 ] + [ 4, 5, 6 ] + [ 7, 8, 9 ] + /* + Another comment + goes here + */ + ] + # Objects will do the same. Keys and values will + # be correctly quoted, when necessary. + multiLineObj: + { + value1: 1, value2: 2 + value3: "three", value4: 4 + } + # Unnecessary quotes will be removed. + unnecessaryQuotes: + { + numPenguins: 36 + numPolarBears: twenty-four + } + # Just to verify that these still work. + multiLineString: + ''' + test + and more test + and still more + ''' +} \ No newline at end of file diff --git a/assets/comments1_result.json b/assets/comments1_result.json new file mode 100644 index 0000000..fd0054e --- /dev/null +++ b/assets/comments1_result.json @@ -0,0 +1,67 @@ +{ + "numPenguins": 417, + "anotherNum": 24, + "randString": "info", + "complicatedObject": + { + "apples": 12, + "bananas": 14 + }, + "arrayOfNums": + [ + 1, + 2, + 3, + 4, + 5 + ], + "multiLine": + [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "averageLines": + [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "matrix": + [ + [ + 1, + 2, + 3 + ], + [ + 4, + 5, + 6 + ], + [ + 7, + 8, + 9 + ] + ], + "multiLineObj": + { + "value1": 1, + "value2": 2, + "value3": "three", + "value4": 4 + }, + "unnecessaryQuotes": + { + "numPenguins": 36, + "numPolarBears": "twenty-four" + }, + "multiLineString": "test\nand more test\nand still more" +} \ No newline at end of file diff --git a/assets/comments1_test.hjson b/assets/comments1_test.hjson new file mode 100644 index 0000000..11d0447 --- /dev/null +++ b/assets/comments1_test.hjson @@ -0,0 +1,74 @@ +# Headers placed in this location will now be +# correctly saved by hjson-java! +{ + # Comments can be placed above values. + numPenguins: 417 # And beside values. + // Multiple comment styles are supported. + anotherNum: 24 + randString: "info", # Even with quotes and commas. + /* + There's a lot of info here. + Here's another line. + And another line. + */ + complicatedObject: + { + apples: 12 + bananas: 14 + /* + a fancy + interior comment + for you + */ + } + # Objects and arrays placed on a single line are + # considered "condensed" and will remain that way. + arrayOfNums: [ 1, 2, 3, 4, 5 ] + # Objects and arrays with multiple values per- + # line will continue to have multiple values + # per-line, based on the average line length. + multiLine: + [ + 1, 2, 3 + 4, 5, 6 + ] + # These should get averaged to 2. + averageLines: + [ + 1 + 2, 3 + 4, 5, 6 + ] + # This works especially well for multi-dimensional + # arrays (i.e. matrices). + matrix: + [ + [ 1, 2, 3 ] + [ 4, 5, 6 ] + [ 7, 8, 9 ] + /* + Another comment + goes here + */ + ] + # Objects will do the same. Keys and values will + # be correctly quoted, when necessary. + multiLineObj: + { + value1: 1, value2: 2 + value3: "three", value4: 4 + } + # Unnecessary quotes will be removed. + unnecessaryQuotes: + { + "numPenguins": 36, + "numPolarBears": "twenty-four" + } + # Just to verify that these still work. + multiLineString: + ''' + test + and more test + and still more + ''' +} \ No newline at end of file diff --git a/assets/legacy_result.hjson b/assets/legacy_result.hjson new file mode 100644 index 0000000..e5c55e8 --- /dev/null +++ b/assets/legacy_result.hjson @@ -0,0 +1,9 @@ +{ + num: 55 + dec: 55.5 + obj: + { + value: 55 + } + array: [ 55 ] +} \ No newline at end of file diff --git a/assets/legacy_result.json b/assets/legacy_result.json new file mode 100644 index 0000000..32ab595 --- /dev/null +++ b/assets/legacy_result.json @@ -0,0 +1,12 @@ +{ + "num": 55, + "dec": 55.5, + "obj": + { + "value": 55 + }, + "array": + [ + 55 + ] +} \ No newline at end of file diff --git a/assets/legacy_test.hjson b/assets/legacy_test.hjson new file mode 100644 index 0000000..79e91f2 --- /dev/null +++ b/assets/legacy_test.hjson @@ -0,0 +1,7 @@ +num: 55 +dec: 55.5 +obj: +{ + value: 55 +} +array: [ 55 ] \ No newline at end of file diff --git a/assets/mltabs_result.hjson b/assets/mltabs_result.hjson index 4b11b84..72e3bc2 100644 --- a/assets/mltabs_result.hjson +++ b/assets/mltabs_result.hjson @@ -1,5 +1,5 @@ { - foo: + foo: ''' bar joe oki doki diff --git a/assets/pass1_result.hjson b/assets/pass1_result.hjson index 2a3f4c9..df9f3b0 100644 --- a/assets/pass1_result.hjson +++ b/assets/pass1_result.hjson @@ -1,11 +1,6 @@ [ JSON Test Pattern pass1 - { - "object with 1 member": - [ - array with 1 element - ] - } + { "object with 1 member": [ "array with 1 element" ] } {} [] -42 @@ -42,24 +37,10 @@ "# -- --> */": " " " s p a c e d ": [ - 1 - 2 - 3 - 4 - 5 - 6 - 7 - ] - compact: - [ - 1 - 2 - 3 - 4 - 5 - 6 - 7 + 1, 2, 3 + 4, 5, 6 ] + compact: [ 1, 2, 3, 4, 5, 6 ] jsontext: '''{"object with 1 member":["array with 1 element"]}''' quotes: " " %22 0x22 034 " "/\\\"쫾몾ꮘﳞ볚\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": A key can be any string diff --git a/assets/pass1_result.json b/assets/pass1_result.json index 69b354d..1925717 100644 --- a/assets/pass1_result.json +++ b/assets/pass1_result.json @@ -45,8 +45,7 @@ 3, 4, 5, - 6, - 7 + 6 ], "compact": [ 1, @@ -54,8 +53,7 @@ 3, 4, 5, - 6, - 7 + 6 ], "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", "quotes": "" \" %22 0x22 034 "", diff --git a/assets/pass1_test.json b/assets/pass1_test.json index 61cfd90..543bd9f 100644 --- a/assets/pass1_test.json +++ b/assets/pass1_test.json @@ -37,9 +37,9 @@ "# -- --> */": " ", " s p a c e d " :[1,2 , 3 -, + , -4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], +4 , 5 , 6 ,],"compact":[1,2,3,4,5,6], "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", "quotes": "" \u0022 %22 0x22 034 "", "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" diff --git a/assets/pass2_result.hjson b/assets/pass2_result.hjson index 5a9fd5e..17cce1d 100644 --- a/assets/pass2_result.hjson +++ b/assets/pass2_result.hjson @@ -1,39 +1 @@ -[ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - [ - Not too deep - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] -] \ No newline at end of file +[ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ "Not too deep" ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] \ No newline at end of file diff --git a/assets/comments_result.hjson b/assets/rmcomments_result.hjson similarity index 100% rename from assets/comments_result.hjson rename to assets/rmcomments_result.hjson diff --git a/assets/comments_result.json b/assets/rmcomments_result.json similarity index 100% rename from assets/comments_result.json rename to assets/rmcomments_result.json diff --git a/assets/comments_test.hjson b/assets/rmcomments_test.hjson similarity index 100% rename from assets/comments_test.hjson rename to assets/rmcomments_test.hjson diff --git a/assets/sameline_result.hjson b/assets/sameline_result.hjson new file mode 100644 index 0000000..fdd0528 --- /dev/null +++ b/assets/sameline_result.hjson @@ -0,0 +1,11 @@ +{ + obj: { + inner: { value: true } + } + array: [ + { + inner: [ 0, 80 ] + num: 33 + } + ] +} diff --git a/assets/sameline_result.json b/assets/sameline_result.json new file mode 100644 index 0000000..c149357 --- /dev/null +++ b/assets/sameline_result.json @@ -0,0 +1,20 @@ +{ + "obj": + { + "inner": + { + "value": true + } + }, + "array": + [ + { + "inner": + [ + 0, + 80 + ], + "num": 33 + } + ] +} \ No newline at end of file diff --git a/assets/sameline_test.hjson b/assets/sameline_test.hjson new file mode 100644 index 0000000..fdd0528 --- /dev/null +++ b/assets/sameline_test.hjson @@ -0,0 +1,11 @@ +{ + obj: { + inner: { value: true } + } + array: [ + { + inner: [ 0, 80 ] + num: 33 + } + ] +} diff --git a/assets/strings_result.hjson b/assets/strings_result.hjson index 6569fe7..73fb766 100644 --- a/assets/strings_result.hjson +++ b/assets/strings_result.hjson @@ -9,19 +9,19 @@ notml2: " \n" notml3: "\n \n \n \n" notml4: "\t\n" - multiline1: + multiline1: ''' first line indented line last line ''' - multiline2: + multiline2: ''' first line indented line last line ''' - multiline3: + multiline3: ''' first line indented line @@ -51,21 +51,7 @@ yes: true no: false null: null - array: - [ - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 0 - -1 - 0.5 - ] + array: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, -1, 0.5 ] } special: { diff --git a/assets/testlist.txt b/assets/testlist.txt index 49adf34..445aaa9 100644 --- a/assets/testlist.txt +++ b/assets/testlist.txt @@ -1,5 +1,5 @@ charset_test.hjson -comments_test.hjson +comments1_test.hjson empty_test.hjson failCharset1_test.hjson failJSON02_test.json @@ -65,6 +65,7 @@ failStr7a_test.hjson failStr8a_test.hjson kan_test.hjson keys_test.hjson +legacy_test.hjson mltabs_test.json oa_test.hjson pass1_test.json @@ -72,6 +73,8 @@ pass2_test.json pass3_test.json pass4_test.json passSingle_test.hjson +rmcomments_test.hjson +sameline_test.hjson stringify1_test.hjson strings2_test.hjson strings_test.hjson diff --git a/build.gradle b/build.gradle index 0de0cc3..d349ebc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'java' -version = '3.0.0' +version = '3.0.0-C11' group = 'org.hjson' description = """Hjson, the Human JSON.""" @@ -13,13 +13,13 @@ sourceSets { srcDir 'src/main' } } - test { - java { - srcDir 'src/test' - } - resources { - srcDir 'assets' - } + test { + java { + srcDir 'src/test' + } + resources { + srcDir 'assets' + } } } diff --git a/src/main/org/hjson/CommentStyle.java b/src/main/org/hjson/CommentStyle.java new file mode 100644 index 0000000..cc48fea --- /dev/null +++ b/src/main/org/hjson/CommentStyle.java @@ -0,0 +1,24 @@ +package org.hjson; + +/** + * This class represents the various comment styles that can be used when adding + * new comments to a {@link JsonValue}. + */ +public enum CommentStyle { + /** + * Hash style comments, indicated by placing a # symbol at the + * start of the comment, followed by the beginning of each new line. + */ + HASH, + /** + * The C style line comment, indicated by placing a // at the + * start of the comment, followed by the beginning of each new line. + */ + LINE, + /** + * A block style comment, indicated by placing a /* at the + * start of the comment, followed by a * / (with no spaces) at + * the very end of the comment. + */ + BLOCK +} diff --git a/src/main/org/hjson/CommentType.java b/src/main/org/hjson/CommentType.java new file mode 100644 index 0000000..de6d53f --- /dev/null +++ b/src/main/org/hjson/CommentType.java @@ -0,0 +1,26 @@ +package org.hjson; + +/** + * This class represents the specific type of comment to be used in conjunction with a {@link JsonValue}. + * Comments can be placed by calling {@link JsonValue#setComment(CommentType, CommentStyle, String)} + * or another such variant. + */ +public enum CommentType { + /** + * Indicates that this comment precedes the value that it is paired with, to be placed one line + * before the value. In the case of parent or root objects, this type indicates that the comment + * is a header inside of the json file. + */ + BOL, + /** + * Indicates that this comment follows the value that it is paired with, to be placed at the end + * of the line. In the case of parent or root objects, this type indicates that the comment is a + * footer inside of the json file. + */ + EOL, + /** + * Indicates that this comment falls anywhere else in association with this value. This usually + * implies that the value is inside of an empty object or array. + */ + INTERIOR +} diff --git a/src/main/org/hjson/HjsonDsf.java b/src/main/org/hjson/HjsonDsf.java index 9c704ba..1885f6b 100644 --- a/src/main/org/hjson/HjsonDsf.java +++ b/src/main/org/hjson/HjsonDsf.java @@ -21,8 +21,6 @@ ******************************************************************************/ package org.hjson; -import java.util.*; -import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -44,11 +42,6 @@ private HjsonDsf() {} * @return DSF provider */ public static IHjsonDsfProvider hex(boolean stringify) { return new DsfHex(stringify); } - // /** - // * Returns a date DSF provider. - // * @return DSF provider - // */ - // public static IHjsonDsfProvider date() { return new DsfDate(); } static boolean isInvalidDsfChar(char c) { diff --git a/src/main/org/hjson/HjsonOptions.java b/src/main/org/hjson/HjsonOptions.java index b4e10a5..a1e74a3 100644 --- a/src/main/org/hjson/HjsonOptions.java +++ b/src/main/org/hjson/HjsonOptions.java @@ -27,11 +27,28 @@ public class HjsonOptions { private IHjsonDsfProvider[] dsf; + private boolean outputComments; + private boolean outputEmptyLines; private boolean legacyRoot; + private boolean bracesSameLine; + private boolean allowCondense; + private boolean allowMultiVal; + private boolean emitRootBraces; + private String space, commentSpace; + private String newLine; public HjsonOptions() { dsf=new IHjsonDsfProvider[0]; legacyRoot=true; + bracesSameLine=false; + allowCondense=true; + allowMultiVal=true; + emitRootBraces=false; + space=" "; + commentSpace=""; + outputComments=false; + outputEmptyLines=false; + newLine=JsonValue.eol; } /** @@ -39,45 +56,250 @@ public HjsonOptions() { * * @return providers. */ - public IHjsonDsfProvider[] getDsfProviders() { return dsf.clone(); } + public IHjsonDsfProvider[] getDsfProviders() { + return dsf.clone(); + } /** * Sets the DSF providers. * * @param value value + * @return this, to enable chaining */ - public void setDsfProviders(IHjsonDsfProvider[] value) { dsf=value.clone(); } + public HjsonOptions setDsfProviders(IHjsonDsfProvider... value) { + dsf = value.clone(); + return this; + } /** * Detects whether objects without root braces are supported. * * @return true if this feature is enabled. */ - public boolean getParseLegacyRoot() { return legacyRoot; } + public boolean getParseLegacyRoot() { + return legacyRoot; + } /** * Sets whether root braces should be emitted. * * @param value value + * @return this, to enable chaining */ - public void setParseLegacyRoot(boolean value) { legacyRoot=value; } + public HjsonOptions setParseLegacyRoot(boolean value) { + legacyRoot = value; + return this; + } /** * Detects whether root braces should be emitted. * - * @deprecated will always return true. * @return true if this feature is enabled. */ - @Deprecated - public boolean getEmitRootBraces() { return true; } + public boolean getEmitRootBraces() { + return emitRootBraces; + } /** * Sets whether root braces should be emitted. * - * @deprecated root braces are always emitted. * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setEmitRootBraces(boolean value) { + emitRootBraces = value; + return this; + } + + /** + * Detects whether braces and brackets should be placed on new lines. + * + * @return whether braces and brackets follow the KR / Java syntax. + */ + public boolean bracesSameLine() { return bracesSameLine; } + + /** + * Sets whether braces and brackets should be placed on new lines. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setBracesSameLine(boolean value) { + bracesSameLine = value; + return this; + } + + /** + * Detects whether more than one value is ever allowed on a single line. + * + * @return true if more than one value is allowed. + */ + public boolean getAllowMultiVal() { + return allowMultiVal; + } + + /** + * Sets whether more than one value is ever allowed to be placed on a single line. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setAllowMultiVal(boolean value) { + allowMultiVal = value; + return this; + } + + /** + * Detects whether objects an arrays are allowed to be displayed on a single line. + * + * @return true if objects and arrays can be displayed on a single line. + */ + public boolean getAllowCondense() { + return allowCondense; + } + + /** + * Sets whether objects and arrays can be displayed on a single line. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setAllowCondense(boolean value) { + allowCondense = value; + return this; + } + + /** + * Gets the characters to be placed per-level on each new line. + * + * @return the number of spaces. + */ + public String getSpace() { return space; } + + /** + * Sets the characters to be placed per-level on each new line. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setSpace(String value) { + space = value; + return this; + } + + /** + * Sets the number of spaces to be placed per-level on each new line. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setSpace(int value) { + space = numSpaces(value); + return this; + } + + /** + * Gets the characters to be placed before comments on new lines. + * + * @return the number of spaces + */ + public String getCommentSpace() { + return commentSpace; + } + + /** + * Sets the characters to be placed before comments on new lines. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setCommentSpace(String value) { + commentSpace = value; + return this; + } + + /** + * Gets the new line character(s) to be output by the writer. + * + * @return the new line character(s) + */ + public String getNewLine() { + return this.newLine; + } + + /** + * Sets the new line character(s) to be output by the writer. + * + * @param nl The new line characters to be used + * @return this, to enable chaining + */ + public HjsonOptions setNewLine(String nl) { + if ("\n".equals(nl) || "\r\n".equals(nl)) newLine = nl; + return this; + } + + /** + * Sets the number of spaces to be placed before comments on new lines. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setCommentSpace(int value) { + commentSpace = numSpaces(value); + return this; + } + + /** + * Generates a String object based on the input number of spaces. + * + * @param value value + * @return a string containing the input number of spaces. + */ + private String numSpaces(int value) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < value; i++) { + sb.append(' '); + } + return sb.toString(); + } + + /** + * Sets whether comments are enabled on the output. + * + * @param value value + * @return this, to enable chaining */ - @Deprecated - public void setEmitRootBraces(boolean value) { } + public HjsonOptions setOutputComments(boolean value) { + outputComments = value; + return this; + } + /** + * Gets whether comments are enabled on the output. + * + * @return whether comments are enabled + */ + public boolean getOutputComments() { + return outputComments; + } + + /** + * Sets whether comments are enabled on the output. + * + * @param value value + * @return this, to enable chaining + */ + public HjsonOptions setOutputEmptyLines(boolean value) { + outputEmptyLines = value; + return this; + } + + /** + * Gets whether comments are enabled on the output. + * + * @return whether comments are enabled + */ + public boolean getOutputEmptyLines() { + return outputEmptyLines; + } } diff --git a/src/main/org/hjson/HjsonParser.java b/src/main/org/hjson/HjsonParser.java index fec6336..d13ee06 100644 --- a/src/main/org/hjson/HjsonParser.java +++ b/src/main/org/hjson/HjsonParser.java @@ -1,4 +1,5 @@ -/******************************************************************************* +/* + ****************************************************************************** * Copyright (c) 2013, 2015 EclipseSource. * Copyright (c) 2015-2017 Christian Zangl * @@ -24,11 +25,9 @@ import java.io.*; +// Todo: Rewrite class HjsonParser { - private static final int MIN_BUFFER_SIZE=10; - private static final int DEFAULT_BUFFER_SIZE=1024; - private final String buffer; private Reader reader; private int index; @@ -37,9 +36,8 @@ class HjsonParser { private int current; private StringBuilder captureBuffer, peek; private boolean capture; - private boolean legacyRoot; - - private IHjsonDsfProvider[] dsfProviders; + private final boolean legacyRoot; + private final IHjsonDsfProvider[] dsfProviders; HjsonParser(String string, HjsonOptions options) { buffer=string; @@ -57,7 +55,7 @@ class HjsonParser { this(readToEnd(reader), options); } - static String readToEnd(Reader reader) throws IOException { + private static String readToEnd(Reader reader) throws IOException { // read everything into a buffer int n; char[] part=new char[8*1024]; @@ -66,21 +64,27 @@ static String readToEnd(Reader reader) throws IOException { return sb.toString(); } - void reset() { + private void reset() { index=lineOffset=current=0; line=1; peek=new StringBuilder(); - reader=new StringReader(buffer); + // Don't lose the final character. + reader=new StringReader(buffer + ' '); capture=false; captureBuffer=null; } JsonValue parse() throws IOException { - // Braces for the root object are optional - read(); - skipWhiteSpace(); + String header=readBetweenVals(); + JsonValue value = tryParse(); + value.setFullComment(CommentType.BOL, header); + return value; + } + + private JsonValue tryParse() throws IOException { + // Braces for the root object are optional if (legacyRoot) { switch (current) { case '[': @@ -89,15 +93,18 @@ JsonValue parse() throws IOException { default: try { // assume we have a root object without braces - return checkTrailing(readObject(true)); + return checkTrailing(readObject(false)); } catch (Exception exception) { // test if we are dealing with a single JSON value instead (true/false/null/num/"") reset(); read(); - skipWhiteSpace(); - try { return checkTrailing(readValue()); } - catch (Exception exception2) { } - throw exception; // throw original error + readBetweenVals(); + try { + return checkTrailing(readValue()); + } catch (Exception exception2) { + // throw original error + throw exception; + } } } } else { @@ -105,8 +112,8 @@ JsonValue parse() throws IOException { } } - JsonValue checkTrailing(JsonValue v) throws ParseException, IOException { - skipWhiteSpace(); + private JsonValue checkTrailing(JsonValue v) throws ParseException, IOException { + v.setFullComment(CommentType.EOL, readBetweenVals()); if (!isEndOfText()) throw error("Extra characters in input: "+current); return v; } @@ -116,7 +123,7 @@ private JsonValue readValue() throws IOException { case '\'': case '"': return readString(); case '[': return readArray(); - case '{': return readObject(false); + case '{': return readObject(true); default: return readTfnns(); } } @@ -133,18 +140,18 @@ private JsonValue readTfnns() throws IOException { read(); boolean isEol=current<0 || current=='\r' || current=='\n'; if (isEol || current==',' || - current=='}' || current==']' || - current=='#' || - current=='/' && (peek()=='/' || peek()=='*') - ) { + current=='}' || current==']' || + current=='#' || + current=='/' && (peek()=='/' || peek()=='*') + ) { switch (first) { case 'f': case 'n': case 't': String svalue=value.toString().trim(); - if (svalue.equals("false")) return JsonValue.FALSE; - else if (svalue.equals("null")) return JsonValue.NULL; - else if (svalue.equals("true")) return JsonValue.TRUE; + if (svalue.equals("false")) return JsonLiteral.jsonFalse(); + else if (svalue.equals("null")) return JsonLiteral.jsonNull(); + else if (svalue.equals("true")) return JsonLiteral.jsonTrue(); break; default: if (first=='-' || first>='0' && first<='9') { @@ -161,46 +168,129 @@ private JsonValue readTfnns() throws IOException { } } + private JsonObject readObject(boolean expectCloser) throws IOException { + // Skip the opening brace. + if (expectCloser) read(); + JsonObject object=new JsonObject(); + boolean compact=isContainerCompact(expectCloser); + ContainerData data=new ContainerData(compact); + + while (true) { + // Comment above / before name. + String bol=readBetweenVals(); + + if (checkEndOfContainer("object", '}', expectCloser)) { + // Because we reached the end, we learned + // that this was an interior comment. + object.setFullComment(CommentType.INTERIOR, bol); + break; + } + // Name comes next. + String name=readName(); + + // Colon and potential surrounding spaces. + // Comments will be fully ignored, here. + readBetweenVals(); + if (!readIf(':')) { + throw expected("':'"); + } + readBetweenVals(); + + // The value itself. + JsonValue value=readValue(); + + finishContainerElement(data, value); + // Set comments and add. + value.setFullComment(CommentType.BOL, bol); + object.add(name, value); + } + return data.into(object); + } + private JsonArray readArray() throws IOException { - read(); + read(); // Clear the opening bracket. JsonArray array=new JsonArray(); - skipWhiteSpace(); - if (readIf(']')) { - return array; - } + boolean compact=isContainerCompact(true); + ContainerData data=new ContainerData(compact); + while (true) { - skipWhiteSpace(); - array.add(readValue()); - skipWhiteSpace(); - if (readIf(',')) skipWhiteSpace(); // , is optional - if (readIf(']')) break; - else if (isEndOfText()) throw error("End of input while parsing an array (did you forget a closing ']'?)"); + // Any comments above / before value. + String bol=readBetweenVals(); + + if (checkEndOfContainer("array", ']', true)) { + // Because we reached the end, we learned + // that this was an interior comment. + array.setFullComment(CommentType.INTERIOR, bol); + break; + } + // The value must be next. + JsonValue value=readValue(); + value.setFullComment(CommentType.BOL, bol); + + finishContainerElement(data, value); + // Successfully parsed a value. + array.add(value); } - return array; + return data.into(array); } - private JsonObject readObject(boolean objectWithoutBraces) throws IOException { - if (!objectWithoutBraces) read(); - JsonObject object=new JsonObject(); - skipWhiteSpace(); - while (true) { - if (objectWithoutBraces) { - if (isEndOfText()) break; - } else { - if (isEndOfText()) throw error("End of input while parsing an object (did you forget a closing '}'?)"); - if (readIf('}')) break; + private void finishContainerElement(ContainerData data, JsonValue value) throws IOException { + int delimiter, numCommas=0; + while ((delimiter=readNextDelimiter())==',') { + skipToNL(); + numCommas++; + } + // We should now be at the eol. + if (delimiter=='\n') { // Reached eol. + data.nl(); // Update the data to reflect this. + // Check for a comma on the next line. + // Also skip empty lines. + while ((delimiter=readNextDelimiter())>0) { + if (delimiter==',') { + data.overrideCondensed(); // Can no longer be treated as condensed. + numCommas++; + } } - String name=readName(); - skipWhiteSpace(); - if (!readIf(':')) { - throw expected("':'"); + } else { // Did not reach eol. + // There was something else on this line. + // See if it was an EOL #. + String eol=readBetweenVals(true); + if (!eol.isEmpty()) { // This is an EOL #. + value.setFullComment(CommentType.EOL, eol); + } else { // There's another value on this line. + data.incrLineLength(); } - skipWhiteSpace(); - object.add(name, readValue()); - skipWhiteSpace(); - if (readIf(',')) skipWhiteSpace(); // , is optional } - return object; + if (numCommas>1) throw error("Extra comma detected. Unclear element"); + } + + private boolean checkEndOfContainer(String type, char closer, boolean expectCloser) throws IOException { + if (isEndOfText()) { + if (expectCloser) throw error("End of input while parsing an "+type+". Did you forget a closing'"+closer+"'?"); + return true; + } + if (expectCloser) return readIf(closer); + return false; + } + + private int readNextDelimiter() throws IOException { + skipToNL(); + int delimiter=current; + if (delimiter=='\n' || delimiter==',') { + read(); + return delimiter; + } + return -1; + } + + private boolean isContainerCompact(boolean expectCloser) throws IOException { + skipToNL(); + readIf('\r'); + // The object is compact if there is non-whitespace + // on the same line. If any further values are placed + // on subsequent lines, they will most likely look + // better this way, anyway. + return current!='\n' && expectCloser; } private String readName() throws IOException { @@ -213,7 +303,7 @@ private String readName() throws IOException { if (name.length()==0) throw error("Found ':' but no key name (for an empty key name use quotes)"); else if (space>=0 && space!=name.length()) { index=start+space; throw error("Found whitespace in your key name (use quotes to include)"); } return name.toString(); - } else if (isWhiteSpace(current)) { + } else if (isWhitespace(current)) { if (space<0) space=name.length(); } else if (current<' ') { throw error("Name is not closed"); @@ -225,7 +315,6 @@ private String readName() throws IOException { } private String readMlString() throws IOException { - // Parse a multiline string value. StringBuilder sb=new StringBuilder(); int triple=0; @@ -235,7 +324,7 @@ private String readMlString() throws IOException { // skip white/to (newline) for (; ; ) { - if (isWhiteSpace(current) && current!='\n') read(); + if (isWhitespace(current) && current!='\n') read(); else break; } if (current=='\n') { read(); skipIndent(indent); } @@ -273,7 +362,7 @@ else if (current=='\'') { private void skipIndent(int indent) throws IOException { while (indent-->0) { - if (isWhiteSpace(current) && current!='\n') read(); + if (isWhitespace(current) && current!='\n') read(); else break; } } @@ -283,7 +372,7 @@ private JsonValue readString() throws IOException { } private String readStringInternal(boolean allowML) throws IOException { - // callees make sure that (current=='"' || current=='\'') + // callers make sure that (current=='"' || current=='\'') int exitCh = current; read(); startCapture(); @@ -349,7 +438,7 @@ private static boolean isDigit(char ch) { return ch>='0' && ch<='9'; } - static JsonValue tryParseNumber(StringBuilder value, boolean stopAtNext) throws IOException { + static JsonValue tryParseNumber(StringBuilder value, boolean stopAtNext) { int idx=0, len=value.length(); if (idx=0 && current!='\n'); + if (toEOL) break; + else read(); } else if (current=='/' && peek()=='*') { + int commentOffset=index-lineOffset-1; read(); do { read(); + if (current=='\n') { + read(); + // Make sure that we still only skip whitespace. + // Spacing may be different on each line. + skipIfWhiteSpace(commentOffset); + } } while (current>=0 && !(current=='*' && peek()=='/')); read(); read(); + // Don't cut these values apart. + while (current=='\r' || current=='\n') { + read(); + } } else break; } + // trim() should be removed. + return endCapture().trim(); + } + + private int skipWhitespace() throws IOException { + int numSkipped=0; + while (isWhitespace()) { + read(); + numSkipped++; + } + return numSkipped; + } + + private void skipToNL() throws IOException { + while (current==' ' || current=='\t' || current=='\r') read(); } private int peek(int idx) throws IOException { @@ -439,49 +563,74 @@ private int peek() throws IOException { } private boolean read() throws IOException { - if (current=='\n') { line++; lineOffset=index; } - if (peek.length()>0) - { + if (peek.length()>0) { // normally peek will only hold not more than one character so this should not matter for performance current=peek.charAt(0); peek.deleteCharAt(0); + } else { + current=reader.read(); } - else current=reader.read(); - if (current<0) return false; + if (current<0) { + return false; + } index++; - if (capture) captureBuffer.append((char)current); + + if (capture) { + captureBuffer.append((char) current); + } return true; } + private void skip(int num) throws IOException { + pauseCapture(); + for (int i=0; i0) captureBuffer.deleteCharAt(len-1); - capture=false; + int len = captureBuffer.length(); + if (len > 0) { + captureBuffer.deleteCharAt(len - 1); + } + capture = false; } private String endCapture() { pauseCapture(); - String captured; - if (captureBuffer.length()>0) { - captured=captureBuffer.toString(); + final String captured; + if (captureBuffer.length() > 0) { + captured = captureBuffer.toString(); captureBuffer.setLength(0); } else { - captured=""; + captured = ""; } capture=false; return captured; @@ -491,30 +640,79 @@ private ParseException expected(String expected) { if (isEndOfText()) { return error("Unexpected end of input"); } - return error("Expected "+expected); + return error("Expected " + expected); } private ParseException error(String message) { - int column=index-lineOffset; - int offset=isEndOfText()?index:index-1; - return new ParseException(message, offset, line, column-1); + int column = index - lineOffset; + int offset = isEndOfText() ? index : index - 1; + return new ParseException(message, offset, line, column - 1); } - static boolean isWhiteSpace(int ch) { - return ch==' ' || ch=='\t' || ch=='\n' || ch=='\r'; + static boolean isWhitespace(int ch) { + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; } - private boolean isWhiteSpace() { - return isWhiteSpace((char)current); + private boolean isWhitespace() { + return isWhitespace((char)current); } private boolean isHexDigit() { - return current>='0' && current<='9' - || current>='a' && current<='f' - || current>='A' && current<='F'; + return current >= '0' && current <= '9' + || current >= 'a' && current <= 'f' + || current >= 'A' && current <= 'F'; } private boolean isEndOfText() { - return current==-1; + return current == -1; + } + + private static class ContainerData { + private int lineLength = 1; + private int sumLineLength = 0; + private int numLines = 0; + private boolean condensed; + + private ContainerData(boolean condensed) { + this.condensed = condensed; + } + + private void incrLineLength() { + lineLength++; + } + + private void overrideCondensed() { + condensed=false; + } + + private void nl() { + sumLineLength+=lineLength; + lineLength=1; + numLines++; + } + + private int finalLineLength(int size) { + return sumLineLength > 0 ? avgLineLength() : condensed ? size : 1; + } + + private int avgLineLength() { + int avgLineLength=sumLineLength/numLines; + if (avgLineLength<=0) { + avgLineLength=1; + } + return avgLineLength; + } + + private JsonArray into(JsonArray array) { + return array + .setLineLength(finalLineLength(array.size())) + .setCondensed(condensed); + } + + private JsonObject into(JsonObject object) { + return object + .setLineLength(finalLineLength(object.size())) + .setCondensed(condensed); + } } } diff --git a/src/main/org/hjson/HjsonWriter.java b/src/main/org/hjson/HjsonWriter.java index 2331a9f..3acca28 100644 --- a/src/main/org/hjson/HjsonWriter.java +++ b/src/main/org/hjson/HjsonWriter.java @@ -1,4 +1,5 @@ -/******************************************************************************* +/* + ****************************************************************************** * Copyright (c) 2015-2017 Christian Zangl * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -23,29 +24,73 @@ import java.io.IOException; import java.io.Writer; -import java.util.regex.Matcher; import java.util.regex.Pattern; class HjsonWriter { - private IHjsonDsfProvider[] dsfProviders; + private final IHjsonDsfProvider[] dsfProviders; + private final boolean outputComments; + private final boolean outputEmptyLines; + private final boolean bracesSameLine; + private final boolean allowCondense; + private final boolean allowMultiVal; + private final boolean emitRootBraces; + private final String space; + private final String commentSpace; + private final String eol; - static Pattern needsEscapeName=Pattern.compile("[,\\{\\[\\}\\]\\s:#\"']|//|/\\*"); + private static final Pattern NEEDS_ESCAPE_NAME = Pattern.compile("[,{\\[}\\]\\s:#\"']|//|/\\*"); public HjsonWriter(HjsonOptions options) { if (options!=null) { dsfProviders=options.getDsfProviders(); + bracesSameLine=options.bracesSameLine(); + allowCondense=options.getAllowCondense(); + allowMultiVal=options.getAllowMultiVal(); + space=options.getSpace(); + commentSpace=options.getCommentSpace(); + outputComments=options.getOutputComments(); + outputEmptyLines=options.getOutputEmptyLines(); + emitRootBraces=options.getEmitRootBraces(); + eol=options.getNewLine(); } else { dsfProviders=new IHjsonDsfProvider[0]; + bracesSameLine=false; + allowCondense=true; + allowMultiVal=true; + emitRootBraces=false; + space=" "; + commentSpace=""; + outputComments=false; + outputEmptyLines=false; + eol=JsonValue.eol; } } - void nl(Writer tw, int level) throws IOException { - tw.write(JsonValue.eol); - for (int i=0; i0) nl(tw, level); else tw.write(separator); } - tw.write('{'); - - for (JsonObject.Member pair : obj) { - nl(tw, level+1); - tw.write(escapeName(pair.getName())); - tw.write(":"); - save(pair.getValue(), tw, level+1, " ", false); - } - - if (obj.size()>0) nl(tw, level); - tw.write('}'); + writeObject(obj, tw, level, separator, noIndent); break; case ARRAY: JsonArray arr=value.asArray(); - int n=arr.size(); - if (!noIndent) { if (n>0) nl(tw, level); else tw.write(separator); } - tw.write('['); - for (int i=0; i0) nl(tw, level); - tw.write(']'); + writeArray(arr, tw, level, separator, noIndent); break; case BOOLEAN: tw.write(separator); tw.write(value.isTrue()?"true":"false"); break; case STRING: - writeString(value.asString(), tw, level, separator); + tw.write(separator); + writeString(value.asString(), tw, level, forceQuotes); break; default: tw.write(separator); + if (forceQuotes) tw.write('"'); tw.write(value.toString()); + if (forceQuotes) tw.write('"'); break; } + + // Write any following comments. + if (outputComments && value.hasEOLComment()) { + writeEOLComment(tw, value, level); + } + } + private void writeObject(JsonObject obj, Writer tw, int level, String separator, boolean noIndent) throws IOException { + // Start the beginning of the container. + boolean emitBraces = emitBraces(obj, level); + if (!emitBraces) openContainer(tw, noIndent, obj.isCondensed(), level, separator, '{'); + + int index=0; + for (JsonObject.Member pair : obj) { + if (outputEmptyLines) { + for (int i=0; i0) { // Manually separate. + tw.write(", "); + } else { + tw.write(' '); + } + } else { + tw.write(", "); + } + } + + private void closeContainer(Writer tw, boolean forceNl, boolean compact, int size, int level, char closeWith) throws IOException { + if (forceNl) { + nl(tw, level); + } else if (size>0) { + if (compact && allowCondense && allowMultiVal) tw.write(' '); + else nl(tw, level); + } + tw.write(closeWith); + } + + private static String escapeName(String name) { + if (name.length()==0 || NEEDS_ESCAPE_NAME.matcher(name).find()) return "\""+JsonWriter.escapeString(name)+"\""; else return name; } - void writeString(String value, Writer tw, int level, String separator) throws IOException { - if (value.length()==0) { tw.write(separator+"\"\""); return; } + private void writeString(String value, Writer tw, int level, boolean forceQuotes) throws IOException { + if (value.length()==0) { tw.write("\"\""); return; } char left=value.charAt(0), right=value.charAt(value.length()-1); - char left1=value.length()>1?value.charAt(1):'\0', left2=value.length()>2?value.charAt(2):'\0'; + char left1=value.length()>1?value.charAt(1):'\0'; boolean doEscape=false; char[] valuec=value.toCharArray(); for(char ch : valuec) { @@ -122,14 +294,15 @@ void writeString(String value, Writer tw, int level, String separator) throws IO } if (doEscape || - HjsonParser.isWhiteSpace(left) || HjsonParser.isWhiteSpace(right) || + HjsonParser.isWhitespace(left) || HjsonParser.isWhitespace(right) || left=='"' || left=='\'' || left=='#' || left=='/' && (left1=='*' || left1=='/') || JsonValue.isPunctuatorChar(left) || HjsonParser.tryParseNumber(value, true)!=null || - startsWithKeyword(value)) { + startsWithKeyword(value)) + { // If the String contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we first check if the String can be expressed in multiline @@ -138,24 +311,28 @@ void writeString(String value, Writer tw, int level, String separator) throws IO boolean noEscape=true; for(char ch : valuec) { if (needsEscape(ch)) { noEscape=false; break; } } - if (noEscape) { tw.write(separator+"\""+value+"\""); return; } + if (noEscape) { tw.write("\""+value+"\""); return; } boolean noEscapeML=true, allWhite=true; for(char ch : valuec) { if (needsEscapeML(ch)) { noEscapeML=false; break; } - else if (!HjsonParser.isWhiteSpace(ch)) allWhite=false; + else if (!HjsonParser.isWhitespace(ch)) allWhite=false; } - if (noEscapeML && !allWhite && !value.contains("'''")) writeMLString(value, tw, level, separator); - else tw.write(separator+"\""+JsonWriter.escapeString(value)+"\""); + if (noEscapeML && !allWhite && !value.contains("'''")) writeMLString(value, tw, level); + else tw.write("\""+JsonWriter.escapeString(value)+"\""); + } + else { + if (forceQuotes) tw.write('"'); + tw.write(value); + if (forceQuotes) tw.write('"'); } - else tw.write(separator+value); } - void writeMLString(String value, Writer tw, int level, String separator) throws IOException { + private void writeMLString(String value, Writer tw, int level) throws IOException { String[] lines=value.replace("\r", "").split("\n", -1); if (lines.length==1) { - tw.write(separator+"'''"); + tw.write("'''"); tw.write(lines[0]); tw.write("'''"); } @@ -173,18 +350,41 @@ void writeMLString(String value, Writer tw, int level, String separator) throws } } - static boolean startsWithKeyword(String text) { + private void writeComment(String comment, Writer tw, int level) throws IOException { + String[] lines = comment.split("\r?\n"); + // The first line is already indented. No nl() needed. + indentComment(tw); + tw.write(lines[0]); + + // The rest of the lines are not. + for (int i=1; ip+1 && (text.charAt(p+1)=='/' || text.charAt(p+1)=='*')); } - static boolean needsQuotes(char c) { + private static boolean forceQuoteArray(JsonValue value, JsonArray array, boolean outputComments) { + return value.isString() && (array.isCondensed() || array.getLineLength()>1 || (outputComments && value.hasEOLComment())); + } + + // Technically different methods. + private static boolean forceQuoteValue(JsonValue value, JsonObject object, boolean outputComments) { + return value.isString() && (object.isCondensed() || object.getLineLength()>1 || (outputComments && value.hasEOLComment())); + } + + private static boolean needsQuotes(char c) { switch (c) { case '\t': case '\f': @@ -197,7 +397,7 @@ static boolean needsQuotes(char c) { } } - static boolean needsEscape(char c) { + private static boolean needsEscape(char c) { switch (c) { case '\"': case '\\': @@ -207,7 +407,7 @@ static boolean needsEscape(char c) { } } - static boolean needsEscapeML(char c) { + private static boolean needsEscapeML(char c) { switch (c) { case '\n': case '\r': diff --git a/src/main/org/hjson/IHjsonDsfProvider.java b/src/main/org/hjson/IHjsonDsfProvider.java index 620bd87..1f7b818 100644 --- a/src/main/org/hjson/IHjsonDsfProvider.java +++ b/src/main/org/hjson/IHjsonDsfProvider.java @@ -31,25 +31,28 @@ public interface IHjsonDsfProvider * * @return name */ - public String getName(); + String getName(); + /** * Gets the description of this DSF. * * @return description */ - public String getDescription(); + String getDescription(); + /** * Tries to parse the text as a DSF value. * * @param text the DSF value * @return JsonValue */ - public JsonValue parse(String text); + JsonValue parse(String text); + /** * Stringifies DSF values. * * @param value the JSON value * @return string */ - public String stringify(JsonValue value); + String stringify(JsonValue value); } diff --git a/src/main/org/hjson/JsonArray.java b/src/main/org/hjson/JsonArray.java index 0623df0..0523132 100644 --- a/src/main/org/hjson/JsonArray.java +++ b/src/main/org/hjson/JsonArray.java @@ -22,14 +22,11 @@ ******************************************************************************/ package org.hjson; -import java.io.IOException; -import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; - /** * Represents a JSON array, an ordered collection of JSON values. *

@@ -61,17 +58,20 @@ * This class is not supposed to be extended by clients. *

*/ -@SuppressWarnings("serial") // use default serial UID public class JsonArray extends JsonValue implements Iterable { private final List values; + private transient boolean condensed; + private transient int lineLength; /** * Creates a new empty JsonArray. */ public JsonArray() { values=new ArrayList(); + condensed=false; + lineLength=1; } /** @@ -93,6 +93,8 @@ private JsonArray(JsonArray array, boolean unmodifiable) { } else { values=new ArrayList(array.values); } + condensed=array.condensed; + lineLength=array.lineLength; } /** @@ -111,6 +113,20 @@ public static JsonArray unmodifiableArray(JsonArray array) { return new JsonArray(array, true); } + /** + * Unsafe. Returns a raw list of the values contained within this array. For compatibility with + * other config wrappers. + * + * @return the array as a list of raw objects. + */ + public List asRawList() { + final List array=new ArrayList<>(); + for (JsonValue value : this) { + array.add(value.asRaw()); + } + return array; + } + /** * Appends the JSON representation of the specified int value to the end of this * array. @@ -124,6 +140,20 @@ public JsonArray add(int value) { return this; } + /** + * Variant of {@link #add(int)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(int value, String comment) { + values.add(valueOf(value).setComment(comment)); + return this; + } + /** * Appends the JSON representation of the specified long value to the end of this * array. @@ -137,6 +167,20 @@ public JsonArray add(long value) { return this; } + /** + * Variant of {@link #add(long)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(long value, String comment) { + values.add(valueOf(value).setComment(comment)); + return this; + } + /** * Appends the JSON representation of the specified float value to the end of this * array. @@ -150,6 +194,20 @@ public JsonArray add(float value) { return this; } + /** + * Variant of {@link #add(float)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(float value, String comment) { + values.add(valueOf(value).setComment(comment)); + return this; + } + /** * Appends the JSON representation of the specified double value to the end of this * array. @@ -163,6 +221,20 @@ public JsonArray add(double value) { return this; } + /** + * Variant of {@link #add(double)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(double value, String comment) { + values.add(valueOf(value).setComment(comment)); + return this; + } + /** * Appends the JSON representation of the specified boolean value to the end of this * array. @@ -176,6 +248,20 @@ public JsonArray add(boolean value) { return this; } + /** + * Variant of {@link #add(boolean)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(boolean value, String comment) { + values.add(valueOf(value).setComment(comment)); + return this; + } + /** * Appends the JSON representation of the specified string to the end of this array. * @@ -188,6 +274,20 @@ public JsonArray add(String value) { return this; } + /** + * Variant of {@link #add(String)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(String value, String comment) { + values.add(valueOf(value).setComment(comment)); + return this; + } + /** * Appends the specified JSON value to the end of this array. * @@ -203,6 +303,20 @@ public JsonArray add(JsonValue value) { return this; } + /** + * Variant of {@link #add(JsonValue)} which appends a standard, BOL comment to the new value. + * + * @param value + * the value to add to the aray. + * @param comment + * the value to be used as this element's comment. + * @return the array itself, to enable method chaining. + */ + public JsonArray add(JsonValue value, String comment) { + values.add(value.setComment(comment)); + return this; + } + /** * Replaces the element at the specified position in this array with the JSON representation of * the specified int value. @@ -217,7 +331,26 @@ public JsonArray add(JsonValue value) { * index >= size */ public JsonArray set(int index, int value) { - values.set(index, valueOf(value)); + set(index, valueOf(value)); + return this; + } + + /** + * Variant of {@link #set(int, int)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, int value, String comment) { + set(index, valueOf(value).setComment(comment)); return this; } @@ -235,7 +368,26 @@ public JsonArray set(int index, int value) { * index >= size */ public JsonArray set(int index, long value) { - values.set(index, valueOf(value)); + set(index, valueOf(value)); + return this; + } + + /** + * Variant of {@link #set(int, long)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, long value, String comment) { + set(index, valueOf(value).setComment(comment)); return this; } @@ -253,7 +405,26 @@ public JsonArray set(int index, long value) { * index >= size */ public JsonArray set(int index, float value) { - values.set(index, valueOf(value)); + set(index, valueOf(value)); + return this; + } + + /** + * Variant of {@link #set(int, float)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, float value, String comment) { + set(index, valueOf(value).setComment(comment)); return this; } @@ -271,7 +442,26 @@ public JsonArray set(int index, float value) { * index >= size */ public JsonArray set(int index, double value) { - values.set(index, valueOf(value)); + set(index, valueOf(value)); + return this; + } + + /** + * Variant of {@link #set(int, double)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, double value, String comment) { + set(index, valueOf(value).setComment(comment)); return this; } @@ -289,7 +479,26 @@ public JsonArray set(int index, double value) { * index >= size */ public JsonArray set(int index, boolean value) { - values.set(index, valueOf(value)); + set(index, valueOf(value)); + return this; + } + + /** + * Variant of {@link #set(int, boolean)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, boolean value, String comment) { + set(index, valueOf(value).setComment(comment)); return this; } @@ -307,7 +516,26 @@ public JsonArray set(int index, boolean value) { * index >= size */ public JsonArray set(int index, String value) { - values.set(index, valueOf(value)); + set(index, valueOf(value)); + return this; + } + + /** + * Variant of {@link #set(int, String)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, String value, String comment) { + set(index, valueOf(value).setComment(comment)); return this; } @@ -327,7 +555,82 @@ public JsonArray set(int index, JsonValue value) { if (value==null) { throw new NullPointerException("value is null"); } - values.set(index, value); + values.set(index, value).setAccessed(true); + return this; + } + + /** + * Variant of {@link #set(int, JsonValue)} which appends a standard, BOL comment to the value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @param comment + * the value to be used as the comment for this element. + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, JsonValue value, String comment) { + if (value==null) { + throw new NullPointerException("value is null"); + } + values.set(index, value.setComment(comment)); + return this; + } + + /** + * Locates a member of this object according to the index and appends a standard, BOL + * comment. + * + * @param index + * the index of the member to be altered. + * @param comment + * the value to set as this member's comment. + * @return the array itself to enable chaining. + */ + public JsonArray setComment(int index, String comment) { + get(index).setComment(comment); + return this; + } + + /** + * Locates a member of this object according to the index and appends a new comment + * according to the input parameters. + * + * @param index + * The index of the member to be altered. + * @param type + * The where the comment should be placed relative to its value. + * @param style + * The style to use, i.e. #, //, etc. + * @param comment + * the value to set as this member's comment. + * @return the array itself to enable chaining. + */ + public JsonArray setComment(int index, CommentType type, CommentStyle style, String comment) { + get(index).setComment(type, style, comment); + return this; + } + + /** + * Marks every value in this array as being accessed or not accessed. + * + * @param b + * whether to mark each field as accessed. + * @return the array itself, to enable chaining. + */ + public JsonArray setAllAccessed(boolean b) { + for (JsonValue value : this) { + value.setAccessed(b); + if (value.isObject()) { + value.asObject().setAllAccessed(b); + } else if (value.isArray()) { + value.asArray().setAllAccessed(b); + } + } return this; } @@ -364,6 +667,31 @@ public boolean isEmpty() { return values.isEmpty(); } + /** + * Clears every element from this array. + * + * @throws UnsupportedOperationException if this object is unmodifiable. + * @return the array itself, to enable method chaining + */ + public JsonArray clear() { + values.clear(); + return this; + } + + /** + * Adds every value from another array. + * + * @throws UnsupportedOperationException if this object is unmodifiable. + * @param array The array to copy values from. + * @return the array itself, to enable method chaining + */ + public JsonArray addAll(JsonArray array) { + for (JsonValue value : array) { + add(value); + } + return this; + } + /** * Returns the value of the element at the specified position in this array. * @@ -375,7 +703,49 @@ public boolean isEmpty() { * index >= size */ public JsonValue get(int index) { - return values.get(index); + return values.get(index).setAccessed(true); + } + + /** + * Returns the index of the given element, or else -1 if not found. + * + * @param value The value being queried in the array. + * @return The index of the element, or else -1 if not found. + */ + public int indexOf(JsonValue value) { + return values.indexOf(value); + } + + /** + * Returns the last index of the given element, or else -1 if not found. + * + * @param value The value being queried in the array. + * @return The last index of the element, or else -1 if not found. + */ + public int lastIndexOf(JsonValue value) { + return values.lastIndexOf(value); + } + + /** + * Returns whether this array contains a value. + * + * @param value The value to search for. + * @return true if this array contains the value. + */ + public boolean contains(JsonValue value) { + return values.contains(value); + } + + /** + * Returns whether this array contains a value of unknown type. + * + * Todo: implement concrete functions for other known types. + * + * @param value The value to search for. + * @return true if this array contains the value. + */ + public boolean contains(Object value) { + return contains(JsonValue.valueOf(value)); } /** @@ -389,6 +759,111 @@ public List values() { return Collections.unmodifiableList(values); } + /** + * Gets the number of elements to be displayed on a single line when this array is serialized. + * + * @return the number of elements per-line. + */ + public int getLineLength() { return lineLength; } + + /** + * Sets the number of elements to be displayed on a single line when this array is serialized. + * This does not check whether an incorrect comment syntax is used. As a result, you may wind + * up breaking your file when any element contains a single line comment. + * + * @param value + * the number of elements to be displayed per-line. + * @return this, to enable chaining + */ + public JsonArray setLineLength(int value) { lineLength=value; return this; } + + /** + * Detects whether this array is "condensed" i.e. whether it should be displayed entirely on + * one line. + * + * @return whether this array is condensed. + */ + public boolean isCondensed() { return condensed; } + + /** + * Sets whether this array should be "condensed," i.e. whether it should be displayed entirely on + * one line. + * + * @param value + * whether this array should be condensed. + * @return this, to enable chaining + */ + public JsonArray setCondensed(boolean value) { condensed=value; return this; } + + /** + * Generates a list of paths that have not yet been accessed in-code. + * + * @return the list of unused paths. + */ + public List getUnusedPaths() { + return this.getUsedPaths(false); + } + + /** + * Generates a list of paths that have been accessed in-code. + * + * @return the list of unused paths. + */ + public List getUsedPaths() { + return this.getUsedPaths(true); + } + + /** + * Generates a list of paths that either have or have not been accessed in-code. + * + * @param used whether the value should have been accessed. + * @return the list of unused paths. + */ + public List getUsedPaths(boolean used) { + final List paths=new ArrayList<>(); + int index=0; + for (JsonValue v : this) { + if (used == v.isAccessed()) { + paths.add("["+index+"]"); + } + if (v.isObject()) { + for (String s : v.asObject().getUsedPaths(used)) { + paths.add("["+index+"]."+s); + } + } else if (v.isArray()) { + for (String s : v.asArray().getUsedPaths(used)) { + paths.add("["+index+"]"+s); + } + } + index++; + } + return paths; + } + + /** + * Generates a list of all possible paths in this array. + * + * @return the list of paths. + */ + public List getAllPaths() { + final List paths=new ArrayList(); + int index=0; + for (JsonValue v : this) { + paths.add("["+index+"]"); + if (v.isObject()) { + for (String s : v.asObject().getAllPaths()) { + paths.add("["+index+"]."+s); + } + } else if (v.isArray()) { + for (String s : v.asArray().getAllPaths()) { + paths.add("["+index+"]"+s); + } + } + index++; + } + return paths; + } + /** * Returns an iterator over the values of this array in document order. The returned iterator * cannot be used to modify this array. @@ -428,9 +903,37 @@ public JsonArray asArray() { return this; } + @Override + public JsonValue shallowCopy() { + JsonArray clone=(JsonArray)new JsonArray().copyMetadata(this, false); + for (JsonValue value : values) { + clone.add(value); + } + return clone; + } + + @Override + public JsonArray deepCopy(boolean trackAccess) { + JsonArray clone=(JsonArray)new JsonArray().copyMetadata(this, trackAccess); + for (JsonValue value : values) { + clone.add(value.deepCopy(trackAccess)); + } + return clone; + } + + @Override + public JsonValue copyMetadata(JsonValue value, boolean trackAccess) { + if (value instanceof JsonArray) { + final JsonArray array=value.asArray(); + this.lineLength=array.lineLength; + this.condensed=array.condensed; + } + return super.copyMetadata(value, trackAccess); + } + @Override public int hashCode() { - return values.hashCode(); + return super.hashCode() * 59 + values.hashCode(); } @Override @@ -438,13 +941,10 @@ public boolean equals(Object object) { if (this==object) { return true; } - if (object==null) { - return false; - } - if (getClass()!=object.getClass()) { - return false; + if (object instanceof JsonArray) { + JsonArray other=(JsonArray)object; + return values.equals(other.values) && commentsMatch(other); } - JsonArray other=(JsonArray)object; - return values.equals(other.values); + return false; } } diff --git a/src/main/org/hjson/JsonDsf.java b/src/main/org/hjson/JsonDsf.java index 8e9c8c3..1b83ce8 100644 --- a/src/main/org/hjson/JsonDsf.java +++ b/src/main/org/hjson/JsonDsf.java @@ -21,7 +21,6 @@ ******************************************************************************/ package org.hjson; -@SuppressWarnings("serial") // use default serial UID class JsonDsf extends JsonValue { private final Object value; @@ -45,8 +44,26 @@ public Object asDsf() { return value; } + @Override + public JsonValue deepCopy(boolean trackAccess) { + JsonValue clone=new JsonDsf(value).copyComments(this); + return trackAccess ? clone.setAccessed(accessed) : clone; + } + @Override public int hashCode() { - return value.hashCode(); + return super.hashCode() * 59 + value.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this==object) { + return true; + } + if (object instanceof JsonDsf) { + JsonDsf other=(JsonDsf)object; + return value.equals(other.value) && commentsMatch(other); + } + return false; } } diff --git a/src/main/org/hjson/JsonLiteral.java b/src/main/org/hjson/JsonLiteral.java index 0c5f11c..72c4ca1 100644 --- a/src/main/org/hjson/JsonLiteral.java +++ b/src/main/org/hjson/JsonLiteral.java @@ -22,18 +22,10 @@ ******************************************************************************/ package org.hjson; -import java.io.IOException; - - -@SuppressWarnings("serial") // use default serial UID -class JsonLiteral extends JsonValue { +public class JsonLiteral extends JsonValue { enum Iv { T, F, N }; - static final JsonValue NULL=new JsonLiteral(Iv.N); - static final JsonValue TRUE=new JsonLiteral(Iv.T); - static final JsonValue FALSE=new JsonLiteral(Iv.F); - private final Iv value; private JsonLiteral(Iv value) { @@ -50,9 +42,16 @@ public String toString() { } } - @Override - public int hashCode() { - return value.hashCode(); + public static JsonLiteral jsonNull() { + return new JsonLiteral(Iv.N); + } + + public static JsonLiteral jsonTrue() { + return new JsonLiteral(Iv.T); + } + + public static JsonLiteral jsonFalse() { + return new JsonLiteral(Iv.F); } @Override @@ -85,18 +84,26 @@ public boolean asBoolean() { return value==Iv.N ? super.asBoolean() : value==Iv.T; } + @Override + public JsonValue deepCopy(boolean trackAccess) { + JsonValue clone=new JsonLiteral(value).copyComments(this); + return trackAccess ? clone.setAccessed(accessed) : clone; + } + + @Override + public int hashCode() { + return super.hashCode() * 59 + value.hashCode(); + } + @Override public boolean equals(Object object) { if (this==object) { return true; } - if (object==null) { - return false; - } - if (getClass()!=object.getClass()) { - return false; + if (object instanceof JsonLiteral) { + JsonLiteral other=(JsonLiteral)object; + return value==other.value && commentsMatch(other); } - JsonLiteral other=(JsonLiteral)object; - return value==other.value; + return false; } } diff --git a/src/main/org/hjson/JsonNumber.java b/src/main/org/hjson/JsonNumber.java index 24b3251..5fc79e2 100644 --- a/src/main/org/hjson/JsonNumber.java +++ b/src/main/org/hjson/JsonNumber.java @@ -22,11 +22,8 @@ ******************************************************************************/ package org.hjson; -import java.io.IOException; import java.math.BigDecimal; - -@SuppressWarnings("serial") // use default serial UID class JsonNumber extends JsonValue { private final double value; @@ -78,9 +75,15 @@ public double asDouble() { return value; } + @Override + public JsonValue deepCopy(boolean trackAccess) { + JsonValue clone=new JsonNumber(value).copyComments(this); + return trackAccess ? clone.setAccessed(accessed) : clone; + } + @Override public int hashCode() { - return Double.valueOf(value).hashCode(); + return super.hashCode() * 59 + Double.valueOf(value).hashCode(); } @Override @@ -88,13 +91,10 @@ public boolean equals(Object object) { if (this==object) { return true; } - if (object==null) { - return false; - } - if (getClass()!=object.getClass()) { - return false; + if (object instanceof JsonNumber) { + JsonNumber other=(JsonNumber)object; + return value==other.value && commentsMatch(other); } - JsonNumber other=(JsonNumber)object; - return value==other.value; + return false; } } diff --git a/src/main/org/hjson/JsonObject.java b/src/main/org/hjson/JsonObject.java index 353f6c1..4214f13 100644 --- a/src/main/org/hjson/JsonObject.java +++ b/src/main/org/hjson/JsonObject.java @@ -24,15 +24,10 @@ import java.io.IOException; import java.io.ObjectInputStream; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; +import java.util.*; import org.hjson.JsonObject.Member; - /** * Represents a JSON object, a set of name/value pairs, where the names are strings and the values * are JSON values. @@ -77,6 +72,8 @@ public class JsonObject extends JsonValue implements Iterable { private final List names; private final List values; private transient HashIndexTable table; + private transient boolean condensed; + private transient int lineLength; /** * Creates a new empty JsonObject. @@ -85,6 +82,8 @@ public JsonObject() { names=new ArrayList(); values=new ArrayList(); table=new HashIndexTable(); + condensed=false; + lineLength=1; } /** @@ -107,6 +106,8 @@ private JsonObject(JsonObject object, boolean unmodifiable) { values=new ArrayList(object.values); } table=new HashIndexTable(); + condensed=object.condensed; + lineLength=object.lineLength; updateHashIndex(); } @@ -127,6 +128,39 @@ public static JsonObject unmodifiableObject(JsonObject object) { return new JsonObject(object, true); } + /** + * Unsafe. Returns a raw map of the values contained within this object. For compatibility with + * other config wrappers. + * + * @return the object as a map of string -> raw object. + */ + public Map asRawMap() { + final Map map=new HashMap<>(); + for (final Member member : this) { + map.put(member.name, member.value.asRaw()); + } + return map; + } + + /** + * Returns the list of keys contained within this object. + * + * @return the every name defined in the object. + */ + public List getNames() { + return names; + } + + /** + * Returns whether the input key is contained within this object. + * + * @param name The name of the key to search for. + * @return whether the key exists. + */ + public boolean has(String name) { + return indexOf(name)!=-1; + } + /** * Appends a new member to the end of this object, with the specified name and the JSON * representation of the specified int value. @@ -146,8 +180,23 @@ public static JsonObject unmodifiableObject(JsonObject object) { * @return the object itself, to enable method chaining */ public JsonObject add(String name, int value) { - add(name, valueOf(value)); - return this; + return add(name, valueOf(value)); + } + + /** + * Variant of {@link #add(String, int)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, int value, String comment) { + return add(name, valueOf(value).setComment(comment)); } /** @@ -169,8 +218,23 @@ public JsonObject add(String name, int value) { * @return the object itself, to enable method chaining */ public JsonObject add(String name, long value) { - add(name, valueOf(value)); - return this; + return add(name, valueOf(value)); + } + + /** + * Variant of {@link #add(String, long)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, long value, String comment) { + return add(name, valueOf(value).setComment(comment)); } /** @@ -192,8 +256,23 @@ public JsonObject add(String name, long value) { * @return the object itself, to enable method chaining */ public JsonObject add(String name, float value) { - add(name, valueOf(value)); - return this; + return add(name, valueOf(value)); + } + + /** + * Variant of {@link #add(String, float)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, float value, String comment) { + return add(name, valueOf(value).setComment(comment)); } /** @@ -215,8 +294,23 @@ public JsonObject add(String name, float value) { * @return the object itself, to enable method chaining */ public JsonObject add(String name, double value) { - add(name, valueOf(value)); - return this; + return add(name, valueOf(value)); + } + + /** + * Variant of {@link #add(String, double)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, double value, String comment) { + return add(name, valueOf(value).setComment(comment)); } /** @@ -238,8 +332,23 @@ public JsonObject add(String name, double value) { * @return the object itself, to enable method chaining */ public JsonObject add(String name, boolean value) { - add(name, valueOf(value)); - return this; + return add(name, valueOf(value)); + } + + /** + * Variant of {@link #add(String, boolean)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, boolean value, String comment) { + return add(name, valueOf(value).setComment(comment)); } /** @@ -261,8 +370,23 @@ public JsonObject add(String name, boolean value) { * @return the object itself, to enable method chaining */ public JsonObject add(String name, String value) { - add(name, valueOf(value)); - return this; + return add(name, valueOf(value)); + } + + /** + * Variant of {@link #add(String, int)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, String value, String comment) { + return add(name, valueOf(value).setComment(comment)); } /** @@ -296,6 +420,83 @@ public JsonObject add(String name, JsonValue value) { return this; } + /** + * Variant of {@link #add(String, JsonValue)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, JsonValue value, String comment) { + if (comment==null) { + throw new NullPointerException("comment is null"); + } + value.setComment(comment); + return add(name, value); + } + + /** + * Variant of {@link #add(String, JsonValue)} which will insert a value at a particular position. + * + * Todo: decide whether to keep this before merging with hjson-java. + * + * @param index + * the index where this value will be placed + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject insert(int index, String name, JsonValue value) { + if (name==null) { + throw new NullPointerException("name is null"); + } + if (value==null) { + throw new NullPointerException("value is null"); + } + // Collect all current members into an array; + final List members=new ArrayList<>(); + for (Member m : this) { + members.add(m); + } + members.add(index, new Member(name, value)); + // Reset everything. + clear(); + // Re-add the values, now in the correct order. + for (Member m : members) { + add(m.name, m.value); + } + return this; + } + + /** + * Variant of {@link #insert(int, String, JsonValue)} which includes a comment. + * + * @param index + * the index where this value will be placed + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the comment to set on the value. + * @return the object itself, to enable method chaining + */ + public JsonObject insert(int index, String name, JsonValue value, String comment) { + if (comment==null) { + throw new NullPointerException("comment is null"); + } + value.setComment(comment); + insert(index, name, value); + return this; + } + /** * Sets the value of the member with the specified name to the JSON representation of the * specified int value. If this object does not contain a member with this name, a @@ -314,8 +515,23 @@ public JsonObject add(String name, JsonValue value) { * @return the object itself, to enable method chaining */ public JsonObject set(String name, int value) { - set(name, valueOf(value)); - return this; + return set(name, valueOf(value)); + } + + /** + * Variant of {@link #set(String, int)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, int value, String comment) { + return set(name, valueOf(value).setComment(comment)); } /** @@ -336,8 +552,22 @@ public JsonObject set(String name, int value) { * @return the object itself, to enable method chaining */ public JsonObject set(String name, long value) { - set(name, valueOf(value)); - return this; + return set(name, valueOf(value)); + } + /** + * Variant of {@link #set(String, long)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, long value, String comment) { + return set(name, valueOf(value).setComment(comment)); } /** @@ -358,8 +588,23 @@ public JsonObject set(String name, long value) { * @return the object itself, to enable method chaining */ public JsonObject set(String name, float value) { - set(name, valueOf(value)); - return this; + return set(name, valueOf(value)); + } + + /** + * Variant of {@link #set(String, float)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, float value, String comment) { + return set(name, valueOf(value).setComment(comment)); } /** @@ -380,8 +625,23 @@ public JsonObject set(String name, float value) { * @return the object itself, to enable method chaining */ public JsonObject set(String name, double value) { - set(name, valueOf(value)); - return this; + return set(name, valueOf(value)); + } + + /** + * Variant of {@link #set(String, double)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, double value, String comment) { + return set(name, valueOf(value).setComment(comment)); } /** @@ -402,8 +662,23 @@ public JsonObject set(String name, double value) { * @return the object itself, to enable method chaining */ public JsonObject set(String name, boolean value) { - set(name, valueOf(value)); - return this; + return set(name, valueOf(value)); + } + + /** + * Variant of {@link #set(String, boolean)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, boolean value, String comment) { + return set(name, valueOf(value).setComment(comment)); } /** @@ -424,8 +699,23 @@ public JsonObject set(String name, boolean value) { * @return the object itself, to enable method chaining */ public JsonObject set(String name, String value) { - set(name, valueOf(value)); - return this; + return set(name, valueOf(value)); + } + + /** + * Variant of {@link #set(String, String)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, String value, String comment) { + return set(name, valueOf(value).setComment(comment)); } /** @@ -451,6 +741,7 @@ public JsonObject set(String name, JsonValue value) { if (value==null) { throw new NullPointerException("value is null"); } + value.setAccessed(true); int index=indexOf(name); if (index!=-1) { values.set(index, value); @@ -462,6 +753,79 @@ public JsonObject set(String name, JsonValue value) { return this; } + /** + * Variant of {@link #set(String, JsonValue)} which appends a standard comment before the beginning of + * this value. + * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @param comment + * the string to be used as this value's comment + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, JsonValue value, String comment) { + if (value==null) { + throw new NullPointerException("value is null"); + } + return set(name, value.setComment(comment)); + } + + /** + * Locates a member of this object according to the key name and appends a standard, BOL + * comment. + * + * @param name + * the name of the member to be altered. + * @param comment + * the value to set as this member's comment. + * @return the object itself, to enable chaining. + */ + public JsonObject setComment(String name, String comment) { + get(name).setComment(comment); + return this; + } + + /** + * Locates a member of this object according to the key name and appends a new comment + * according to the input parameters. + * + * @param name + * The name of the member to be altered. + * @param type + * The where the comment should be placed relative to its value. + * @param style + * The style to use, i.e. #, //, etc. + * @param comment + * the value to set as this member's comment. + * @return the object itself, to enable chaining. + */ + public JsonObject setComment(String name, CommentType type, CommentStyle style, String comment) { + get(name).setComment(type, style, comment); + return this; + } + + /** + * Marks every value in this object as being accessed or not accessed. + * + * @param b + * whether to mark each field as accessed. + * @return the object itself, to enable chaining. + */ + public JsonObject setAllAccessed(boolean b) { + for (Member m : this) { + final JsonValue value=m.value; + value.setAccessed(b); + if (value.isObject()) { + value.asObject().setAllAccessed(b); + } else if (value.isArray()) { + value.asArray().setAllAccessed(b); + } + } + return this; + } + /** * Removes a member with the specified name from this object. If this object contains multiple * members with the given name, only the last one is removed. If this object does not contain a @@ -498,7 +862,7 @@ public JsonValue get(String name) { throw new NullPointerException("name is null"); } int index=indexOf(name); - return index!=-1 ? values.get(index) : null; + return index!=-1 ? values.get(index).setAccessed(true) : null; } /** @@ -614,6 +978,22 @@ public String getString(String name, String defaultValue) { return value!=null ? value.asString() : defaultValue; } + /** + * Returns the value of the member with the specified index in this object. + * + *

Note that this method is intended for reflecting on data in the object without semantically + * "consuming" it. For this reason, it does not implicitly flag the returned value as used.

+ * + * @param index + * the index of the member whose value is to be returned + * @return the value of the last member with the specified index, or null if this + * object does not contain a member with that index + * @throws ArrayIndexOutOfBoundsException if the given index does not exist. + */ + public JsonValue get(int index) { + return values.get(index); + } + /** * Returns the number of members (name/value pairs) in this object. * @@ -632,6 +1012,37 @@ public boolean isEmpty() { return names.isEmpty(); } + /** + * Clears every element from this object. + * + * @throws UnsupportedOperationException if this object is unmodifiable. + * @return the object itself, to enable method chaining + */ + public JsonObject clear() { + names.clear(); + values.clear(); + table.clear(); + return this; + } + + /** + * Adds every member (key/value pair) from another object. + * + * @throws UnsupportedOperationException if this object is unmodifiable. + * @param object The object to copy values from. + * @return the object itself, to enable method chaining + */ + public JsonObject addAll(JsonObject object) { + for (Member m : object) { + if (has(m.getName())) { + set(m.getName(), m.getValue()); + } else { + add(m.getName(), m.getValue()); + } + } + return this; + } + /** * Returns a list of the names in this object in document order. The returned list is backed by * this object and will reflect subsequent changes. It cannot be used to modify this object. @@ -643,6 +1054,150 @@ public List names() { return Collections.unmodifiableList(names); } + /** + * Gets the number of elements to be displayed on a single line when this array is serialized. + * + * @return the number of elements per-line. + */ + public int getLineLength() { return lineLength; } + + /** + * Sets the number of elements to be displayed on a single line when this array is serialized. + * This does not check whether an incorrect comment syntax is used. As a result, you may wind + * up breaking your file when any element contains a single line comment. + * + * @param value + * the number of elements to be displayed per-line. + * @return the object itself, to enable method chaining + */ + public JsonObject setLineLength(int value) { lineLength=value; return this; } + + /** + * Detects whether this object is "condensed" i.e. whether it should be displayed entirely on + * one line. + * + * @return whether this object is condensed. + */ + public boolean isCondensed() { return condensed; } + + /** + * Sets whether this object should be "condensed," i.e. whether it should be displayed entirely on + * one line. + * + * @param value + * whether this object should be condensed. + * @return the object itself, to enable method chaining + */ + public JsonObject setCondensed(boolean value) { condensed=value; return this; } + + /** + * Generates a list of paths that have not yet been accessed in-code. + * + * @return the list of unused paths. + */ + public List getUnusedPaths() { + return this.getUsedPaths(false); + } + + /** + * Generates a list of paths that have been accessed in-code. + * + * @return the list of used paths. + */ + public List getUsedPaths() { + return this.getUsedPaths(true); + } + + /** + * Generates a list of paths that either have or have not been accessed in-code. + * + * @param used whether the path should have been accessed. + * @return the list of used paths. + */ + public List getUsedPaths(boolean used) { + final List paths=new ArrayList(); + for (Member m : this) { + if (used == m.value.isAccessed()) { + paths.add(m.name); + } + if (m.value.isObject()) { + for (String s : m.value.asObject().getUsedPaths(used)) { + paths.add(m.name+"."+s); + } + } else if (m.value.isArray()) { + for (String s : m.value.asArray().getUsedPaths(used)) { + paths.add(m.name+s); + } + } + } + return paths; + } + + /** + * Generates a list of all possible paths in this object. + * + * @return the list of paths. + */ + public List getAllPaths() { + final List paths=new ArrayList(); + for (Member m : this) { + paths.add(m.name); + if (m.value.isObject()) { + for (String s : m.value.asObject().getAllPaths()) { + paths.add(m.name+"."+s); + } + } else if (m.value.isArray()) { + for (String s : m.value.asArray().getAllPaths()) { + paths.add(m.name+s); + } + } + } + return paths; + } + + /** + * Sorts all members of this object according to their keys, in alphabetical order. + * + * @return the object itself, to enable chaining. + */ + public JsonObject sort() { + // Collect all members into an array. + List members=new ArrayList<>(); + for (Member m : this) { + members.add(m); + } + // Get the underlying array so it can be sorted. + Member[] membersArray=members.toArray(new Member[0]); + + // Sort the new array. + Arrays.sort(membersArray, new MemberComparator()); + + // Clear the original values. + clear(); + + // Re-add the values, now in order. + for (Member m : membersArray) { + add(m.name, m.value); + } + + return this; + } + + /** + * Returns the index of the member with the given name. If this object contains multiple members + * with the same name, the last member will be returned. + * + * @param name The name of the member whose value is being returned. + * @return The index of the member with this name, or else -1. + */ + public int indexOf(String name) { + int index=table.get(name); + if (index!=-1 && name.equals(names.get(index))) { + return index; + } + return names.lastIndexOf(name); + } + /** * Returns an iterator over the members of this object in document order. The returned iterator * cannot be used to modify this object. @@ -686,6 +1241,34 @@ public JsonObject asObject() { return this; } + @Override + public JsonValue shallowCopy() { + JsonObject clone=(JsonObject)new JsonObject().copyMetadata(this, false); + for (Member member : this) { + clone.add(member.getName(), member.getValue()); + } + return clone; + } + + @Override + public JsonValue deepCopy(boolean trackAccess) { + JsonObject clone=(JsonObject)new JsonObject().copyMetadata(this, trackAccess); + for (Member member : this) { + clone.add(member.getName(), member.getValue().deepCopy(trackAccess)); + } + return clone; + } + + @Override + public JsonValue copyMetadata(JsonValue value, boolean trackAccess) { + if (value instanceof JsonObject) { + final JsonObject object=value.asObject(); + this.lineLength=object.lineLength; + this.condensed=object.condensed; + } + return super.copyMetadata(value, trackAccess); + } + @Override public int hashCode() { int result=1; @@ -699,22 +1282,11 @@ public boolean equals(Object obj) { if (this==obj) { return true; } - if (obj==null) { - return false; - } - if (getClass()!=obj.getClass()) { - return false; + if (obj instanceof JsonObject) { + JsonObject other=(JsonObject)obj; + return names.equals(other.names) && values.equals(other.values) && commentsMatch(other); } - JsonObject other=(JsonObject)obj; - return names.equals(other.names) && values.equals(other.values); - } - - int indexOf(String name) { - int index=table.get(name); - if (index!=-1 && name.equals(names.get(index))) { - return index; - } - return names.lastIndexOf(name); + return false; } private synchronized void readObject(ObjectInputStream inputStream) throws IOException, @@ -828,5 +1400,16 @@ int get(Object name) { private int hashSlotfor (Object element) { return element.hashCode() & hashTable.length-1; } + + private void clear() { + Arrays.fill(hashTable, (byte) 0); + } + } + + public static class MemberComparator implements Comparator { + @Override + public int compare(Member m1, Member m2) { + return m1.name.compareToIgnoreCase(m2.name); + } } } diff --git a/src/main/org/hjson/JsonParser.java b/src/main/org/hjson/JsonParser.java index bbf7501..702464a 100644 --- a/src/main/org/hjson/JsonParser.java +++ b/src/main/org/hjson/JsonParser.java @@ -162,7 +162,7 @@ private JsonValue readNull() throws IOException { readRequiredChar('u'); readRequiredChar('l'); readRequiredChar('l'); - return JsonValue.NULL; + return JsonLiteral.jsonNull(); } private JsonValue readTrue() throws IOException { @@ -170,7 +170,7 @@ private JsonValue readTrue() throws IOException { readRequiredChar('r'); readRequiredChar('u'); readRequiredChar('e'); - return JsonValue.TRUE; + return JsonLiteral.jsonTrue(); } private JsonValue readFalse() throws IOException { @@ -179,7 +179,7 @@ private JsonValue readFalse() throws IOException { readRequiredChar('l'); readRequiredChar('s'); readRequiredChar('e'); - return JsonValue.FALSE; + return JsonLiteral.jsonFalse(); } private void readRequiredChar(char ch) throws IOException { diff --git a/src/main/org/hjson/JsonString.java b/src/main/org/hjson/JsonString.java index 5aff6af..fc99153 100644 --- a/src/main/org/hjson/JsonString.java +++ b/src/main/org/hjson/JsonString.java @@ -22,9 +22,6 @@ ******************************************************************************/ package org.hjson; -import java.io.IOException; - - @SuppressWarnings("serial") // use default serial UID class JsonString extends JsonValue { @@ -52,9 +49,15 @@ public String asString() { return string; } + @Override + public JsonValue deepCopy(boolean trackAccess) { + JsonValue clone=new JsonString(string).copyComments(this); + return trackAccess ? clone.setAccessed(accessed) : clone; + } + @Override public int hashCode() { - return string.hashCode(); + return super.hashCode() * 59 + string.hashCode(); } @Override @@ -62,13 +65,10 @@ public boolean equals(Object object) { if (this==object) { return true; } - if (object==null) { - return false; - } - if (getClass()!=object.getClass()) { - return false; + if (object instanceof JsonString) { + JsonString other=(JsonString)object; + return string.equals(other.string) && commentsMatch(other); } - JsonString other=(JsonString)object; - return string.equals(other.string); + return false; } } diff --git a/src/main/org/hjson/JsonValue.java b/src/main/org/hjson/JsonValue.java index 856facb..bfb0f83 100644 --- a/src/main/org/hjson/JsonValue.java +++ b/src/main/org/hjson/JsonValue.java @@ -22,22 +22,21 @@ ******************************************************************************/ package org.hjson; -import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Represents a JSON value. This can be a JSON object, an array, * a number, a string, or one of the literals * true, false, and null. *

- * The literals true, false, and null are - * represented by the constants {@link #TRUE}, {@link #FALSE}, and {@link #NULL}. - *

- *

* JSON objects and arrays are represented by the subtypes * {@link JsonObject} and {@link JsonArray}. Instances of these types can be created using the * public constructors of these classes. @@ -64,35 +63,38 @@ @SuppressWarnings("serial") // use default serial UID public abstract class JsonValue implements Serializable { + static String eol=System.getProperty("line.separator"); + /** - * Represents the JSON literal true. + * Comments that will be used by each type of value. */ - public static final JsonValue TRUE=JsonLiteral.TRUE; + protected String bolComment="", eolComment="", intComment=""; /** - * Represents the JSON literal false. + * A flag indicating whether this value has been specifically called for. */ - public static final JsonValue FALSE=JsonLiteral.FALSE; + protected boolean accessed=false; /** - * Represents the JSON literal null. + * Indicates the number of empty lines above this value. */ - public static final JsonValue NULL=JsonLiteral.NULL; - - static String eol=System.getProperty("line.separator"); + protected int numLines=0; /** - * Gets the newline charater(s). + * Gets the newline character(s). * * @return the eol value */ + @Deprecated public static String getEol() { return eol; } /** - * Sets the newline charater(s). + * Sets the newline character(s). * + * @deprecated Use {@link HjsonOptions#setNewLine} * @param value the eol value */ + @Deprecated public static void setEol(String value) { if (value.equals("\r\n") || value.equals("\n")) eol=value; } @@ -248,7 +250,7 @@ public static JsonValue valueOf(double value) { * @return a JSON value that represents the given string */ public static JsonValue valueOf(String string) { - return string==null ? NULL : new JsonString(string); + return string==null ? JsonLiteral.jsonNull() : new JsonString(string); } /** @@ -258,7 +260,7 @@ public static JsonValue valueOf(String string) { * @return a JSON value that represents the given value */ public static JsonValue valueOf(boolean value) { - return value ? TRUE : FALSE; + return value ? JsonLiteral.jsonTrue() : JsonLiteral.jsonFalse(); } /** @@ -271,6 +273,48 @@ public static JsonValue valueOfDsf(Object value) { return new JsonDsf(value); } + /** + * Returns a JsonValue from an Object of unknown type. + * + * @param value the value to get a JSON representation for + * @return a new JsonValue. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static JsonValue valueOf(Object value) { + if (value==null) { + return JsonLiteral.jsonNull(); + } else if (value instanceof Number) { + return new JsonNumber(((Number) value).doubleValue()); + } else if (value instanceof String) { + return new JsonString((String) value); + } else if (value instanceof Boolean) { + return (Boolean) value ? JsonLiteral.jsonTrue() : JsonLiteral.jsonFalse(); + } else if (value instanceof Enum) { + return new JsonString(((Enum) value).name()); + } else if (value instanceof Iterable) { + JsonArray array=new JsonArray(); + for (Object o : (Iterable) value) { + array.add(valueOf(o)); + } + return array; + } else if (value instanceof Map) { + JsonObject object=new JsonObject(); + Set entries=((Map)value).entrySet(); + for (Map.Entry entry : entries) { + object.set(entry.getKey().toString(), valueOf(entry.getValue())); + } + return object; + } else if (value.getClass().isArray()) { + JsonArray array=new JsonArray(); + int length=Array.getLength(value); + for (int i=0; itrue if this value does contain comments. + */ + public boolean hasComments() { + return hasBOLComment() || hasEOLComment() || hasInteriorComment(); + } + + /** + * Detects whether this value contains any beginning of line comments. + * + * @return true if this value does contain any BOL comments. + */ + public boolean hasBOLComment() { return !bolComment.isEmpty(); } + + /** + * Detects whether this value contains any end of line comments. + * + * @return true if this value does contain any EOL comments. + */ + public boolean hasEOLComment() { return !eolComment.isEmpty(); } + + /** + * Detects whether this value contains any interior comments, which may be present inside of + * object and array types with no association to any of their members. + * + * @return true if this value does contain any interior comments. + */ + public boolean hasInteriorComment() { return !intComment.isEmpty(); } + + /** + * Gets any comment that exists before this value. + * + * @return The full contents of this comment, including any comment indicators. + */ + public String getBOLComment() { return bolComment; } + + /** + * Gets any comment that exists after this value. + * + * @return The full contents of this comment, including any comment indicators. + */ + public String getEOLComment() { return eolComment; } + + /** + * Gets any non-BOL or EOL comment contained within this value. + * + * @return The full contents of this comment, including any comment indicators. + */ + public String getInteriorComment() { return intComment; } + + /** + * Gets any comment associated with this value by its type. + * + * @param type The type of comment being queried. + * @return The full contents of this comment, including any comment indicators. + */ + public String getComment(CommentType type) { + switch (type) { + case BOL: return getBOLComment(); + case EOL: return getEOLComment(); + default: return getInteriorComment(); + } + } + + /** + * Gets any comment that exists above this value. Its indicators will be manually stripped. + * + * @return The text contents of this comment, excluding any comment indicators. + */ + public String getCommentText() { + return getCommentText(CommentType.BOL); + } + + /** + * Gets any comment associated with this value. Its indicators will be manually stripped. + * + * @param type The type of comment being queried. + * @return The text contents of this comment, excluding any comment indicators. + */ + public String getCommentText(CommentType type) { + return stripComment(getComment(type)); + } + + /** + * Adds a comment to be associated with this value. + * + * @param type Whether to place this comment before the line, after the line, or inside the + * object or array, if applicable. + * @param style Whether to use #, //, or another such comment style. + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue setComment(CommentType type, CommentStyle style, String comment) { + return setFullComment(type, formatComment(style, comment)); + } + + /** + * Shorthand for {@link #setComment(CommentType, CommentStyle, String)} which defaults to sending + * a beginning of line comment using the default indicator, #. + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue setComment(String comment) { + return setComment(CommentType.BOL, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #setComment(CommentType, CommentStyle, String)} which defaults to + * sending an end of line comment using the default indicator, #. + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue setEOLComment(String comment) { + return setComment(CommentType.EOL, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #setComment(CommentType, CommentStyle, String)} which defaults to + * sending an interior comment using the default indicator, #. + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue setInteriorComment(String comment) { + return setComment(CommentType.INTERIOR, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #appendComment(CommentType, CommentStyle, String)} which defaults to + * appending a beginning of line comment using the default indicator, # + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue appendComment(String comment) { + return appendComment(CommentType.BOL, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #appendComment(CommentType, CommentStyle, String)} which defaults to + * appending an end of line comment using the default indicator, # + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue appendEOLComment(String comment) { + return appendComment(CommentType.EOL, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #appendComment(CommentType, CommentStyle, String)} which defaults to + * appending an interior comment using the default indicator, # + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue appendInteriorComment(String comment) { + return appendComment(CommentType.INTERIOR, CommentStyle.HASH, comment); + } + + /** + * Adds a new line onto the existing comment in the given position. + * + * @param type Whether to place this comment before the line, after the line, or inside the + * object or array, if applicable. + * @param style Whether to use #, //, or another such comment style. + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue appendComment(CommentType type, CommentStyle style, String comment) { + String existing=getComment(type); + if (existing.isEmpty()) { + return setFullComment(type, formatComment(style, comment)); + } + return setFullComment(type, existing+'\n'+formatComment(style, comment)); + } + + /** + * Shorthand for calling {@link #prependComment(CommentType, CommentStyle, String)} which defaults to + * appending a beginning of line comment using the default indicator, # + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue prependComment(String comment) { + return prependComment(CommentType.BOL, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #prependComment(CommentType, CommentStyle, String)} which defaults to + * appending an end of line comment using the default indicator, # + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue prependEOLComment(String comment) { + return prependComment(CommentType.EOL, CommentStyle.HASH, comment); + } + + /** + * Shorthand for calling {@link #prependComment(CommentType, CommentStyle, String)} which defaults to + * appending an interior comment using the default indicator, # + * + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue prependInteriorComment(String comment) { + return prependComment(CommentType.INTERIOR, CommentStyle.HASH, comment); + } + + /** + * Adds a new line onto the existing comment in the given position. + * + * @param type Whether to place this comment before the line, after the line, or inside the + * object or array, if applicable. + * @param style Whether to use #, //, or another such comment style. + * @param comment The unformatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue prependComment(CommentType type, CommentStyle style, String comment) { + String existing=getComment(type); + if (existing.isEmpty()) { + return setFullComment(type, formatComment(style, comment)); + } + return setFullComment(type, formatComment(style, comment)+"\n"+existing); + } + + /** + * Counterpart to {@link #setComment(CommentType, CommentStyle, String)} which receives + * the full, formatted comment to be stored by this value. + * + * @param type Whether to place this comment before the line, after the line, or inside of the + * object or array, if applicable. + * @param comment The fully-formatted comment to be paired with this value. + * @return this, to enable chaining + */ + public JsonValue setFullComment(CommentType type, String comment) { + switch (type) { + case BOL: bolComment=comment; break; + case EOL: eolComment=comment; break; + case INTERIOR: intComment=comment; break; + } + return this; + } + + /** + * Copies every comment from another JSON value into this value. The input value be null, but + * will simply be ignored, if so. + * + * @param value The JSON value being copied out of. + * @return this, to enable chaining + */ + public JsonValue copyComments(JsonValue value) { + if (value!=null) { + this.bolComment=value.bolComment; + this.eolComment=value.eolComment; + this.intComment=value.intComment; + } + return this; + } + + /** + * Removes every comment from this JSON value. + * + * @return this, to enable chaining. + */ + public JsonValue clearComments() { + this.bolComment=""; + this.eolComment=""; + this.intComment=""; + return this; + } + + /** + * Returns whether the two value contain identical comments. + * + * @param value The JSON value being compared to. + * @return true if the values contain identical comments. + */ + public boolean commentsMatch(JsonValue value) { + return bolComment.equals(value.bolComment) + && eolComment.equals(value.eolComment) + && intComment.equals(value.intComment); + } + + /** + * Generates the formatted expression of the given text as a JSON comment. + * + * @param style Whether to use #, //, or another such comment style. + * @param comment The unformatted comment to be paired with this value. + * @return The formatted, "raw" comment. + */ + public static String formatComment(CommentStyle style, String comment) { + String[] lines=comment.split("\r?\n"); + if (style.equals(CommentStyle.BLOCK)) { + return formatBlockComment(lines); + } + StringBuilder formatted=new StringBuilder(); + // Iterate through all lines in the comment. + for (int i=0; i0) formatted.append(eol); + // Add the indicator and extra space. + if (style.equals(CommentStyle.HASH)) { + formatted.append("# "); + } else { + formatted.append("// "); + } + // Add the actual line from the comment. + formatted.append(lines[i]); + } + return formatted.toString(); + } + + /** + * Generates the formatted expression of the given text as a block style comment. + * + *

Note that this method will be broken by extraneous new lines in the input.

+ * + * @param lines The previously split string lines for this comment. + * @return The formatted, "raw" comment. + */ + public static String formatBlockComment(String... lines) { + StringBuilder formatted=new StringBuilder(); + if (lines.length==1) { + formatted.append("/* "); + formatted.append(lines[0]); + formatted.append(" */"); + return formatted.toString(); + } + formatted.append("/*"); + formatted.append(eol); + for (String line : lines) { + formatted.append(" * "); + formatted.append(line); + formatted.append(eol); + } + formatted.append(" */"); + return formatted.toString(); + } + + /** + * Generates the raw text contents of this comment, stripping any comment indicators. + * + * @param comment The raw comment body, with indicators. + * @return The stripped version of the comment, including its message only. + */ + public static String stripComment(String comment) { + String noSingles=comment.replaceAll("(?<=^|\n\r?)\\s*(//|#)\\s?", ""); + if (noSingles.contains("/*")) { + return noSingles.replaceAll("(\\s*\n\r?)?\\s?\\*/", "") + .replaceAll("(?<=^|\n\r?)\\s*/?\\*\\s?", ""); + } + return noSingles; + } + /** * Returns this JSON value as {@link JsonObject}, assuming that this value represents a JSON * object. If this is not the case, an exception is thrown. @@ -472,6 +915,73 @@ public boolean asBoolean() { public Object asDsf() { throw new UnsupportedOperationException("Not a DSF"); } + + /** + * Unsafe. Returns the raw form of this JSON value. For compatibility with other config wrappers. + * @param the type of object to be returned. + * @return the raw object. + * @throws ClassCastException when the type returned does not match the original value. + */ + @SuppressWarnings("unchecked") + public T asRaw() { + switch (getType()) { + case STRING : return (T) asString(); + case NUMBER : return (T) Double.valueOf(asDouble()); + case OBJECT : return (T) asObject().asRawMap(); + case ARRAY : return (T) asArray().asRawList(); + case BOOLEAN : return (T) Boolean.valueOf(asBoolean()); + case DSF : return (T) asDsf(); + default : return null; + } + } + + /** + * Generates a new instance of this value which is largely identical to this one. If this value + * is a container type, a new container will be generated containing the original values. + * + * @return A new instance of this value with the same data, comments, and other metadata. + */ + public JsonValue shallowCopy() { + return deepCopy(); + } + + /** + * Generates a new instance of this value which is exactly identical to this one. Regardless of + * which type this value is, it will exclusively contain new instances recursively. + * + *

This implementation ignores whether the value has been accessed, but this may additionally + * be copied with an optional boolean parameter.

+ * + * @return A new instance of this value with the same data, comments, and other metadata. + */ + public JsonValue deepCopy() { + return deepCopy(false); + } + + /** + * Generates a new instance of this value which is exactly identical to this one. Regardless of + * which type this value is, it will exclusively contain new instances recursively. + * + * @param trackAccess Whether to additionally persist access records to the new value. + * @return A new instance of this value with the same data, comments, and other metadata. + */ + public abstract JsonValue deepCopy(boolean trackAccess); + + /** + * Copies all metadata from another JSON value, including its comments, line length, + * whether it was accessed, and any other data. + * + * @param value The value being copied from. + * @param trackAccess Whether to additionally copy access trackers. + * @return this, for method chaining. + */ + public JsonValue copyMetadata(JsonValue value, boolean trackAccess) { + this.copyComments(value); + this.numLines=value.numLines; + if (trackAccess) this.accessed=value.accessed; + return this; + } + /** * Writes the JSON representation of this value to the given writer in its minimal form, without * any additional whitespace. @@ -501,7 +1011,8 @@ public void writeTo(Writer writer, Stringify format) throws IOException { switch (format) { case PLAIN: new JsonWriter(false).save(this, buffer, 0); break; case FORMATTED: new JsonWriter(true).save(this, buffer, 0); break; - case HJSON: new HjsonWriter(null).save(this, buffer, 0, "", true); break; + case HJSON: new HjsonWriter(null, false).save(this, buffer, 0, "", true); break; + case HJSON_COMMENTS: new HjsonWriter(null, true).save(this, buffer, 0, "", true); break; } buffer.flush(); } @@ -587,7 +1098,11 @@ public boolean equals(Object object) { @Override public int hashCode() { - return super.hashCode(); + int result = 1; + result *= 59 + (this.bolComment == null ? 43 : this.bolComment.hashCode()); + result *= 59 + (this.eolComment == null ? 43 : this.eolComment.hashCode()); + result *= 59 + (this.intComment == null ? 43 : this.intComment.hashCode()); + return result; } static boolean isPunctuatorChar(int c) { diff --git a/src/main/org/hjson/JsonWriter.java b/src/main/org/hjson/JsonWriter.java index 8d73c7f..a672e35 100644 --- a/src/main/org/hjson/JsonWriter.java +++ b/src/main/org/hjson/JsonWriter.java @@ -46,7 +46,7 @@ public void save(JsonValue value, Writer tw, int level) throws IOException { switch (value.getType()) { case OBJECT: JsonObject obj=value.asObject(); - if (obj.size()>0) nl(tw, level); + if (obj.size()>0 && level>0) nl(tw, level); tw.write('{'); for (JsonObject.Member pair : obj) { if (following) tw.write(","); @@ -98,7 +98,7 @@ public void save(JsonValue value, Writer tw, int level) throws IOException { static String escapeName(String name) { boolean needsEscape=name.length()==0; for(char ch : name.toCharArray()) { - if (HjsonParser.isWhiteSpace(ch) || ch=='{' || ch=='}' || ch=='[' || ch==']' || ch==',' || ch==':') { + if (HjsonParser.isWhitespace(ch) || ch=='{' || ch=='}' || ch=='[' || ch==']' || ch==',' || ch==':') { needsEscape=true; break; } diff --git a/src/main/org/hjson/Stringify.java b/src/main/org/hjson/Stringify.java index b7f7484..cc1e103 100644 --- a/src/main/org/hjson/Stringify.java +++ b/src/main/org/hjson/Stringify.java @@ -29,5 +29,7 @@ public enum Stringify { FORMATTED, /** Hjson. */ HJSON, + /** Hjson w/ comments */ + HJSON_COMMENTS, } diff --git a/src/test/org/hjson/test/JsonObjectTest.java b/src/test/org/hjson/test/JsonObjectTest.java new file mode 100644 index 0000000..0393f80 --- /dev/null +++ b/src/test/org/hjson/test/JsonObjectTest.java @@ -0,0 +1,43 @@ +package org.hjson.test; + +import org.hjson.JsonObject; +import org.hjson.JsonValue; + +import java.util.Arrays; + +public class JsonObjectTest extends JsonTest { + + @Override + void run() { + getUnusedPaths_returnsUnusedPathsOnly(); + getUsedPaths_returnsUsedPathsOnly(); + getAllPaths_returnsAllPaths(); + } + + void getUnusedPaths_returnsUnusedPathsOnly() { + final JsonObject subject = parse("a:{b:[{c:{}}]}"); + subject.get("a"); + + assertEquals(Arrays.asList("a.b", "a.b[0]", "a.b[0].c"), subject.getUnusedPaths()); + } + + void getUsedPaths_returnsUsedPathsOnly() { + final JsonObject subject = parse("a:{b:[{c:[]}]},x:[{y:{z:{}}}]"); + subject.get("a").asObject().get("b"); + subject.get("x").asArray().get(0); + + assertEquals(Arrays.asList("a", "a.b", "x", "x[0]"), subject.getUsedPaths()); + } + + void getAllPaths_returnsAllPaths() { + final JsonObject subject = parse("a:{},b:{},c:[[{d:[]}],[]]"); + subject.get("a"); + subject.get("c").asArray().get(0); + + assertEquals(Arrays.asList("a", "b", "c", "c[0]", "c[0][0]", "c[0][0].d", "c[1]"), subject.getAllPaths()); + } + + private static JsonObject parse(final String json) { + return JsonValue.readHjson(json).asObject(); + } +} diff --git a/src/test/org/hjson/test/JsonTest.java b/src/test/org/hjson/test/JsonTest.java new file mode 100644 index 0000000..6bf8893 --- /dev/null +++ b/src/test/org/hjson/test/JsonTest.java @@ -0,0 +1,78 @@ +package org.hjson.test; + +import org.hjson.JsonValue; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public abstract class JsonTest { + + private final Set methodsReported = new HashSet<>(); + protected boolean testsPassing = true; + + abstract void run(); + + final boolean allPassing() { + JsonValue.setEol("\n"); + run(); + return testsPassing; + } + + protected final void assertEquals(Object expected, Object actual) { + if (!Objects.equals(expected, actual)) { + System.err.println("Expected:\n" + expected + "\nActual:\n" + actual); + fail(); + } else { + pass(); + } + } + + protected final void assertNotEquals(Object expected, Object actual) { + if (Objects.equals(expected, actual)) { + System.err.println("Values should not match:\nExpected:\n" + expected + "\nActual:\n" + actual); + fail(); + } else { + pass(); + } + } + + protected final void assertSame(Object expected, Object actual) { + if (expected != actual) { + System.err.println("Expected instance equality:\nExpected:\n" + expected + "\nActual:\n" + actual); + fail(); + } else { + pass(); + } + } + + protected final void assertNotSame(Object expected, Object actual) { + if (expected == actual) { + System.err.println("Should be a different instance:\nExpected:\n" + expected + "\nActual:\n" + actual); + fail(); + } else { + pass(); + } + } + + protected final void pass() { + final String caller = getCaller(); + if (methodsReported.add(caller)) { + System.out.println("- " + caller + " OK"); + } + } + + protected final void fail() { + System.out.println("- " + getCaller() + " FAILED @ " + getCallerDetails()); + this.testsPassing = false; + } + + private String getCaller() { + return Thread.currentThread().getStackTrace()[4].getMethodName(); + } + + private String getCallerDetails() { + final StackTraceElement[] st = Thread.currentThread().getStackTrace(); + return st[3].getMethodName() + "[" + st[4].getLineNumber() + "]"; + } +} diff --git a/src/test/org/hjson/test/JsonValueTest.java b/src/test/org/hjson/test/JsonValueTest.java new file mode 100644 index 0000000..e57c083 --- /dev/null +++ b/src/test/org/hjson/test/JsonValueTest.java @@ -0,0 +1,156 @@ +package org.hjson.test; + +import org.hjson.CommentStyle; +import org.hjson.JsonLiteral; +import org.hjson.JsonObject; +import org.hjson.JsonValue; + +final class JsonValueTest extends JsonTest { + + @Override + void run() { + formatComment_generatesHashComment(); + formatComment_generatesLineComment(); + formatComment_generatesBlockComment(); + formatComment_generatesMultilineHash(); + formatComment_generatesMultilineLine(); + formatComment_generatesMultilineBlock(); + stripComment_stripsHashComment(); + stripComment_stripsLineComment(); + stripComment_stripsBlockComment(); + stripComment_stripsMultilineHash(); + stripComment_stripsMultilineLine(); + stripComment_stripsMultilineBlock(); + stripComment_stripsComplexComment(); + setComment_getCommentText_preservesExactText(); + shallowCopy_deeplyCopiesValues(); + shallowCopy_shallowCopiesContainer(); + deepCopy_deeplyCopiesContainer(); + } + + private void formatComment_generatesHashComment() { + final String comment = "here's a comment"; + final String expected = "# here's a comment"; + + assertEquals(expected, JsonValue.formatComment(CommentStyle.HASH, comment)); + } + + private void formatComment_generatesLineComment() { + final String comment = "here's another comment"; + final String expected = "// here's another comment"; + + assertEquals(expected, JsonValue.formatComment(CommentStyle.LINE, comment)); + } + + private void formatComment_generatesBlockComment() { + final String comment = "here's a block comment"; + final String expected = "/* here's a block comment */"; + + assertEquals(expected, JsonValue.formatComment(CommentStyle.BLOCK, comment)); + } + + private void formatComment_generatesMultilineHash() { + final String comment = "here's a comment\nwith multiple lines"; + final String expected = "# here's a comment\n# with multiple lines"; + + assertEquals(expected, JsonValue.formatComment(CommentStyle.HASH, comment)); + } + + private void formatComment_generatesMultilineLine() { + final String comment = "here's a comment\nwith multiple lines"; + final String expected = "// here's a comment\n// with multiple lines"; + + assertEquals(expected, JsonValue.formatComment(CommentStyle.LINE, comment)); + } + + private void formatComment_generatesMultilineBlock() { + final String comment = "here's a block comment\nwith multiple lines"; + final String expected = "/*\n * here's a block comment\n * with multiple lines\n */"; + + assertEquals(expected, JsonValue.formatComment(CommentStyle.BLOCK, comment)); + } + + private void stripComment_stripsHashComment() { + final String comment = "# hashed comment"; + final String expected = "hashed comment"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void stripComment_stripsLineComment() { + final String comment = "// line comment"; + final String expected = "line comment"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void stripComment_stripsBlockComment() { + final String comment = "/* block comment */"; + final String expected = "block comment"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void stripComment_stripsMultilineHash() { + final String comment = "# hashed comment\n# second line"; + final String expected = "hashed comment\nsecond line"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void stripComment_stripsMultilineLine() { + final String comment = "// line comment\n// second line"; + final String expected = "line comment\nsecond line"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void stripComment_stripsMultilineBlock() { + final String comment = "/* block comment\n* second line\n */"; + final String expected = "block comment\nsecond line"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void stripComment_stripsComplexComment() { + final String comment = "/* block comment\n* second line\n /*\n# third line\n// fourth line"; + final String expected = "block comment\nsecond line\nthird line\nfourth line"; + + assertEquals(expected, JsonValue.stripComment(comment)); + } + + private void setComment_getCommentText_preservesExactText() { + final String comment = "Hello, World!"; + final JsonValue value = JsonLiteral.jsonNull().setComment(comment); + + assertEquals(comment, value.getCommentText()); + } + + private void shallowCopy_deeplyCopiesValues() { + final JsonValue value = JsonValue.valueOf(true).setAccessed(true).setComment("comment"); + final JsonValue clone = value.shallowCopy(); + + assertEquals(value, clone); + assertNotEquals(value.isAccessed(), clone.isAccessed()); + } + + private void shallowCopy_shallowCopiesContainer() { + final JsonObject value = new JsonObject().add("test", "test"); + final JsonObject clone = (JsonObject) value.shallowCopy(); + + assertEquals(value, clone); + for (int i = 0; i < value.size(); i++) { + assertSame(value.get(i), clone.get(i)); + } + } + + private void deepCopy_deeplyCopiesContainer() { + final JsonObject value = new JsonObject().add("test", "test"); + final JsonObject clone = (JsonObject) value.deepCopy(); + + assertEquals(value, clone); + for (int i = 0; i < value.size(); i++) { + assertNotSame(value.get(i), clone.get(i)); + } + } +} diff --git a/src/test/org/hjson/test/Main.java b/src/test/org/hjson/test/Main.java index c06e8d2..24dded1 100644 --- a/src/test/org/hjson/test/Main.java +++ b/src/test/org/hjson/test/Main.java @@ -2,6 +2,8 @@ import org.hjson.*; import java.io.*; +import java.nio.charset.StandardCharsets; + import static java.lang.System.out; public class Main { @@ -10,7 +12,7 @@ public static String convertStreamToString(InputStream is) throws IOException { Writer writer=new StringWriter(); char[] buffer=new char[1024]; try { - Reader reader=new BufferedReader(new InputStreamReader(is, "UTF-8")); + Reader reader=new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); int n; while ((n=reader.read(buffer))!=-1) writer.write(buffer, 0, n); } finally { @@ -24,6 +26,7 @@ private static String load(String file, boolean cr) throws Exception { if (res==null) throw new Exception(file+" not found!"); String text=convertStreamToString(res); String std=text.replace("\r", ""); // make sure we have unix style text regardless of the input + if (std.endsWith("\n")) std=std.substring(0, std.length()-1); // clip off empty lines at the end of the input return cr ? std.replace("\n", "\r\n") : std; } @@ -31,17 +34,25 @@ private static boolean test(String name, String file, boolean inputCr, boolean o int extIdx=file.lastIndexOf('.'); boolean isJson=extIdx>=0 && file.substring(extIdx).equals(".json"); boolean shouldFail=name.startsWith("fail"); + boolean comments=name.startsWith("comments"); + boolean sameLine=name.startsWith("sameline"); JsonValue.setEol(outputCr?"\r\n":"\n"); String text=load(file, inputCr); try { HjsonOptions opt=new HjsonOptions(); - opt.setParseLegacyRoot(false); + if (comments) { + opt.setOutputComments(true); + } + if (sameLine) { + opt.setBracesSameLine(true); + } JsonValue data=JsonValue.readHjson(text, opt); String data1=data.toString(Stringify.FORMATTED); - String hjson1=data.toString(Stringify.HJSON); + String hjson1=data.toString(opt); + if (!shouldFail) { JsonValue result=JsonValue.readJSON(load(name+"_result.json", inputCr)); String data2=result.toString(Stringify.FORMATTED); @@ -82,7 +93,7 @@ static boolean failErr(String name, String type, String s1, String s2) { public static void main(String[] args) throws Exception { - out.println("running tests..."); + out.println("running output tests..."); String[] testNames=load("testlist.txt", false).split("\n"); boolean allOK=true; @@ -103,6 +114,9 @@ && test(name, file, true, true)) { else { allOK=false; } } + if (!new JsonValueTest().allPassing()) allOK=false; + if (!new JsonObjectTest().allPassing()) allOK=false; + if (!allOK) { out.println("FAILED!"); System.exit(1);