From eea00864cd5b8b33f1e4904ab54fcd88b0f179eb Mon Sep 17 00:00:00 2001 From: marhali Date: Sat, 6 Sep 2025 12:07:52 +0200 Subject: [PATCH] feat: apply patches to add support for json5 comments Authors: @Zim-Inn, @Ultreon --- .../java/de/marhali/json5/Json5Array.java | 419 +++++++----- .../java/de/marhali/json5/Json5Boolean.java | 12 + .../java/de/marhali/json5/Json5Element.java | 92 ++- .../de/marhali/json5/Json5Hexadecimal.java | 43 +- src/main/java/de/marhali/json5/Json5Null.java | 9 +- .../java/de/marhali/json5/Json5Number.java | 12 + .../java/de/marhali/json5/Json5Object.java | 112 ++- .../java/de/marhali/json5/Json5Options.java | 48 +- .../de/marhali/json5/Json5OptionsBuilder.java | 42 +- .../java/de/marhali/json5/Json5Primitive.java | 142 ++-- .../java/de/marhali/json5/Json5String.java | 17 + .../de/marhali/json5/stream/Json5Lexer.java | 641 ++++++++++-------- .../de/marhali/json5/stream/Json5Parser.java | 40 +- .../de/marhali/json5/stream/Json5Writer.java | 104 ++- src/test/java/de/marhali/json5/TestJson5.java | 63 +- .../de/marhali/json5/TestJson5Comments.java | 211 ++++++ .../de/marhali/json5/TestJson5Object.java | 8 +- .../de/marhali/json5/TestJson5Options.java | 43 +- .../de/marhali/json5/TestJson5Parser.java | 101 ++- .../de/marhali/json5/TestJson5Writer.java | 34 +- .../json5/TestJson5WriterQuoteless.java | 171 +++++ src/test/resources/expect.comments.json5 | 21 + .../resources/expect.quoteless.comment.json5 | 31 + ...351\207\212\346\213\267\350\264\235.json5" | 25 + src/test/resources/test.comments.json5 | 21 + 25 files changed, 1807 insertions(+), 655 deletions(-) create mode 100644 src/test/java/de/marhali/json5/TestJson5Comments.java create mode 100644 src/test/java/de/marhali/json5/TestJson5WriterQuoteless.java create mode 100644 src/test/resources/expect.comments.json5 create mode 100644 src/test/resources/expect.quoteless.comment.json5 create mode 100644 "src/test/resources/expect.\346\227\240\346\263\250\351\207\212\346\213\267\350\264\235.json5" create mode 100644 src/test/resources/test.comments.json5 diff --git a/src/main/java/de/marhali/json5/Json5Array.java b/src/main/java/de/marhali/json5/Json5Array.java index 86349e9..3a0e22d 100644 --- a/src/main/java/de/marhali/json5/Json5Array.java +++ b/src/main/java/de/marhali/json5/Json5Array.java @@ -32,6 +32,7 @@ * @author Inderjeet Singh * @author Joel Leitch */ +@SuppressWarnings("unused") public final class Json5Array extends Json5Element implements Iterable { private final List elements; @@ -56,11 +57,201 @@ public Json5Array deepCopy() { for (Json5Element element : elements) { result.add(element.deepCopy()); } + result.setComment(this.getComment()); return result; } return new Json5Array(); } + @Override + public Json5Element noCommentCopy() { + if (!elements.isEmpty()) { + Json5Array result = new Json5Array(elements.size()); + for (Json5Element element : elements) { + result.add(element.noCommentCopy()); + } + return result; + } + return new Json5Array(); + } + + /** + * Returns true if the array is empty + * + * @return true if the array is empty + */ + public boolean isEmpty() { + return elements.isEmpty(); + } + + /** + * convenience method to get this array as a boolean if it contains a single element. + * + * @return get this element as a boolean if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid boolean. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public boolean getAsBoolean() { + if (elements.size() == 1) { + return elements.get(0).getAsBoolean(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link Number} if it contains a single element. + * + * @return get this element as a number if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid Number. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public Number getAsNumber() { + if (elements.size() == 1) { + return elements.get(0).getAsNumber(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link String} if it contains a single element. + * + * @return get this element as a String if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid String. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public String getAsString() { + if (elements.size() == 1) { + return elements.get(0).getAsString(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a double if it contains a single element. + * + * @return get this element as a double if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid double. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public double getAsDouble() { + if (elements.size() == 1) { + return elements.get(0).getAsDouble(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a float if it contains a single element. + * + * @return get this element as a float if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid float. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public float getAsFloat() { + if (elements.size() == 1) { + return elements.get(0).getAsFloat(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a long if it contains a single element. + * + * @return get this element as a long if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid long. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public long getAsLong() { + if (elements.size() == 1) { + return elements.get(0).getAsLong(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as an integer if it contains a single element. + * + * @return get this element as an integer if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid integer. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public int getAsInt() { + if (elements.size() == 1) { + return elements.get(0).getAsInt(); + } + throw new IllegalStateException(); + } + + @Override + public byte getAsByte() { + if (elements.size() == 1) { + return elements.get(0).getAsByte(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link BigDecimal} if it contains a single element. + * + * @return get this element as a {@link BigDecimal} if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive}. + * @throws NumberFormatException if the element at index 0 is not a valid {@link BigDecimal}. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public BigDecimal getAsBigDecimal() { + if (elements.size() == 1) { + return elements.get(0).getAsBigDecimal(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link BigInteger} if it contains a single element. + * + * @return get this element as a {@link BigInteger} if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive}. + * @throws NumberFormatException if the element at index 0 is not a valid {@link BigInteger}. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public BigInteger getAsBigInteger() { + if (elements.size() == 1) { + return elements.get(0).getAsBigInteger(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a primitive short if it contains a single element. + * + * @return get this element as a primitive short if it is a single element array. + * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and + * is not a valid short. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public short getAsShort() { + if (elements.size() == 1) { + return elements.get(0).getAsShort(); + } + throw new IllegalStateException(); + } + /** * Adds the specified boolean to self. * @@ -120,7 +311,8 @@ public void addAll(Json5Array array) { /** * Replaces the element at the specified position in this array with the specified element. - * Element can be null. + * Element can be null. + * * @param index index of the element to replace * @param element element to be stored at the specified position * @return the element previously at the specified position @@ -133,6 +325,7 @@ public Json5Element set(int index, Json5Element element) { /** * Removes the first occurrence of the specified element from this array, if it is present. * If the array does not contain the element, it is unchanged. + * * @param element element to be removed from this array, if present * @return true if this array contained the specified element, false otherwise */ @@ -144,18 +337,21 @@ public boolean remove(Json5Element element) { * Removes the element at the specified position in this array. Shifts any subsequent elements * to the left (subtracts one from their indices). Returns the element that was removed from * the array. + * * @param index index the index of the element to be removed * @return the element previously at the specified position * @throws IndexOutOfBoundsException if the specified index is outside the array bounds */ + @SuppressWarnings("UnusedReturnValue") public Json5Element remove(int index) { return elements.remove(index); } /** * Returns true if this array contains the specified element. - * @return true if this array contains the specified element. + * * @param element whose presence in this array is to be tested + * @return true if this array contains the specified element. */ public boolean contains(Json5Element element) { return elements.contains(element); @@ -171,12 +367,48 @@ public int size() { } /** - * Returns true if the array is empty + * 将本对象的非空注释覆盖合并到目标对象 + * 取两者长度较短者作为最大遍历长度 + * 要求目标对象的同索引子元素应与本子元素具备类型一致性 * - * @return true if the array is empty + * @param target 目标对象 */ - public boolean isEmpty() { - return elements.isEmpty(); + public void mergeCommentTo(Json5Array target) { + super.copyCommentTo(target); + if (!elements.isEmpty()) { + int loop = Math.min(elements.size(), target.size()); + for (int i = 0; i < loop; i++) { + Json5Element element = this.get(i); + if (!element.hasComment()) { + continue; + } + if (element instanceof Json5Primitive) { + element.copyCommentTo(target.get(i)); + } else if (element instanceof Json5Array) { + ((Json5Array) element).mergeCommentTo((Json5Array) target.get(i)); + } else if (element instanceof Json5Object) { + ((Json5Object) element).mergeCommentTo((Json5Object) target.get(i)); + } + } + } + } + + /** + * Sets the comment for an element. + * comment字段不会参与equals 和hashCode + * + * @param index index of elements + * @param comment comment for the member + */ + public void setComment(int index, String comment) { + Json5Element element = get(index); + if (element instanceof Json5Null) { + Json5Null json5Null = new Json5Null(); + json5Null.setComment(comment); + elements.set(index, json5Null); + } else { + element.setComment(comment); + } } /** @@ -201,181 +433,26 @@ public Json5Element get(int i) { return elements.get(i); } - /** - * convenience method to get this array as a {@link Number} if it contains a single element. - * - * @return get this element as a number if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid Number. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public Number getAsNumber() { - if (elements.size() == 1) { - return elements.get(0).getAsNumber(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a {@link String} if it contains a single element. - * - * @return get this element as a String if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid String. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public String getAsString() { - if (elements.size() == 1) { - return elements.get(0).getAsString(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a double if it contains a single element. - * - * @return get this element as a double if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid double. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public double getAsDouble() { - if (elements.size() == 1) { - return elements.get(0).getAsDouble(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a {@link BigDecimal} if it contains a single element. - * - * @return get this element as a {@link BigDecimal} if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive}. - * @throws NumberFormatException if the element at index 0 is not a valid {@link BigDecimal}. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public BigDecimal getAsBigDecimal() { - if (elements.size() == 1) { - return elements.get(0).getAsBigDecimal(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a {@link BigInteger} if it contains a single element. - * - * @return get this element as a {@link BigInteger} if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive}. - * @throws NumberFormatException if the element at index 0 is not a valid {@link BigInteger}. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public BigInteger getAsBigInteger() { - if (elements.size() == 1) { - return elements.get(0).getAsBigInteger(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a float if it contains a single element. - * - * @return get this element as a float if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid float. - * @throws IllegalStateException if the array has more than one element. - */ @Override - public float getAsFloat() { - if (elements.size() == 1) { - return elements.get(0).getAsFloat(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a long if it contains a single element. - * - * @return get this element as a long if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid long. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public long getAsLong() { - if (elements.size() == 1) { - return elements.get(0).getAsLong(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as an integer if it contains a single element. - * - * @return get this element as an integer if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid integer. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public int getAsInt() { - if (elements.size() == 1) { - return elements.get(0).getAsInt(); - } - throw new IllegalStateException(); - } - - @Override - public byte getAsByte() { - if (elements.size() == 1) { - return elements.get(0).getAsByte(); - } - throw new IllegalStateException(); - } - - /** - * convenience method to get this array as a primitive short if it contains a single element. - * - * @return get this element as a primitive short if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid short. - * @throws IllegalStateException if the array has more than one element. - */ - @Override - public short getAsShort() { - if (elements.size() == 1) { - return elements.get(0).getAsShort(); - } - throw new IllegalStateException(); + public int hashCode() { + return elements.hashCode(); } /** - * convenience method to get this array as a boolean if it contains a single element. + * 暂不支持直接转POJO对象,请使用toStandardString然后使用其它反序列化框架处理 + * @see Json5Element#toString(Json5Options) + * @see Json5Element#toStandardString * - * @return get this element as a boolean if it is single element array. - * @throws ClassCastException if the element in the array is of not a {@link Json5Primitive} and - * is not a valid boolean. - * @throws IllegalStateException if the array has more than one element. + * @param clazz The class of the POJO to convert to. + * @param The type of the POJO. + * @return An list instance of the specified POJO class, populated with data from this Json5Array. */ - @Override - public boolean getAsBoolean() { - if (elements.size() == 1) { - return elements.get(0).getAsBoolean(); - } - throw new IllegalStateException(); + public List toPojoList(Class clazz) throws Exception { + throw new Exception("不支持的操作!"); } @Override public boolean equals(Object o) { return (o == this) || (o instanceof Json5Array && ((Json5Array) o).elements.equals(elements)); } - - @Override - public int hashCode() { - return elements.hashCode(); - } } \ No newline at end of file diff --git a/src/main/java/de/marhali/json5/Json5Boolean.java b/src/main/java/de/marhali/json5/Json5Boolean.java index 374fc0b..bfa7958 100644 --- a/src/main/java/de/marhali/json5/Json5Boolean.java +++ b/src/main/java/de/marhali/json5/Json5Boolean.java @@ -25,4 +25,16 @@ public final class Json5Boolean extends Json5Primitive { public Json5Boolean(Boolean value) { super(value); } + + @Override + public Json5Element deepCopy() { + Json5Boolean o = new Json5Boolean((Boolean) value); + o.setComment(getComment()); + return o; + } + + @Override + public Json5Element noCommentCopy() { + return new Json5Boolean((Boolean) value); + } } diff --git a/src/main/java/de/marhali/json5/Json5Element.java b/src/main/java/de/marhali/json5/Json5Element.java index bd7167a..5d49cbc 100644 --- a/src/main/java/de/marhali/json5/Json5Element.java +++ b/src/main/java/de/marhali/json5/Json5Element.java @@ -34,12 +34,54 @@ * @author Joel Leitch */ public abstract class Json5Element { + private String comment; + + /** + * Gets the comment associated with this element. + * + * @return The comment string, or null if none exists. + */ + public String getComment() { + return this.comment; + } + + /** + * 对于Json5Null对象的注释,请通过以下方法来set + * @see Json5Object#setComment(String, String) + * @see Json5Array#setComment(int, String) + * comment字段不会参与equals 和hashCode + * + * @param comment The comment string. Can be multi-line. + */ + public void setComment(String comment) { + // 不应该直接操作Json5Null.INSTANCE实例的注释 + // 应该通过JsonObject或Json5Array的 setComment方法来操作 + if (this == Json5Null.INSTANCE) { + return; + } + this.comment = comment; + } + + /** + * Checks if a comment is associated with this element. + * + * @return True if a comment exists, false otherwise. + */ + public boolean hasComment() { + return this.comment != null; + } + /** - * @return A deep copy of this element. Immutable elements like primitives - * and nulls are not copied. + * @return 深拷贝对象,包含各个子元素的注释 */ public abstract Json5Element deepCopy(); + /** + * + * @return 返回一个不带任何注释的拷贝对象 + */ + public abstract Json5Element noCommentCopy(); + /** * provides check for verifying if this element is an array or not. * @@ -76,6 +118,40 @@ public boolean isJson5Null() { return this instanceof Json5Null; } + /** + * 当对象为空字符串 空Json5Object 空Json5Array Json5Null时返回True,其它情况返回False. + * + * @return 对象是否为空 + */ + public boolean isEmpty() { + if (this instanceof Json5Null) { + return true; + } else if (this instanceof Json5Object) { + // noinspection RedundantCast + return ((Json5Object) this).isEmpty(); + } else if (this instanceof Json5Array) { + // noinspection RedundantCast + return ((Json5Array) this).isEmpty(); + } else if (this instanceof Json5String) { + // noinspection RedundantCast + return ((Json5String) this).isEmpty(); + } + return false; + } + + /** + * 将本对象的注释复制到目标对象 + * + * @param target 目标对象 + * @see Json5Object#mergeCommentTo(Json5Object) + * @see Json5Array#mergeCommentTo(Json5Array) + */ + public void copyCommentTo(Json5Element target) { + if (target != null && this.comment != null) { + target.setComment(this.comment); + } + } + /** * convenience method to get this element as a {@link Json5Object}. If the element is of some * other type, a {@link IllegalStateException} will result. Hence it is best to use this method @@ -133,6 +209,7 @@ public Json5Primitive getAsJson5Primitive() { * @return get this element as a {@link Json5Null}. * @throws IllegalStateException if the element is of another type. */ + @SuppressWarnings("unused") public Json5Null getAsJson5Null() { if (isJson5Null()) { return (Json5Null) this; @@ -286,6 +363,7 @@ public short getAsShort() { /** * Returns a simple String representation of this element. * For pretty-printing use {@link Json5Writer} with custom configuration options. + * * @see #toString(Json5Options) */ @Override @@ -293,8 +371,17 @@ public String toString() { return toString(Json5Options.DEFAULT); } + /** + * 将对象转为无注释标准json字符串 + * @return 压缩的json字符串 + */ + public String toStandardString() { + return toString(new Json5OptionsBuilder().notWriteComments().build()); + } + /** * Returns the String representation of this element. + * * @param options Configured serialization behaviour * @return Stringified representation of this element */ @@ -306,7 +393,6 @@ public String toString(Json5Options options) { Json5Writer json5Writer = new Json5Writer(options, stringWriter); json5Writer.write(this); return stringWriter.toString(); - } catch (IOException e) { throw new AssertionError(e); } diff --git a/src/main/java/de/marhali/json5/Json5Hexadecimal.java b/src/main/java/de/marhali/json5/Json5Hexadecimal.java index 391a905..12731db 100644 --- a/src/main/java/de/marhali/json5/Json5Hexadecimal.java +++ b/src/main/java/de/marhali/json5/Json5Hexadecimal.java @@ -25,6 +25,23 @@ * @author Marcel Haßlinger */ public final class Json5Hexadecimal extends Json5Primitive { + /** + * Creates a primitive containing a hex value. + * + * @param hex the value to create the primitive with. + */ + public Json5Hexadecimal(BigInteger hex) { + super(hex); + } + + /** + * Creates a primitive containing a hex value. For String to Number conversion see {@link #parseHexString(String)} + * + * @param hex the value to create the primitive with. + */ + public Json5Hexadecimal(String hex) { + super(parseHexString(hex)); + } /** * Converts the provided hex string into it's number representation. @@ -49,36 +66,30 @@ public static BigInteger parseHexString(String hex) { /** * Converts the provided number into it's hex literal character representation. * - * @param bigInteger the number value + * @param bigInteger the number value * @param prefixPositive Prefix positive values with {@code +0x...} if true otherwise {@code 0x...}. * @return Hex character string including prefix */ public static String serializeHexString(BigInteger bigInteger, boolean prefixPositive) { Objects.requireNonNull(bigInteger); - if(bigInteger.signum() >= 0) { + if (bigInteger.signum() >= 0) { return (prefixPositive ? "+0x" : "0x") + bigInteger.toString(16); } else { return "-0x" + bigInteger.abs().toString(16); } } - /** - * Creates a primitive containing a hex value. - * - * @param hex the value to create the primitive with. - */ - public Json5Hexadecimal(BigInteger hex) { - super(hex); + @Override + public Json5Element deepCopy() { + Json5Hexadecimal o = new Json5Hexadecimal((BigInteger) value); + o.setComment(getComment()); + return o; } - /** - * Creates a primitive containing a hex value. For String to Number conversion see {@link #parseHexString(String)} - * - * @param hex the value to create the primitive with. - */ - public Json5Hexadecimal(String hex) { - super(parseHexString(hex)); + @Override + public Json5Element noCommentCopy() { + return new Json5Hexadecimal((BigInteger) value); } /** diff --git a/src/main/java/de/marhali/json5/Json5Null.java b/src/main/java/de/marhali/json5/Json5Null.java index 1e1f2bc..2efaadb 100644 --- a/src/main/java/de/marhali/json5/Json5Null.java +++ b/src/main/java/de/marhali/json5/Json5Null.java @@ -31,13 +31,20 @@ public final class Json5Null extends Json5Element { /** * Constructor for internal use only. Use {@link #INSTANCE} instead. */ - private Json5Null() {} + public Json5Null() { } /** * Returns the same instance since it is an immutable value */ @Override public Json5Null deepCopy() { + Json5Null json5Null = new Json5Null(); + json5Null.setComment(this.getComment()); + return json5Null; + } + + @Override + public Json5Element noCommentCopy() { return INSTANCE; } diff --git a/src/main/java/de/marhali/json5/Json5Number.java b/src/main/java/de/marhali/json5/Json5Number.java index a988d6b..8a4680a 100644 --- a/src/main/java/de/marhali/json5/Json5Number.java +++ b/src/main/java/de/marhali/json5/Json5Number.java @@ -25,4 +25,16 @@ public final class Json5Number extends Json5Primitive { public Json5Number(Number number) { super(number); } + + @Override + public Json5Element deepCopy() { + Json5Number o = new Json5Number((Number) value); + o.setComment(getComment()); + return o; + } + + @Override + public Json5Element noCommentCopy() { + return new Json5Number((Number) value); + } } diff --git a/src/main/java/de/marhali/json5/Json5Object.java b/src/main/java/de/marhali/json5/Json5Object.java index 88a3341..7b30656 100644 --- a/src/main/java/de/marhali/json5/Json5Object.java +++ b/src/main/java/de/marhali/json5/Json5Object.java @@ -24,15 +24,15 @@ /** * A class representing an object type in Json. An object consists of name-value pairs where names - * are strings, and values are any other type of {@link Json5Element}. This allows for a creating a + * are strings, and values are any other type of {@link Json5Element}. This allows for creating a * tree of Json5Elements. The member elements of this object are maintained in order they were added. * * @author Inderjeet Singh * @author Joel Leitch */ +@SuppressWarnings("unused") public final class Json5Object extends Json5Element { - private final LinkedTreeMap members = - new LinkedTreeMap(); + private final LinkedTreeMap members = new LinkedTreeMap(); /** * Creates a deep copy of this element and all its children @@ -43,9 +43,24 @@ public Json5Object deepCopy() { for (Map.Entry entry : members.entrySet()) { result.add(entry.getKey(), entry.getValue().deepCopy()); } + result.setComment(this.getComment()); return result; } + @Override + public Json5Element noCommentCopy() { + Json5Object result = new Json5Object(); + for (Map.Entry entry : members.entrySet()) { + result.add(entry.getKey(), entry.getValue().noCommentCopy()); + } + return result; + } + + @Override + public boolean isEmpty() { + return members.isEmpty(); + } + /** * Adds a member, which is a name-value pair, to self. The name must be a String, but the value * can be an arbitrary Json5Element, thereby allowing you to build a full tree of Json5Elements @@ -56,6 +71,33 @@ public Json5Object deepCopy() { */ public void add(String property, Json5Element value) { members.put(property, value == null ? Json5Null.INSTANCE : value); + if (value != null && value.hasComment()) { + setComment(property, value.getComment()); + } + } + + /** + * 将本对象的非空注释覆盖合并到目标对象 + * 要求目标对象的同key子节点应与本子节点具备类型一致性 + * + * @param target 目标对象 + */ + public void mergeCommentTo(Json5Object target) { + super.copyCommentTo(target); + for (String key : target.keySet()) { + Json5Element element = this.get(key); + if (element != null && !element.hasComment()) { + continue; + } + if (element instanceof Json5Primitive) { + element.copyCommentTo(target.get(key)); + } else if (element instanceof Json5Array) { + ((Json5Array) element).mergeCommentTo((Json5Array) target.get(key)); + } else if (element instanceof Json5Object) { + ((Json5Object) element).mergeCommentTo((Json5Object) target.get(key)); + } + target.setComment(key, this.getComment(key)); + } } /** @@ -64,6 +106,7 @@ public void add(String property, Json5Element value) { * @param property name of the member that should be removed. * @return the {@link Json5Element} object that is being removed. */ + @SuppressWarnings("UnusedReturnValue") public Json5Element remove(String property) { return members.remove(property); } @@ -112,6 +155,41 @@ public void addProperty(String property, Character value) { add(property, value == null ? Json5Null.INSTANCE : new Json5String(value.toString())); } + // + + /** + * Sets the comment for a member. + * comment字段不会参与equals 和hashCode + * + * @param property name of the member + * @param comment comment for the member + */ + public void setComment(String property, String comment) { + Json5Element element = get(property); + if (element == null) { + throw new RuntimeException( + "Property " + property + " is not defined! You should define a property before set comment."); + } + if (element instanceof Json5Null) { + Json5Null value = new Json5Null(); + value.setComment(comment); + members.put(property, value); + } else { + element.setComment(comment); + } + } + + /** + * Gets the comment for a member. + * + * @param property name of the member + * @return comment for the member + */ + public String getComment(String property) { + return get(property).getComment(); + } + // + /** * Returns a set of members of this object. The set is ordered, and the order is in which the * elements were added. @@ -143,7 +221,7 @@ public int size() { /** * Convenience method to check if a member with the specified name is present in this object. * - * @param memberName name of the member that is being checked for presence. + * @param memberName the name of the member that is being checked for presence. * @return true if there is a member with the specified name, false otherwise. */ public boolean has(String memberName) { @@ -163,7 +241,7 @@ public Json5Element get(String memberName) { /** * Convenience method to get the specified member as a Json5Primitive element. * - * @param memberName name of the member being requested. + * @param memberName the name of the member being requested. * @return the Json5Primitive corresponding to the specified member. */ public Json5Primitive getAsJson5Primitive(String memberName) { @@ -173,7 +251,7 @@ public Json5Primitive getAsJson5Primitive(String memberName) { /** * Convenience method to get the specified member as a Json5Array. * - * @param memberName name of the member being requested. + * @param memberName the name of the member being requested. * @return the Json5Array corresponding to the specified member. */ public Json5Array getAsJson5Array(String memberName) { @@ -183,21 +261,33 @@ public Json5Array getAsJson5Array(String memberName) { /** * Convenience method to get the specified member as a Json5Object. * - * @param memberName name of the member being requested. + * @param memberName the name of the member being requested. * @return the Json5Object corresponding to the specified member. */ public Json5Object getAsJson5Object(String memberName) { return (Json5Object) members.get(memberName); } - @Override - public boolean equals(Object o) { - return (o == this) || (o instanceof Json5Object - && ((Json5Object) o).members.equals(members)); + /** + * 暂不支持直接转POJO对象,请使用toStandardString然后使用其它反序列化框架处理 + * @see Json5Element#toString(Json5Options) + * @see Json5Element#toStandardString + * + * @param clazz The class of the POJO to convert to. + * @param The type of the POJO. + * @return An instance of the specified POJO class, populated with data from this Json5Object. + */ + public T toPojo(Class clazz) throws Exception { + throw new Exception("不支持的操作!"); } @Override public int hashCode() { return members.hashCode(); } + + @Override + public boolean equals(Object o) { + return (o == this) || (o instanceof Json5Object && ((Json5Object) o).members.equals(members)); + } } \ No newline at end of file diff --git a/src/main/java/de/marhali/json5/Json5Options.java b/src/main/java/de/marhali/json5/Json5Options.java index c73704f..0ef8109 100644 --- a/src/main/java/de/marhali/json5/Json5Options.java +++ b/src/main/java/de/marhali/json5/Json5Options.java @@ -61,11 +61,43 @@ public static Json5OptionsBuilder builder() { */ private final int indentFactor; - public Json5Options(boolean allowInvalidSurrogates, boolean quoteSingle, boolean trailingComma, int indentFactor) { + // + /** + * Whether keys of {@link Json5Object} should be without quotes. + * This is unless starting or ending with a digit. + */ + private final boolean quoteless; + + /** + * parse时是否读取注释 + */ + private final boolean readComments; + + /** + * write时是否写注释 + */ + private final boolean writeComments; + + // + public Json5Options(boolean allowInvalidSurrogates, boolean quoteSingle, boolean trailingComma, int indentFactor, boolean quoteless, boolean readComments, + boolean writeComments) { this.allowInvalidSurrogates = allowInvalidSurrogates; this.quoteSingle = quoteSingle; this.trailingComma = trailingComma; this.indentFactor = Math.max(0, indentFactor); + this.quoteless = quoteless; + this.readComments = readComments; + this.writeComments = writeComments; + } + + public Json5Options(boolean allowInvalidSurrogates, boolean quoteSingle, boolean trailingComma, int indentFactor, boolean quoteless) { + this.allowInvalidSurrogates = allowInvalidSurrogates; + this.quoteSingle = quoteSingle; + this.trailingComma = trailingComma; + this.indentFactor = Math.max(0, indentFactor); + this.quoteless = quoteless; + this.readComments = false; + this.writeComments = true; } public boolean isAllowInvalidSurrogates() { @@ -76,6 +108,10 @@ public boolean isQuoteSingle() { return quoteSingle; } + public boolean isWriteComments() { + return writeComments; + } + public boolean isTrailingComma() { return trailingComma; } @@ -83,4 +119,14 @@ public boolean isTrailingComma() { public int getIndentFactor() { return indentFactor; } + + public boolean isReadComments() { + return readComments; + } + + // + public boolean isQuoteless() { + return quoteless; + } + // } \ No newline at end of file diff --git a/src/main/java/de/marhali/json5/Json5OptionsBuilder.java b/src/main/java/de/marhali/json5/Json5OptionsBuilder.java index 69dedf2..7534353 100644 --- a/src/main/java/de/marhali/json5/Json5OptionsBuilder.java +++ b/src/main/java/de/marhali/json5/Json5OptionsBuilder.java @@ -28,8 +28,16 @@ public class Json5OptionsBuilder { private boolean quoteSingle = false; private boolean trailingComma = false; + // + private boolean quoteless = false; + // + private int indentFactor = 0; + private boolean readComments = false; + + private boolean writeComments = true; + /** * Constructs a new builder instance. */ @@ -87,10 +95,42 @@ public Json5OptionsBuilder prettyPrinting() { return this; } + // + /** + * Configures to output {@link Json5Object} keys without quotes. This option only affects Json serialization. + * This option has no effect if the key starts or ends with a digit, or if the key contains non-alphanumeric characters. + * + * @return Current builder instance + */ + public Json5OptionsBuilder quoteless() { + this.quoteless = true; + return this; + } + // + + /** + * Configures to read comments from the source. This option only affects Json parsing. + * @return Current builder instance + */ + public Json5OptionsBuilder readComments() { + this.readComments = true; + return this; + } + + /** + * 设置json5Options序列化时不写注释 + */ + public Json5OptionsBuilder notWriteComments() { + this.writeComments = false; + return this; + } /** * @return Configured {@link Json5Options} */ public Json5Options build() { - return new Json5Options(allowInvalidSurrogates, quoteSingle, trailingComma, indentFactor); + // + return new Json5Options(allowInvalidSurrogates, quoteSingle, trailingComma, indentFactor, quoteless, + readComments,writeComments); + // } } diff --git a/src/main/java/de/marhali/json5/Json5Primitive.java b/src/main/java/de/marhali/json5/Json5Primitive.java index f442a53..c1219b9 100644 --- a/src/main/java/de/marhali/json5/Json5Primitive.java +++ b/src/main/java/de/marhali/json5/Json5Primitive.java @@ -33,9 +33,15 @@ * @author Joel Leitch */ public abstract class Json5Primitive extends Json5Element { + protected final Object value; + + public Json5Primitive(Object value) { + this.value = Objects.requireNonNull(value); + } /** * Quick creator for a primitive with boolean value. + * * @param value Boolean value to apply. * @return Corresponding primitive with provided value. */ @@ -45,6 +51,7 @@ public static Json5Primitive of(Boolean value) { /** * Quick creator for a primitive with number value. + * * @param value Number value to apply. * @return Corresponding primitive with provided value. */ @@ -55,6 +62,7 @@ public static Json5Primitive of(Number value) { /** * Quick creator for a primitive with string value. * Set hexadecimal to true to receive a {@link Json5Hexadecimal}. + * * @param value String value to apply. * @param hexadecimal Is the provided value a hex string literal? * @return Corresponding primitive with provided value. @@ -65,6 +73,7 @@ public static Json5Primitive of(String value, boolean hexadecimal) { /** * Quick creator for a primitive with string value. + * * @param value String value to apply. * @return Corresponding primitive with provided value. */ @@ -72,29 +81,6 @@ public static Json5Primitive of(String value) { return new Json5String(value); } - protected final Object value; - - public Json5Primitive(Object value) { - this.value = Objects.requireNonNull(value); - } - - /** - * Returns the same value as primitives are immutable. - */ - @Override - public Json5Element deepCopy() { - return this; - } - - /** - * Check whether this primitive contains a boolean value. - * - * @return true if this primitive contains a boolean value, false otherwise. - */ - public boolean isBoolean() { - return value instanceof Boolean; - } - /** * convenience method to get this element as a boolean value. * @@ -109,15 +95,6 @@ public boolean getAsBoolean() { return Boolean.parseBoolean(getAsString()); } - /** - * Check whether this primitive contains a Number. - * - * @return true if this primitive contains a Number, false otherwise. - */ - public boolean isNumber() { - return value instanceof Number; - } - /** * convenience method to get this element as a Number. * @@ -129,15 +106,6 @@ public Number getAsNumber() { return value instanceof String ? new LazilyParsedNumber((String) value) : (Number) value; } - /** - * Check whether this primitive contains a String value. - * - * @return true if this primitive contains a String value, false otherwise. - */ - public boolean isString() { - return value instanceof String; - } - /** * convenience method to get this element as a String. * @@ -166,48 +134,63 @@ public double getAsDouble() { } /** - * convenience method to get this element as a {@link BigDecimal}. + * convenience method to get this element as a float. * - * @return get this element as a {@link BigDecimal}. - * @throws NumberFormatException if the value contained is not a valid {@link BigDecimal}. + * @return get this element as a float. + * @throws NumberFormatException if the value contained is not a valid float. */ @Override - public BigDecimal getAsBigDecimal() { - return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString()); + public float getAsFloat() { + return isNumber() ? getAsNumber().floatValue() : Float.parseFloat(getAsString()); } /** - * convenience method to get this element as a {@link BigInteger}. + * convenience method to get this element as a primitive long. * - * @return get this element as a {@link BigInteger}. - * @throws NumberFormatException if the value contained is not a valid {@link BigInteger}. + * @return get this element as a primitive long. + * @throws NumberFormatException if the value contained is not a valid long. */ @Override - public BigInteger getAsBigInteger() { - return value instanceof BigInteger ? - (BigInteger) value : new BigInteger(value.toString()); + public long getAsLong() { + return isNumber() ? getAsNumber().longValue() : Long.parseLong(getAsString()); } /** - * convenience method to get this element as a float. + * convenience method to get this element as a primitive integer. * - * @return get this element as a float. - * @throws NumberFormatException if the value contained is not a valid float. + * @return get this element as a primitive integer. + * @throws NumberFormatException if the value contained is not a valid integer. */ @Override - public float getAsFloat() { - return isNumber() ? getAsNumber().floatValue() : Float.parseFloat(getAsString()); + public int getAsInt() { + return isNumber() ? getAsNumber().intValue() : Integer.parseInt(getAsString()); + } + + @Override + public byte getAsByte() { + return isNumber() ? getAsNumber().byteValue() : Byte.parseByte(getAsString()); } /** - * convenience method to get this element as a primitive long. + * convenience method to get this element as a {@link BigDecimal}. * - * @return get this element as a primitive long. - * @throws NumberFormatException if the value contained is not a valid long. + * @return get this element as a {@link BigDecimal}. + * @throws NumberFormatException if the value contained is not a valid {@link BigDecimal}. */ @Override - public long getAsLong() { - return isNumber() ? getAsNumber().longValue() : Long.parseLong(getAsString()); + public BigDecimal getAsBigDecimal() { + return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString()); + } + + /** + * convenience method to get this element as a {@link BigInteger}. + * + * @return get this element as a {@link BigInteger}. + * @throws NumberFormatException if the value contained is not a valid {@link BigInteger}. + */ + @Override + public BigInteger getAsBigInteger() { + return value instanceof BigInteger ? (BigInteger) value : new BigInteger(value.toString()); } /** @@ -222,19 +205,30 @@ public short getAsShort() { } /** - * convenience method to get this element as a primitive integer. + * Check whether this primitive contains a boolean value. * - * @return get this element as a primitive integer. - * @throws NumberFormatException if the value contained is not a valid integer. + * @return true if this primitive contains a boolean value, false otherwise. */ - @Override - public int getAsInt() { - return isNumber() ? getAsNumber().intValue() : Integer.parseInt(getAsString()); + public boolean isBoolean() { + return value instanceof Boolean; } - @Override - public byte getAsByte() { - return isNumber() ? getAsNumber().byteValue() : Byte.parseByte(getAsString()); + /** + * Check whether this primitive contains a Number. + * + * @return true if this primitive contains a Number, false otherwise. + */ + public boolean isNumber() { + return value instanceof Number; + } + + /** + * Check whether this primitive contains a String value. + * + * @return true if this primitive contains a String value, false otherwise. + */ + public boolean isString() { + return value instanceof String; } @Override @@ -242,7 +236,7 @@ public int hashCode() { if (value == null) { return 31; } - // Using recommended hashing algorithm from Effective Java for longs and doubles + // Using the recommended hashing algorithm from Effective Java for longs and doubles if (isIntegral(this)) { long value = getAsNumber().longValue(); return (int) (value ^ (value >>> 32)); @@ -262,7 +256,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - Json5Primitive other = (Json5Primitive)obj; + Json5Primitive other = (Json5Primitive) obj; if (value == null) { return other.value == null; } @@ -287,7 +281,7 @@ private static boolean isIntegral(Json5Primitive primitive) { if (primitive.value instanceof Number) { Number number = (Number) primitive.value; return number instanceof BigInteger || number instanceof Long || number instanceof Integer - || number instanceof Short || number instanceof Byte; + || number instanceof Short || number instanceof Byte; } return false; } diff --git a/src/main/java/de/marhali/json5/Json5String.java b/src/main/java/de/marhali/json5/Json5String.java index d774bd3..b3a0b3c 100644 --- a/src/main/java/de/marhali/json5/Json5String.java +++ b/src/main/java/de/marhali/json5/Json5String.java @@ -25,4 +25,21 @@ public final class Json5String extends Json5Primitive { public Json5String(String string) { super(string); } + + @Override + public Json5Element deepCopy() { + Json5String o = new Json5String((String) value); + o.setComment(getComment()); + return o; + } + + @Override + public Json5Element noCommentCopy() { + return new Json5String((String) value); + } + + @Override + public boolean isEmpty() { + return ((String) this.value).isEmpty(); + } } diff --git a/src/main/java/de/marhali/json5/stream/Json5Lexer.java b/src/main/java/de/marhali/json5/stream/Json5Lexer.java index 98da822..1832ce3 100644 --- a/src/main/java/de/marhali/json5/stream/Json5Lexer.java +++ b/src/main/java/de/marhali/json5/stream/Json5Lexer.java @@ -25,15 +25,25 @@ package de.marhali.json5.stream; -import de.marhali.json5.*; +import de.marhali.json5.Json5Array; +import de.marhali.json5.Json5Boolean; +import de.marhali.json5.Json5Element; +import de.marhali.json5.Json5Hexadecimal; +import de.marhali.json5.Json5Null; +import de.marhali.json5.Json5Number; +import de.marhali.json5.Json5Object; +import de.marhali.json5.Json5Options; +import de.marhali.json5.Json5String; import de.marhali.json5.exception.Json5Exception; import java.io.BufferedReader; import java.io.Reader; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Arrays; import java.util.Objects; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * This is a lexer to convert the provided data into tokens according to the json5 specification. @@ -44,26 +54,21 @@ * @author SyntaxError404 * @see Json5 Standard. */ +@SuppressWarnings("HardcodedLineSeparator") public class Json5Lexer { - - private static final Pattern PATTERN_BOOLEAN = Pattern.compile( - "true|false" - ); + private static final Pattern PATTERN_BOOLEAN = Pattern.compile("true|false"); private static final Pattern PATTERN_NUMBER_FLOAT = Pattern.compile( - "[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?" - ); - private static final Pattern PATTERN_NUMBER_INTEGER = Pattern.compile( - "[+-]?(0|[1-9]\\d*)" - ); - private static final Pattern PATTERN_NUMBER_HEX = Pattern.compile( - "[+-]?0[xX][0-9a-fA-F]+" - ); - private static final Pattern PATTERN_NUMBER_SPECIAL = Pattern.compile( - "[+-]?(Infinity|NaN)" - ); + "[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?"); + + private static final Pattern PATTERN_NUMBER_INTEGER = Pattern.compile("[+-]?(0|[1-9]\\d*)"); + + private static final Pattern PATTERN_NUMBER_HEX = Pattern.compile("[+-]?0[xX][0-9a-fA-F]+"); + + private static final Pattern PATTERN_NUMBER_SPECIAL = Pattern.compile("[+-]?(Infinity|NaN)"); private final Reader reader; + private final Json5Options options; /** @@ -80,10 +85,12 @@ public class Json5Lexer { * the absolute position in the string */ private long index; + /** * the relative position in the line */ private long character; + /** * the line number */ @@ -93,15 +100,19 @@ public class Json5Lexer { * the previous character */ private char previous; + /** * the current character */ private char current; + private StringBuilder lastComment; + /** * Constructs a new lexer from a specific {@link Reader}. *

Note: The reader must be closed after operation ({@link Reader#close()})!

- * @param reader a reader. + * + * @param reader a reader. * @param options the options for lexing. */ public Json5Lexer(Reader reader, Json5Options options) { @@ -117,13 +128,22 @@ public Json5Lexer(Reader reader, Json5Options options) { previous = 0; current = 0; + lastComment = null; } - private boolean more() { - if (back || eof) - return back && !eof; - - return peek() > 0; + /** + * Returns the last comment that was read and clears it. + * The parser will call this before parsing a new element. + * + * @return The captured comment content, or null if no comment was found. + */ + public String consumeComment() { + if (this.lastComment == null) { + return null; + } + String comment = this.lastComment.toString(); + this.lastComment = null; + return comment; } /** @@ -133,9 +153,179 @@ public void back() { back = true; } + /** + * Reads until encountering a character that is not a whitespace according to the + * JSON5 Specification + * + * @return a non-whitespace character, or {@code 0} if the end of the stream has been reached + */ + public char nextClean() { + while (true) { + if (!more()) { + if (index == -1) { // Empty stream + return 0; + } + throw syntaxError("Unexpected end of data"); + } + + char n = next(); + + if (n == '/') { + char p = peek(); + + if (p == '*') { + // 跳过peek的字符 + next(); + nextMultiLineComment(); + } else if (p == '/') { + // 跳过peek的字符 + next(); + // 再跳过//后紧邻的第一个空格 + if (peek() == ' ') next(); + nextSingleLineComment(); + } else { return n; } + } else if (!isWhitespace(n)) { return n; } + } + } + + /** + * Reads a member name from the source according to the + * JSON5 Specification + * + * @return an member name + */ + public String nextMemberName() { + StringBuilder result = new StringBuilder(); + + char prev; + char n = next(); + + if (n == '"' || n == '\'') { return nextString(n); } + + back(); + n = 0; + + while (true) { + if (!more()) { throw syntaxError("Unexpected end of data"); } + + boolean part = result.length() > 0; + + prev = n; + n = next(); + + if (n == '\\') { // unicode escape sequence + n = next(); + + if (n != 'u') { throw syntaxError("Illegal escape sequence '\\" + n + "' in key"); } + + n = unicodeEscape(true, part); + } else if (!isMemberNameChar(n, part)) { + back(); + break; + } + + checkSurrogate(prev, n); + + result.append(n); + } + + if (result.length() == 0) { throw syntaxError("Empty key"); } + + return result.toString(); + } + + /** + * Reads a value from the source according to the + * JSON5 Specification + * + * @return an member name + */ + public Json5Element nextValue() { + char n = nextClean(); + + switch (n) { + case '"': + case '\'': + String string = nextString(n); + return new Json5String(string); + case '{': + back(); + return Json5Parser.parseObject(this); + case '[': + back(); + return Json5Parser.parseArray(this); + } + + back(); + + String string = nextCleanTo(",]}"); + + if (string.equals("null")) { return Json5Null.INSTANCE; } + + if (PATTERN_BOOLEAN.matcher(string).matches()) { return new Json5Boolean(string.equals("true")); } + + if (PATTERN_NUMBER_INTEGER.matcher(string).matches()) { + BigInteger bigint = new BigInteger(string); + return new Json5Number(bigint); + } + + if (PATTERN_NUMBER_FLOAT.matcher(string).matches()) { return new Json5Number(new BigDecimal(string)); } + + if (PATTERN_NUMBER_SPECIAL.matcher(string).matches()) { + String special; + + int factor; + double d = 0; + + switch (string.charAt(0)) { // +, -, or 0 + case '+': + special = string.substring(1); // + + factor = 1; + break; + + case '-': + special = string.substring(1); // - + factor = -1; + break; + + default: + special = string; + factor = 1; + break; + } + + switch (special) { + case "NaN": + d = Double.NaN; + break; + case "Infinity": + d = Double.POSITIVE_INFINITY; + break; + } + + return new Json5Number(factor * d); + } + + if (PATTERN_NUMBER_HEX.matcher(string).matches()) { + return new Json5Hexadecimal(string); + } + + throw new Json5Exception("Illegal value '" + string + "'"); + } + + @Override + public String toString() { + return " at index " + index + " [character " + character + " in line " + line + "]"; + } + + private boolean more() { + if (back || eof) { return back && !eof; } + + return peek() > 0; + } + private char peek() { - if (eof) - return 0; + if (eof) { return 0; } int c; @@ -179,7 +369,7 @@ private char next() { if (isLineTerminator(current) && (current != '\n' || (current == '\n' && previous != '\r'))) { line++; character = 0; - } else character++; + } else { character++; } return current; } @@ -213,8 +403,7 @@ private boolean isWhitespace(char c) { return true; default: // Unicode category "Zs" (space separators) - if (Character.getType(c) == Character.SPACE_SEPARATOR) - return true; + if (Character.getType(c) == Character.SPACE_SEPARATOR) { return true; } return false; } @@ -226,54 +415,68 @@ private boolean isDecimalDigit(char c) { } private void nextMultiLineComment() { + if (!options.isReadComments()) { + while (true) { + char n = next(); + if (n == 0) { + throw syntaxError("Unterminated multi-line comment"); + } + if (n == '*' && peek() == '/') { + next(); + return; + } + } + } + + if (this.lastComment == null) { + this.lastComment = new StringBuilder(); + } else if (!this.lastComment.isEmpty()) { + this.lastComment.append("\n"); + } + + this.lastComment.append("/*"); while (true) { char n = next(); - + if (n == 0) { + throw syntaxError("Unterminated multi-line comment"); + } + this.lastComment.append(n); if (n == '*' && peek() == '/') { - next(); - return; + this.lastComment.append(next()); + break; } } + + String comment = this.lastComment.toString(); + String[] lines = comment.split("\r?\n"); + String normalizedComment = Arrays.stream(lines).skip(1).map(String::strip).map(l -> " " + l).collect( + Collectors.joining("\n")); + normalizedComment = lines[0] +"\n" + normalizedComment; + this.lastComment = new StringBuilder(normalizedComment); } private void nextSingleLineComment() { - while (true) { - char n = next(); + if (!options.isReadComments()) { + while (true) { + char n = next(); + if (isLineTerminator(n) || n == 0) { + return; + } + } + } - if (isLineTerminator(n) || n == 0) - return; + if (this.lastComment == null) { + this.lastComment = new StringBuilder(); + } else if (!this.lastComment.isEmpty()) { + this.lastComment.append("\n"); } - } - /** - * Reads until encountering a character that is not a whitespace according to the - * JSON5 Specification - * - * @return a non-whitespace character, or {@code 0} if the end of the stream has been reached - */ - public char nextClean() { while (true) { - if (!more()) { - if(index == -1) { // Empty stream - return 0; - } - throw syntaxError("Unexpected end of data"); - } - char n = next(); - - if (n == '/') { - char p = peek(); - - if (p == '*') { - next(); - nextMultiLineComment(); - } else if (p == '/') { - next(); - nextSingleLineComment(); - } else return n; - } else if (!isWhitespace(n)) - return n; + if (isLineTerminator(n) || n == 0) { + return; + } + this.lastComment.append(n); } } @@ -281,8 +484,7 @@ private String nextCleanTo(String delimiters) { StringBuilder result = new StringBuilder(); while (true) { - if (!more()) - throw syntaxError("Unexpected end of data"); + if (!more()) { throw syntaxError("Unexpected end of data"); } char n = nextClean(); @@ -298,14 +500,11 @@ private String nextCleanTo(String delimiters) { } private int dehex(char c) { - if (c >= '0' && c <= '9') - return c - '0'; + if (c >= '0' && c <= '9') { return c - '0'; } - if (c >= 'a' && c <= 'f') - return c - 'a' + 0xA; + if (c >= 'a' && c <= 'f') { return c - 'a' + 0xA; } - if (c >= 'A' && c <= 'F') - return c - 'A' + 0xA; + if (c >= 'A' && c <= 'F') { return c - 'A' + 0xA; } return -1; } @@ -322,30 +521,26 @@ private char unicodeEscape(boolean member, boolean part) { int hex = dehex(n); - if (hex == -1) - throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in " + where); + if (hex == -1) { throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in " + where); } codepoint |= hex << ((3 - i) << 2); } - if (member && !isMemberNameChar((char) codepoint, part)) + if (member && !isMemberNameChar((char) codepoint, part)) { throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in key"); + } return (char) codepoint; } private void checkSurrogate(char hi, char lo) { - if (options.isAllowInvalidSurrogates()) - return; + if (options.isAllowInvalidSurrogates()) { return; } - if (!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo)) - return; + if (!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo)) { return; } - if (!Character.isSurrogatePair(hi, lo)) - throw syntaxError(String.format( - "Invalid surrogate pair: U+%04X and U+%04X", - hi, lo - )); + if (!Character.isSurrogatePair(hi, lo)) { + throw syntaxError(String.format("Invalid surrogate pair: U+%04X and U+%04X", hi, lo)); + } } // https://spec.json5.org/#prod-JSON5String @@ -359,89 +554,88 @@ private String nextString(char quote) { char prev; while (true) { - if (!more()) - throw syntaxError("Unexpected end of data"); + if (!more()) { throw syntaxError("Unexpected end of data"); } prev = n; n = next(); - if (n == quote) - break; + if (n == quote) { break; } - if (isLineTerminator(n) && n != 0x2028 && n != 0x2029) + if (isLineTerminator(n) && n != 0x2028 && n != 0x2029) { throw syntaxError("Unescaped line terminator in string"); + } if (n == '\\') { n = next(); if (isLineTerminator(n)) { - if (n == '\r' && peek() == '\n') - next(); + if (n == '\r' && peek() == '\n') { next(); } // escaped line terminator/ line continuation continue; - } else switch (n) { - case '\'': - case '"': - case '\\': - result.append(n); - continue; - case 'b': - result.append('\b'); - continue; - case 'f': - result.append('\f'); - continue; - case 'n': - result.append('\n'); - continue; - case 'r': - result.append('\r'); - continue; - case 't': - result.append('\t'); - continue; - case 'v': // Vertical Tab - result.append((char) 0x0B); - continue; - - case '0': // NUL - char p = peek(); - - if (isDecimalDigit(p)) - throw syntaxError("Illegal escape sequence '\\0" + p + "'"); - - result.append((char) 0); - continue; - - case 'x': // Hex escape sequence - value = ""; - codepoint = 0; - - for (int i = 0; i < 2; ++i) { - n = next(); - value += n; - - int hex = dehex(n); - - if (hex == -1) - throw syntaxError("Illegal hex escape sequence '\\x" + value + "' in string"); - - codepoint |= hex << ((1 - i) << 2); - } - - n = (char) codepoint; - break; - - case 'u': // Unicode escape sequence - n = unicodeEscape(false, false); - break; - - default: - if (isDecimalDigit(n)) - throw syntaxError("Illegal escape sequence '\\" + n + "'"); - - break; + } else { + switch (n) { + case '\'': + case '"': + case '\\': + result.append(n); + continue; + case 'b': + result.append('\b'); + continue; + case 'f': + result.append('\f'); + continue; + case 'n': + result.append('\n'); + continue; + case 'r': + result.append('\r'); + continue; + case 't': + result.append('\t'); + continue; + case 'v': // Vertical Tab + result.append((char) 0x0B); + continue; + + case '0': // NUL + char p = peek(); + + if (isDecimalDigit(p)) { throw syntaxError("Illegal escape sequence '\0" + p + "'"); } + + result.append((char) 0); + continue; + + case 'x': // Hex escape sequence + value = ""; + codepoint = 0; + + for (int i = 0; i < 2; ++i) { + n = next(); + value += n; + + int hex = dehex(n); + + if (hex == -1) { + throw syntaxError("Illegal hex escape sequence '\\x" + value + "' in string"); + } + + codepoint |= hex << ((1 - i) << 2); + } + + n = (char) codepoint; + break; + + case 'u': // Unicode escape sequence + n = unicodeEscape(false, false); + break; + + default: + if (isDecimalDigit(n)) { throw syntaxError("Illegal escape sequence '\\" + n + "'"); } + + break; + } } } @@ -454,8 +648,7 @@ private String nextString(char quote) { } private boolean isMemberNameChar(char n, boolean part) { - if (n == '$' || n == '_' || n == 0x200C || n == 0x200D) - return true; + if (n == '$' || n == '_' || n == 0x200C || n == 0x200D) { return true; } int type = Character.getType(n); @@ -472,151 +665,18 @@ private boolean isMemberNameChar(char n, boolean part) { case Character.COMBINING_SPACING_MARK: case Character.DECIMAL_DIGIT_NUMBER: case Character.CONNECTOR_PUNCTUATION: - if (part) - return true; + if (part) { return true; } break; } return false; } - /** - * Reads a member name from the source according to the - * JSON5 Specification - * - * @return an member name - */ - public String nextMemberName() { - StringBuilder result = new StringBuilder(); - - char prev; - char n = next(); - - if (n == '"' || n == '\'') - return nextString(n); - - back(); - n = 0; - - while (true) { - if (!more()) - throw syntaxError("Unexpected end of data"); - - boolean part = result.length() > 0; - - prev = n; - n = next(); - - if (n == '\\') { // unicode escape sequence - n = next(); - - if (n != 'u') - throw syntaxError("Illegal escape sequence '\\" + n + "' in key"); - - n = unicodeEscape(true, part); - } else if (!isMemberNameChar(n, part)) { - back(); - break; - } - - checkSurrogate(prev, n); - - result.append(n); - } - - if (result.length() == 0) - throw syntaxError("Empty key"); - - return result.toString(); - } - - /** - * Reads a value from the source according to the - * JSON5 Specification - * - * @return an member name - */ - public Json5Element nextValue() { - char n = nextClean(); - - switch (n) { - case '"': - case '\'': - String string = nextString(n); - return new Json5String(string); - case '{': - back(); - return Json5Parser.parseObject(this); - case '[': - back(); - return Json5Parser.parseArray(this); - } - - back(); - - String string = nextCleanTo(",]}"); - - if (string.equals("null")) - return Json5Null.INSTANCE; - - if (PATTERN_BOOLEAN.matcher(string).matches()) - return new Json5Boolean(string.equals("true")); - - if (PATTERN_NUMBER_INTEGER.matcher(string).matches()) { - BigInteger bigint = new BigInteger(string); - return new Json5Number(bigint); - } - - if (PATTERN_NUMBER_FLOAT.matcher(string).matches()) - return new Json5Number(new BigDecimal(string)); - - if (PATTERN_NUMBER_SPECIAL.matcher(string).matches()) { - String special; - - int factor; - double d = 0; - - switch (string.charAt(0)) { // +, -, or 0 - case '+': - special = string.substring(1); // + - factor = 1; - break; - - case '-': - special = string.substring(1); // - - factor = -1; - break; - - default: - special = string; - factor = 1; - break; - } - - switch (special) { - case "NaN": - d = Double.NaN; - break; - case "Infinity": - d = Double.POSITIVE_INFINITY; - break; - } - - return new Json5Number(factor * d); - } - - if (PATTERN_NUMBER_HEX.matcher(string).matches()) { - return new Json5Hexadecimal(string); - } - - throw new Json5Exception("Illegal value '" + string + "'"); - } - /** * Constructs a new JSONException with a detail message and a causing exception * * @param message the detail message - * @param cause the causing exception + * @param cause the causing exception * @return a JSONException */ protected Json5Exception syntaxError(String message, Throwable cause) { @@ -632,9 +692,4 @@ protected Json5Exception syntaxError(String message, Throwable cause) { protected Json5Exception syntaxError(String message) { return new Json5Exception(message + this); } - - @Override - public String toString() { - return " at index " + index + " [character " + character + " in line " + line + "]"; - } } diff --git a/src/main/java/de/marhali/json5/stream/Json5Parser.java b/src/main/java/de/marhali/json5/stream/Json5Parser.java index c7422ca..9ba1115 100644 --- a/src/main/java/de/marhali/json5/stream/Json5Parser.java +++ b/src/main/java/de/marhali/json5/stream/Json5Parser.java @@ -51,18 +51,31 @@ private Json5Parser() {} public static Json5Element parse(Json5Lexer lexer) { Objects.requireNonNull(lexer); - switch (lexer.nextClean()) { + char c = lexer.nextClean(); + String comment = lexer.consumeComment(); + + Json5Element element; + + switch (c) { case '{': lexer.back(); - return parseObject(lexer); + element = parseObject(lexer); + break; case '[': lexer.back(); - return parseArray(lexer); + element = parseArray(lexer); + break; case 0: return null; default: throw lexer.syntaxError("Unknown or unexpected control character"); } + + if (comment != null) { + element.setComment(comment); + } + + return element; } /** @@ -86,10 +99,14 @@ public static Json5Object parseObject(Json5Lexer lexer) { while (true) { control = lexer.nextClean(); + String comment = lexer.consumeComment(); switch (control) { case 0: throw lexer.syntaxError("A json object must end with '}'"); case '}': + if (comment != null) { + object.setComment(comment); + } return object; default: lexer.back(); @@ -104,7 +121,12 @@ public static Json5Object parseObject(Json5Lexer lexer) { throw lexer.syntaxError("Expected ':' after a key, got '" + control + "' instead"); } - object.add(key, lexer.nextValue()); + Json5Element value = lexer.nextValue(); + object.add(key, value); + if (comment != null) { + object.setComment(key, comment); + value.setComment(comment); + } control = lexer.nextClean(); if(control == '}') { @@ -136,16 +158,24 @@ public static Json5Array parseArray(Json5Lexer lexer) { while(true) { control = lexer.nextClean(); + String comment = lexer.consumeComment(); switch (control) { case 0: throw lexer.syntaxError("A json array must end with ']'"); case ']': + if (comment != null) { + array.setComment(comment); + } return array; default: lexer.back(); } - array.add(lexer.nextValue()); + Json5Element value = lexer.nextValue(); + if (comment != null) { + value.setComment(comment); + } + array.add(value); control = lexer.nextClean(); if(control == ']') { diff --git a/src/main/java/de/marhali/json5/stream/Json5Writer.java b/src/main/java/de/marhali/json5/stream/Json5Writer.java index 7f2a2ae..c0cfe34 100644 --- a/src/main/java/de/marhali/json5/stream/Json5Writer.java +++ b/src/main/java/de/marhali/json5/stream/Json5Writer.java @@ -3,6 +3,7 @@ * * Copyright (C) 2021 SyntaxError404 * Copyright (C) 2022 Marcel Haßlinger + * Copyright (C) 2024 Ultreon Team * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,7 +26,12 @@ package de.marhali.json5.stream; -import de.marhali.json5.*; +import de.marhali.json5.Json5Array; +import de.marhali.json5.Json5Element; +import de.marhali.json5.Json5Object; +import de.marhali.json5.Json5Options; +import de.marhali.json5.Json5Primitive; +import de.marhali.json5.Json5String; import java.io.IOException; import java.io.Writer; @@ -39,14 +45,16 @@ * @author Marcel Haßlinger * @author SyntaxError404 */ +@SuppressWarnings("HardcodedLineSeparator") public final class Json5Writer { - private final Json5Options options; + private final Writer writer; /** * Creates a new instance that writes a JSON5-encoded stream to {@code writer}. *

Note: The writer must be closed after operation ({@link Writer#close()})!

+ * * @param options Parsing and serialization options * @param writer Output stream. For best performance, use a {@link java.io.BufferedWriter} */ @@ -58,44 +66,50 @@ public Json5Writer(Json5Options options, Writer writer) { /** * Encodes and writes the provided {@link Json5Element} into json5 according to the specification * and the configured options. The element can be any json5 element. All child trees will be included. + * * @param element Element to encode * @throws IOException If an I/O error occurs * @see #Json5Writer(Json5Options, Writer) Configuration options - * @see #write(Json5Element, String) + * @see #write(Json5Element, String, boolean) */ public void write(Json5Element element) throws IOException { - write(element, ""); + write(element, "", true); } /** * Encodes and writes the provided {@link Json5Element} into json5 according to the specification * and the configured options. The element can be any json5 element. All child trees will be included. + * * @param element Element to encode * @param indent Indent to apply (for nested elements) * @throws IOException If an I/O error occurs * @see #Json5Writer(Json5Options, Writer) Configuration options * @see #write(Json5Element) without indent */ - public void write(Json5Element element, String indent) throws IOException { + public void write(Json5Element element, String indent, boolean writeComment) throws IOException { Objects.requireNonNull(element); Objects.requireNonNull(indent); - - if(element.isJson5Null()) { + if (writeComment && options.isWriteComments() && element.hasComment()) { + String comment = element.getComment(); + writeElementComment(comment, indent); + } + if (element.isJson5Null()) { writeNull(); - } else if(element.isJson5Object()) { + } else if (element.isJson5Object()) { writeObject(element.getAsJson5Object(), indent); - } else if(element.isJson5Array()) { + } else if (element.isJson5Array()) { writeArray(element.getAsJson5Array(), indent); - } else if(element.isJson5Primitive()) { + } else if (element.isJson5Primitive()) { writePrimitive(element.getAsJson5Primitive()); } else { - throw new UnsupportedOperationException("Unknown json element with type class " - + element.getClass().getName()); + throw new UnsupportedOperationException( + "Unknown json element with type class " + element.getClass().getName()); } } /** * Writes the equivalent of a {@link de.marhali.json5.Json5Null}({@code null}) value. + * * @throws IOException If an I/O error occurs. */ public void writeNull() throws IOException { @@ -104,13 +118,14 @@ public void writeNull() throws IOException { /** * Writes the provided primitive to the stream and encodes it if necessary. + * * @param primitive Primitive value. * @throws IOException If an I/O error occurs. */ public void writePrimitive(Json5Primitive primitive) throws IOException { Objects.requireNonNull(primitive); - if(primitive instanceof Json5String) { + if (primitive instanceof Json5String) { writer.append(quote(primitive.getAsString())); } else { writer.append(primitive.getAsString()); @@ -119,6 +134,7 @@ public void writePrimitive(Json5Primitive primitive) throws IOException { /** * Writes the provided {@link Json5Object} to the stream. + * * @param object Object to encode * @param indent Indent to apply (for nested elements) * @throws IOException If an I/O error occurs. @@ -133,27 +149,38 @@ public void writeObject(Json5Object object, String indent) throws IOException { writer.write("{"); int index = 0; - for(Map.Entry entry : object.entrySet()) { - if(options.getIndentFactor() > 0) { + for (Map.Entry entry : object.entrySet()) { + if (options.getIndentFactor() > 0) { writer.append('\n').append(childIndent); } - writer.append(quote(entry.getKey())).append(":"); + Json5Element element = entry.getValue(); + if (options.isWriteComments() && element.hasComment()) { + writeElementComment(element.getComment(), childIndent); + } + + // + if (options.isQuoteless() && entry.getKey().matches("^[a-zA-Z_][a-zA-Z0-9_]*[a-zA-Z_]$")) { + writer.append(entry.getKey()).append(":"); + } else { + writer.append(quote(entry.getKey())).append(":"); + } + // - if(options.getIndentFactor() > 0) { + if (options.getIndentFactor() > 0) { writer.append(' '); } - write(entry.getValue(), childIndent); + write(element, childIndent, false); - if(options.isTrailingComma() || index < object.size() - 1) { + if (options.isTrailingComma() || index < object.size() - 1) { writer.append(','); } index++; } - if(options.getIndentFactor() > 0 && object.size() > 0) { + if (options.getIndentFactor() > 0 && object.size() > 0) { writer.append('\n').append(indent); } @@ -162,6 +189,7 @@ public void writeObject(Json5Object object, String indent) throws IOException { /** * Writes the provided {@link Json5Array} to the stream. + * * @param array Array to encode * @param indent Indent to apply (for nested elements) * @throws IOException If an I/O error occurs. @@ -175,21 +203,21 @@ public void writeArray(Json5Array array, String indent) throws IOException { writer.write('['); - for(int i = 0; i < array.size(); i++) { + for (int i = 0; i < array.size(); i++) { Json5Element currentElement = array.get(i); - if(options.getIndentFactor() > 0) { + if (options.getIndentFactor() > 0) { writer.append('\n').append(childIndent); } - write(currentElement, childIndent); + write(currentElement, childIndent, options.isWriteComments()); - if(options.isTrailingComma() || i < array.size() - 1) { + if (options.isTrailingComma() || i < array.size() - 1) { writer.append(','); } } - if(options.getIndentFactor() > 0 && !array.isEmpty()) { + if (options.getIndentFactor() > 0 && !array.isEmpty()) { writer.append('\n').append(indent); } @@ -198,27 +226,28 @@ public void writeArray(Json5Array array, String indent) throws IOException { /** * Quotes the provided string according to the json5 specification. + * * @param string String to quote * @return Quoted string */ public String quote(String string) { final char qt = options.isQuoteSingle() ? '\'' : '"'; - if(string == null || string.isEmpty()) { + if (string == null || string.isEmpty()) { return String.valueOf(qt).repeat(2); } StringBuilder quoted = new StringBuilder(string.length() + 2); quoted.append(qt); - for(char c : string.toCharArray()) { - if(c == qt) { + for (char c : string.toCharArray()) { + if (c == qt) { quoted.append('\\'); quoted.append(c); continue; } - switch(c) { + switch (c) { case '\\': quoted.append("\\\\"); break; @@ -242,7 +271,7 @@ public String quote(String string) { break; default: // escape non-graphical characters (https://www.unicode.org/versions/Unicode13.0.0/ch02.pdf#G286941) - switch(Character.getType(c)) { + switch (Character.getType(c)) { case Character.FORMAT: case Character.LINE_SEPARATOR: case Character.PARAGRAPH_SEPARATOR: @@ -251,7 +280,7 @@ public String quote(String string) { case Character.SURROGATE: case Character.UNASSIGNED: quoted.append("\\u"); - quoted.append(String.format("%04X", c)); + quoted.append(String.format("%04X", (int) c)); break; default: quoted.append(c); @@ -263,4 +292,17 @@ public String quote(String string) { quoted.append(qt); return quoted.toString(); } + + private void writeElementComment(String comment, String indent) throws IOException { + String[] lines = comment.split("\n"); + if (comment.startsWith("/*")) { + for (String line : lines) { + writer.append(line).append("\n").append(indent); + } + } else { + for (String line : lines) { + writer.append("// ").append(line).append('\n').append(indent); + } + } + } } \ No newline at end of file diff --git a/src/test/java/de/marhali/json5/TestJson5.java b/src/test/java/de/marhali/json5/TestJson5.java index 5caa3dd..0545abe 100644 --- a/src/test/java/de/marhali/json5/TestJson5.java +++ b/src/test/java/de/marhali/json5/TestJson5.java @@ -16,13 +16,20 @@ package de.marhali.json5; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.*; +import java.util.Optional; /** * Unit tests for the {@link Json5} core class. @@ -30,7 +37,6 @@ * @author Marcel Haßlinger */ public class TestJson5 { - private Json5 json5; private InputStream getTestResource(String fileName) { @@ -45,14 +51,14 @@ private String getTestResourceContent(String fileName) throws IOException { buf.write((byte) result); } - return buf.toString(StandardCharsets.UTF_8); + return Optional.ofNullable(buf.toString(StandardCharsets.UTF_8)).map(s -> s.replace("\r\n", "\n")).map( + s -> s.replace("\r", "\n")).orElse(null); } } @BeforeEach void setup() { - json5 = Json5.builder(builder -> - builder.allowInvalidSurrogate().quoteSingle().indentFactor(2).build()); + json5 = Json5.builder(builder -> builder.allowInvalidSurrogate().quoteSingle().indentFactor(2).build()); } @Test @@ -71,7 +77,7 @@ void serializeString() throws IOException { Json5Object element = new Json5Object(); element.addProperty("key", "value"); element.addProperty("bool", true); - element.add("hex", new Json5Hexadecimal("0x100")); + element.add("hex", new Json5Hexadecimal("0x100")); String jsonString = json5.serialize(element); String expect = "{\n 'key': 'value',\n 'bool': true,\n 'hex': 0x100\n}"; @@ -81,7 +87,7 @@ void serializeString() throws IOException { @Test void ioArrayFile() throws IOException { - try(InputStream stream = getTestResource("test.array.json5")) { + try (InputStream stream = getTestResource("test.array.json5")) { Json5Element element = json5.parse(stream); assertTrue(element.isJson5Array()); assertEquals(getTestResourceContent("expect.array.json5"), json5.serialize(element)); @@ -90,10 +96,47 @@ void ioArrayFile() throws IOException { @Test void ioObjectFile() throws IOException { - try(InputStream stream = getTestResource("test.object.json5")) { + try (InputStream stream = getTestResource("test.object.json5")) { Json5Element element = json5.parse(stream); assertTrue(element.isJson5Object()); assertEquals(getTestResourceContent("expect.object.json5"), json5.serialize(element)); } } + + @Test + void testIsEmptyBehavior() { + // Json5Null + Json5Element nullValue = new Json5Null(); + assertTrue(nullValue.isEmpty(), "Json5Null 应该被视为空"); + + // 空 Json5Object + Json5Object emptyObj = new Json5Object(); + assertTrue(emptyObj.isEmpty(), "空 Json5Object 应该被视为空"); + + // 非空 Json5Object + Json5Object nonEmptyObj = new Json5Object(); + nonEmptyObj.add("key", new Json5String("value")); + assertFalse(nonEmptyObj.isEmpty(), "非空 Json5Object 不应被视为空"); + + // 空 Json5Array + Json5Array emptyArr = new Json5Array(); + assertTrue(emptyArr.isEmpty(), "空 Json5Array 应该被视为空"); + + // 非空 Json5Array + Json5Array nonEmptyArr = new Json5Array(); + nonEmptyArr.add(new Json5String("item")); + assertFalse(nonEmptyArr.isEmpty(), "非空 Json5Array 不应被视为空"); + + // 空 Json5String + Json5String emptyStr = new Json5String(""); + assertTrue(emptyStr.isEmpty(), "空字符串应该被视为空"); + + // 非空 Json5String + Json5String nonEmptyStr = new Json5String("hello"); + assertFalse(nonEmptyStr.isEmpty(), "非空字符串不应被视为空"); + + // 其它类型(例如 Json5Number) + Json5Element number = new Json5Number(42); // 假设你有这个类型 + assertFalse(number.isEmpty(), "Json5Number 不应被视为空"); + } } diff --git a/src/test/java/de/marhali/json5/TestJson5Comments.java b/src/test/java/de/marhali/json5/TestJson5Comments.java new file mode 100644 index 0000000..becc024 --- /dev/null +++ b/src/test/java/de/marhali/json5/TestJson5Comments.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2024 Ultreon Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.marhali.json5; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Unit tests for comment handling in the {@link Json5} core class. + * + * @author Ultreon Team + */ +public class TestJson5Comments { + private Json5 json5; + + private static Json5Object getJson5Object() { + Json5Object root = new Json5Object(); + root.setComment("Root object comment"); + + Json5Boolean enabled = new Json5Boolean(true); + enabled.setComment("This is the enabled flag"); + root.add("enabled", enabled); + + Json5Array services = new Json5Array(); + + Json5String serviceName = new Json5String("auth-service"); + serviceName.setComment("Authentication service"); + services.add(serviceName); + + root.add("services", services); + root.setComment("services", "List of services"); + return root; + } + + private InputStream getTestResource(String fileName) { + return Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName); + } + + private String getTestResourceContent(String fileName) throws IOException { + try (BufferedInputStream bis = new BufferedInputStream(getTestResource(fileName))) { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + for (int result = bis.read(); result != -1; result = bis.read()) { + buf.write((byte) result); + } + + return Optional.ofNullable(buf.toString(StandardCharsets.UTF_8)).map(s -> s.replace("\r\n", "\n")).map( + s -> s.replace("\r", "\n")).orElse(null); + } + } + + @BeforeEach + void setup() { + json5 = Json5.builder( + builder -> builder.allowInvalidSurrogate().readComments().quoteSingle().indentFactor(2).build()); + } + + @Test + void parseAndVerifyComments() throws IOException { + try (InputStream stream = getTestResource("test.comments.json5")) { + Json5Element element = json5.parse(stream); + assertTrue(element.isJson5Object()); + + Json5Object obj = element.getAsJson5Object(); + + // Verify comments are read correctly + assertEquals("The master switch for the feature", obj.get("enabled").getComment()); + assertEquals("A list of network ports", obj.get("ports").getComment()); + + Json5Array ports = obj.getAsJson5Array("ports"); + assertEquals("Standard port", ports.get(0).getComment()); + assertEquals("Alternate port", ports.get(1).getComment()); + assertEquals(""" + /** + * Admin port + * Do not expose publicly + */\ + """, ports.get(2).getComment()); + + Json5Object user = obj.getAsJson5Object("user"); + assertEquals("User configuration", user.getComment()); + assertEquals("The user's login name", user.get("name").getComment()); + } + } + + @Test + void serializeWithComments() throws IOException { + try (InputStream stream = getTestResource("test.comments.json5")) { + Json5Element element = json5.parse(stream); + String expected = getTestResourceContent("expect.comments.json5"); + assertEquals(expected, json5.serialize(element)); + } + } + + @Test + void 少引号顶部注释测试() throws IOException { + try (InputStream stream = getTestResource("expect.quoteless.comment.json5")) { + Json5Options options = + new Json5OptionsBuilder().quoteless().quoteSingle().readComments().prettyPrinting().build(); + Json5 json = new Json5(options); + Json5Element element = json.parse(stream); + String expected = getTestResourceContent("expect.quoteless.comment.json5"); + assertEquals(expected, json.serialize(element)); + } + } + + @Test + void 深拷贝与无注释拷贝测试() throws IOException { + try (InputStream stream = getTestResource("expect.quoteless.comment.json5")) { + Json5Options options = + new Json5OptionsBuilder().quoteless().quoteSingle().readComments().prettyPrinting().build(); + Json5 json = new Json5(options); + Json5Element element = json.parse(stream); + assertEquals(getTestResourceContent("expect.quoteless.comment.json5"), json.serialize(element.deepCopy())); + assertEquals(getTestResourceContent("expect.无注释拷贝.json5"), json.serialize(element.noCommentCopy())); + } + } + + @Test + void programmaticallyAddComments() throws IOException { + Json5Object root = getJson5Object(); + + String expected = """ + // Root object comment + { + // This is the enabled flag + 'enabled': true, + // List of services + 'services': [ + // Authentication service + 'auth-service' + ] + }"""; + assertEquals(expected, json5.serialize(root)); + assertEquals(""" + {"enabled":true,"services":["auth-service"]}\ + """, root.toStandardString()); + } + + @Test + void testCopyCommentToVariants() { + // 基础元素注释复制 + Json5String sourceStr = new Json5String("hello"); + sourceStr.setComment("这是一个字符串注释"); + + Json5String targetStr = new Json5String("hello"); + sourceStr.copyCommentTo(targetStr); + + assertEquals("这是一个字符串注释", targetStr.getComment(), "字符串注释应被复制"); + + // 数组注释复制(包括元素注释) + Json5Element sourceArr = json5.parse(""" + // 数组注释 + [// 注释A + 'a'] + """); + + Json5Array targetArr = new Json5Array(); + targetArr.add(new Json5String("a")); + targetArr.add(new Json5String("b")); + + sourceArr.getAsJson5Array().mergeCommentTo(targetArr); + + assertEquals("数组注释", targetArr.getComment(), "数组注释应被复制"); + assertEquals("注释A", targetArr.get(0).getComment(), "元素0注释应被复制"); + + // 对象注释复制(键注释) + Json5Object sourceObj = json5.parse(""" + // 对象注释 + { + // 注释X + x:1, + // 注释Y + y:2 + } + """).getAsJson5Object(); + Json5Object targetObj = new Json5Object(); + targetObj.add("x", new Json5Number(1)); + targetObj.add("y", new Json5Number(2)); + + sourceObj.mergeCommentTo(targetObj); + + assertEquals("对象注释", targetObj.getComment(), "对象注释应被复制"); + assertEquals("注释X", targetObj.getComment("x"), "键x注释应被复制"); + assertEquals("注释Y", targetObj.getComment("y"), "键y注释应被复制"); + } +} diff --git a/src/test/java/de/marhali/json5/TestJson5Object.java b/src/test/java/de/marhali/json5/TestJson5Object.java index 6801455..6f49942 100644 --- a/src/test/java/de/marhali/json5/TestJson5Object.java +++ b/src/test/java/de/marhali/json5/TestJson5Object.java @@ -16,19 +16,21 @@ package de.marhali.json5; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.jupiter.api.Assertions.*; - /** * Unit tests for {@link Json5Object} * * @author Marcel Haßlinger */ public class TestJson5Object { - @Test void remove() { Json5Object element = new Json5Object(); diff --git a/src/test/java/de/marhali/json5/TestJson5Options.java b/src/test/java/de/marhali/json5/TestJson5Options.java index c0302a2..ef56b59 100644 --- a/src/test/java/de/marhali/json5/TestJson5Options.java +++ b/src/test/java/de/marhali/json5/TestJson5Options.java @@ -36,7 +36,9 @@ public class TestJson5Options { void singleQuoted() { String payload = "['hello',1,'two']"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); @@ -47,7 +49,9 @@ void singleQuoted() { void doubleQuoted() { String payload = "['hello',1,'two']"; - Json5Options options = new Json5Options(true, false, false, 0); + // + Json5Options options = new Json5Options(true, false, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); @@ -58,7 +62,9 @@ void doubleQuoted() { void trailingComma() { String payload = "['hello',1,'two']"; - Json5Options options = new Json5Options(true, true, true, 0); + // + Json5Options options = new Json5Options(true, true, true, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); @@ -69,10 +75,39 @@ void trailingComma() { void prettyPrinting() { String payload = "['hello',1,'two']"; - Json5Options options = new Json5Options(true, true, true, 2); + // + Json5Options options = new Json5Options(true, true, true, 2, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals("[\n 'hello',\n 1,\n 'two',\n]", element.toString(options)); } + + // + @Test + void quoteless() { + String payload = "{hello: 'world', key: 'value'}"; + + Json5Options options = new Json5Options(true, true, true, 2, true); + Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); + Json5Object element = Json5Parser.parseObject(lexer); + + assertEquals("{\n hello: 'world',\n key: 'value',\n}", element.toString(options)); + } + // + + // + @Test + void quotelessAndComment() { + Json5Options options = new Json5Options(true, true, true, 2, true); + Json5Object element = new Json5Object(); + + element.add("hello", Json5Primitive.of("world")); + element.add("key", Json5Primitive.of("value")); + element.setComment("key", "This is a comment\nAnd another comment"); + + assertEquals("{\n hello: 'world',\n // This is a comment\n // And another comment\n key: 'value',\n}", element.toString(options)); + } + // } \ No newline at end of file diff --git a/src/test/java/de/marhali/json5/TestJson5Parser.java b/src/test/java/de/marhali/json5/TestJson5Parser.java index 47b536e..c2d3ff1 100644 --- a/src/test/java/de/marhali/json5/TestJson5Parser.java +++ b/src/test/java/de/marhali/json5/TestJson5Parser.java @@ -36,7 +36,9 @@ public class TestJson5Parser { @Test void array() { String payload = "['hello',1,'two',{'key':'value'}]"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals(payload, element.toString(options)); @@ -48,7 +50,10 @@ void array() { @Test void object() { String payload = "{'key':'value','array':['first','second'],'nested':{'key':'value'}}"; - Json5Options options = new Json5Options(true, true, false, 0); + + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertEquals(payload, element.toString(options)); @@ -59,7 +64,9 @@ void object() { @Test void determineArrayType() { String payload = "['first','second']"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Element element = Json5Parser.parse(lexer); assertTrue(element.isJson5Array()); @@ -69,7 +76,9 @@ void determineArrayType() { @Test void determineObjectType() { String payload = "{'key':'value'}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Element element = Json5Parser.parse(lexer); assertTrue(element.isJson5Object()); @@ -79,7 +88,9 @@ void determineObjectType() { @Test void hexadecimal() { String payload = "{'key':0x100}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertEquals(payload, element.toString(options)); @@ -89,7 +100,9 @@ void hexadecimal() { @Test void insideQuotes() { String payload = "[\"example\",'other']"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals("['example','other']", element.toString(options)); @@ -98,7 +111,9 @@ void insideQuotes() { @Test void mixedQuotes() { String payload = "{ a: \"Test \\' 123\" }"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertEquals("Test ' 123", element.get("a").getAsString()); @@ -107,7 +122,9 @@ void mixedQuotes() { @Test void escapeChars() { String payload = "{ a: \"\\n\\r\\f\\b\\t\\v\\0\u12fa\\x7F\" }"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertEquals("\n\r\f\b\t\u000B\0\u12fa\u007F", element.get("a").getAsString()); @@ -117,7 +134,9 @@ void escapeChars() { @Test void specialNumbers() { String payload = "[+NaN,NaN,-NaN,+Infinity,Infinity,-Infinity]"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals("[NaN,NaN,NaN,Infinity,Infinity,-Infinity]", element.toString(options)); @@ -127,7 +146,9 @@ void specialNumbers() { @Test void malformed() { String payload = "[10}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parse(lexer)); } @@ -135,7 +156,9 @@ void malformed() { @Test void notAObject() { String payload = "[]"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseObject(lexer)); } @@ -143,7 +166,9 @@ void notAObject() { @Test void incompleteObject() { String payload = "{"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseObject(lexer)); } @@ -151,7 +176,9 @@ void incompleteObject() { @Test void notAArray() { String payload = "{}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseArray(lexer)); } @@ -159,7 +186,9 @@ void notAArray() { @Test void incompleteArray() { String payload = "["; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseArray(lexer)); } @@ -167,7 +196,9 @@ void incompleteArray() { @Test void duplicateObjectKeys() { String payload = "{'key':'value','key':'value'}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseObject(lexer)); } @@ -175,7 +206,9 @@ void duplicateObjectKeys() { @Test void noDivider() { String payload = "{'key''value'}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseObject(lexer)); } @@ -183,7 +216,9 @@ void noDivider() { @Test void noComma() { String payload = "{'key':'value''otherKey':'value'}"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parseObject(lexer)); } @@ -191,7 +226,9 @@ void noComma() { @Test void unknownControlCharacter() { String payload = "|"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertThrows(Json5Exception.class, () -> Json5Parser.parse(lexer)); } @@ -199,7 +236,9 @@ void unknownControlCharacter() { @Test void empty() { String payload = ""; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); assertNull(Json5Parser.parse(lexer)); } @@ -207,7 +246,9 @@ void empty() { @Test void memberNames() { String payload = "{ $Lorem\\u0041_Ipsum123指事字: 0 }"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertTrue(element.has("$LoremA_Ipsum123指事字")); @@ -216,7 +257,9 @@ void memberNames() { @Test void multiComments() { String payload = "/**/{/**/a/**/:/**/'b'/**/}/**/"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertTrue(element.has("a")); @@ -225,7 +268,9 @@ void multiComments() { @Test void singleComments() { String payload = "// test\n{ // lorem ipsum\n a: 'b'\n// test\n}// test"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Object element = Json5Parser.parseObject(lexer); assertTrue(element.has("a")); @@ -234,7 +279,9 @@ void singleComments() { @Test void booleans() { String payload = "[true,false]"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals(payload, element.toString(options)); @@ -244,7 +291,9 @@ void booleans() { @Test void numbers() { String payload = "[123e+45,-123e45,123]"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals(123e+45, element.get(0).getAsNumber().doubleValue()); @@ -258,7 +307,9 @@ void numbers() { @Test void nullLiteral() { String payload = "[null,{'key':null}]"; - Json5Options options = new Json5Options(true, true, false, 0); + // + Json5Options options = new Json5Options(true, true, false, 0, false); + // Json5Lexer lexer = new Json5Lexer(new StringReader(payload), options); Json5Array element = Json5Parser.parseArray(lexer); assertEquals(payload, element.toString(options)); diff --git a/src/test/java/de/marhali/json5/TestJson5Writer.java b/src/test/java/de/marhali/json5/TestJson5Writer.java index 6b59f0c..3fd0304 100644 --- a/src/test/java/de/marhali/json5/TestJson5Writer.java +++ b/src/test/java/de/marhali/json5/TestJson5Writer.java @@ -16,6 +16,8 @@ package de.marhali.json5; +import static org.junit.jupiter.api.Assertions.assertEquals; + import de.marhali.json5.stream.Json5Writer; import org.junit.jupiter.api.BeforeEach; @@ -24,8 +26,6 @@ import java.io.IOException; import java.io.StringWriter; -import static org.junit.jupiter.api.Assertions.*; - /** * Unit tests for the {@link Json5Writer}. * @@ -33,8 +33,10 @@ */ public class TestJson5Writer { + // private final Json5Options options - = new Json5Options(true, true, false, 0); + = new Json5Options(true, true, false, 0, false); + // private StringWriter stringWriter; private Json5Writer json5Writer; @@ -63,7 +65,7 @@ void array() throws IOException { array.add(123); array.add(new Json5Hexadecimal("0x100")); array.add("Lorem ipsum"); - array.add(Json5Null.INSTANCE); + array.add(new Json5Null()); array.add(new Json5Object()); json5Writer.write(array); @@ -87,6 +89,26 @@ void object() throws IOException { stringWriter.toString()); } + @Test + void objectWithComments() throws IOException { + Json5Object object = new Json5Object(); + object.add("bool", Json5Primitive.of(false)); + object.add("num", Json5Primitive.of(123)); + object.add("hex", new Json5Hexadecimal("0x100")); + object.add("str", Json5Primitive.of("Lorem ipsum")); + object.add("nulled", new Json5Null()); + object.setComment("nulled", "Null on purpose"); + object.add("array", new Json5Array()); + + json5Writer.write(object); + + assertEquals(""" +{'bool':false,'num':123,'hex':0x100,'str':'Lorem ipsum',// Null on purpose +'nulled':null,'array':[]}\ + """, + stringWriter.toString()); + } + @Test void nullLiteral() throws IOException { json5Writer.write(Json5Null.INSTANCE); @@ -140,9 +162,9 @@ void memberNames() throws IOException { @Test void escapeChars() throws IOException { Json5Array array = new Json5Array(); - array.add("\\\n\r\f\b\t\u000B\u12fa\u000b"); + array.add("\\\n\r\f\b\t\u000Bዺ\u000b"); json5Writer.write(array); - assertEquals("['\\\\\\n\\r\\f\\b\\t\\v\u12fa\\v']", stringWriter.toString()); + assertEquals("['\\\\\\n\\r\\f\\b\\t\\vዺ\\v']", stringWriter.toString()); // Cannot test \x7F } } diff --git a/src/test/java/de/marhali/json5/TestJson5WriterQuoteless.java b/src/test/java/de/marhali/json5/TestJson5WriterQuoteless.java new file mode 100644 index 0000000..352f03e --- /dev/null +++ b/src/test/java/de/marhali/json5/TestJson5WriterQuoteless.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 Marcel Haßlinger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.marhali.json5; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.marhali.json5.stream.Json5Writer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringWriter; + +/** + * Unit tests for the {@link Json5Writer}. + * + * @author Marcel Haßlinger + */ +public class TestJson5WriterQuoteless { + // + private final Json5Options options = new Json5Options(true, true, false, 0, true); + // + + private StringWriter stringWriter; + + private Json5Writer json5Writer; + + @BeforeEach + void beforeEach() { + stringWriter = new StringWriter(); + json5Writer = new Json5Writer(options, stringWriter); + } + + @Test + void quoteEmpty() { + assertEquals("''", json5Writer.quote(null)); + assertEquals("''", json5Writer.quote("")); + } + + @Test + void quoteEscape() { + assertEquals("'\\\\n'", json5Writer.quote("\\n")); + } + + @Test + void array() throws IOException { + Json5Array array = new Json5Array(); + array.add(true); + array.add(123); + array.add(new Json5Hexadecimal("0x100")); + array.add("Lorem ipsum"); + array.add(Json5Null.INSTANCE); + array.add(new Json5Object()); + + json5Writer.write(array); + + // + assertEquals("[true,123,0x100,'Lorem ipsum',null,{}]", stringWriter.toString()); + // + } + + @Test + void object() throws IOException { + Json5Object object = new Json5Object(); + object.add("bool", Json5Primitive.of(false)); + object.add("num", Json5Primitive.of(123)); + object.add("hex", new Json5Hexadecimal("0x100")); + object.add("str", Json5Primitive.of("Lorem ipsum")); + object.add("nulled", Json5Null.INSTANCE); + object.add("array", new Json5Array()); + + json5Writer.write(object); + + assertEquals("{bool:false,num:123,hex:0x100,str:'Lorem ipsum',nulled:null,array:[]}", stringWriter.toString()); + } + + // + @Test + void objectWithComments() throws IOException { + Json5Object object = new Json5Object(); + object.add("bool", Json5Primitive.of(false)); + object.add("num", Json5Primitive.of(123)); + object.add("hex", new Json5Hexadecimal("0x100")); + object.add("str", Json5Primitive.of("Lorem ipsum")); + object.add("nulled", new Json5Null()); + object.setComment("nulled", "Null on purpose"); + object.add("array", new Json5Array()); + + json5Writer.write(object); + + assertEquals(""" + {bool:false,num:123,hex:0x100,str:'Lorem ipsum',// Null on purpose + nulled:null,array:[]}\ + """, stringWriter.toString()); + } + // + + @Test + void nullLiteral() throws IOException { + json5Writer.write(Json5Null.INSTANCE); + assertEquals("null", stringWriter.toString()); + } + + @Test + void booleans() throws IOException { + json5Writer.write(new Json5Boolean(false)); + assertEquals("false", stringWriter.toString()); + } + + @Test + void largeNumber() throws IOException { + json5Writer.write(new Json5Number(123e+45)); + assertEquals("1.23E47", stringWriter.toString()); + } + + @Test + void largeNumberNegate() throws IOException { + json5Writer.write(new Json5Number(-123e+45)); + assertEquals("-1.23E47", stringWriter.toString()); + } + + @Test + void hexadecimal() throws IOException { + json5Writer.write(new Json5Hexadecimal("0x100")); + assertEquals("0x100", stringWriter.toString()); + } + + @Test + void number() throws IOException { + json5Writer.write(new Json5Number(123)); + assertEquals("123", stringWriter.toString()); + } + + @Test + void string() throws IOException { + json5Writer.write(new Json5String("Lorem ipsum")); + assertEquals("'Lorem ipsum'", stringWriter.toString()); + } + + @Test + void memberNames() throws IOException { + Json5Object object = new Json5Object(); + object.addProperty("$LoremA_Ipsum123指事字", 0); + json5Writer.write(object); + assertEquals("{'$LoremA_Ipsum123指事字':0}", stringWriter.toString()); + } + + @Test + void escapeChars() throws IOException { + Json5Array array = new Json5Array(); + array.add("\\\n\r\f\b\t\u000B\u12fa\u000b"); + json5Writer.write(array); + assertEquals("['\\\\\\n\\r\\f\\b\\t\\v\u12fa\\v']", stringWriter.toString()); + // Cannot test \x7F + } +} diff --git a/src/test/resources/expect.comments.json5 b/src/test/resources/expect.comments.json5 new file mode 100644 index 0000000..a345efd --- /dev/null +++ b/src/test/resources/expect.comments.json5 @@ -0,0 +1,21 @@ +{ + // The master switch for the feature + 'enabled': true, + // A list of network ports + 'ports': [ + // Standard port + 8001, + // Alternate port + 8002, + /** + * Admin port + * Do not expose publicly + */ + 9999 + ], + // User configuration + 'user': { + // The user's login name + 'name': 'gemini' + } +} \ No newline at end of file diff --git a/src/test/resources/expect.quoteless.comment.json5 b/src/test/resources/expect.quoteless.comment.json5 new file mode 100644 index 0000000..f773d9c --- /dev/null +++ b/src/test/resources/expect.quoteless.comment.json5 @@ -0,0 +1,31 @@ +// 验证连续单行 +// json5支持quoteless 匹配^[a-zA-Z_][a-zA-Z0-9_]*[a-zA-Z_]$的key可以不加引号 +{ + // json5 支持尾逗号和注释 + cards_apd_business_scenario_t: { + // 此对象值皆视为字面量 + '背景数据': { + target_table_name: 'cards_ar_invoice_info_t', + append_table_name: 'cards_ar_inv_info_apd_t', + append_tmp_table_name: 'cards_ar_inv_info_apd_tmp', + append_effective_type: 'APPEND_PRIORITY', + business_scenario_zh_name: '清关发票补录Jiuli', + business_scenario_en_name: 'Append customs invoice No', + description: '清关发票数据补录', + enabled_flag: 'Y', + delete_flag: 'N', + creation_date: '2025-03-07 13:39:16.000000', + last_updated_by: 174022309561388, + last_update_date: '2025-05-06 20:09:57.436000', + tenant_id: '11111111111111111111111111111111' + }, + // 以下由程序根据.feature文件自动生成 + '重点验证': [ + { + created_by: '${created}' + } + ], + // key为字段名 value为缺省值 value为null时表示让程序自动赋值 value为${seq}时表示使用自增主键值 + '隔离策略': {} + } +} \ No newline at end of file diff --git "a/src/test/resources/expect.\346\227\240\346\263\250\351\207\212\346\213\267\350\264\235.json5" "b/src/test/resources/expect.\346\227\240\346\263\250\351\207\212\346\213\267\350\264\235.json5" new file mode 100644 index 0000000..d7a0807 --- /dev/null +++ "b/src/test/resources/expect.\346\227\240\346\263\250\351\207\212\346\213\267\350\264\235.json5" @@ -0,0 +1,25 @@ +{ + cards_apd_business_scenario_t: { + '背景数据': { + target_table_name: 'cards_ar_invoice_info_t', + append_table_name: 'cards_ar_inv_info_apd_t', + append_tmp_table_name: 'cards_ar_inv_info_apd_tmp', + append_effective_type: 'APPEND_PRIORITY', + business_scenario_zh_name: '清关发票补录Jiuli', + business_scenario_en_name: 'Append customs invoice No', + description: '清关发票数据补录', + enabled_flag: 'Y', + delete_flag: 'N', + creation_date: '2025-03-07 13:39:16.000000', + last_updated_by: 174022309561388, + last_update_date: '2025-05-06 20:09:57.436000', + tenant_id: '11111111111111111111111111111111' + }, + '重点验证': [ + { + created_by: '${created}' + } + ], + '隔离策略': {} + } +} \ No newline at end of file diff --git a/src/test/resources/test.comments.json5 b/src/test/resources/test.comments.json5 new file mode 100644 index 0000000..5e97008 --- /dev/null +++ b/src/test/resources/test.comments.json5 @@ -0,0 +1,21 @@ +{ + //The master switch for the feature + "enabled": true, + // A list of network ports + "ports": [ + // Standard port + 8001, + // Alternate port + 8002, + /** + * Admin port + * Do not expose publicly + */ + 9999 + ], + // User configuration + "user": { + // The user's login name + "name": "gemini" + } +} \ No newline at end of file