- * You can create a Json5 instance by invoking {@link #Json5(Json5Options)}
- * or by using {@link #builder(Function)}.
- *
+ * You can create a Json5 instance by invoking {@link #Json5(Json5Options)}
+ * or by using {@link #builder(Function)}.
+ *
*
*
- * This class contains several utility methods to parse and serialize json5 data by passing
- * {@link Reader}, {@link Writer} or simple {@link String} instances.
+ * This class contains several utility methods to parse and serialize json5 data by passing
+ * {@link Reader}, {@link Writer} or simple {@link String} instances.
*
+ *
* @author Marcel Haßlinger
- * @see JSON5 Specification
+ * @see Json5 Specification
* @see Json5Parser
* @see Json5Writer
*/
public final class Json5 {
/**
- * Constructs a new json5 instance by using the {@link Json5OptionsBuilder}.
+ * Constructs a new json5 instance by using the {@link Json5Options#builder()}.
+ *
* @param builder Options builder
- * @return Provide built options by returning {@link Json5OptionsBuilder#build()} method
+ * @return Built options
*/
- public static Json5 builder(Function builder) {
- return new Json5(builder.apply(new Json5OptionsBuilder()));
+ public static Json5 builder(Function builder) {
+ return new Json5(builder.apply(Json5Options.builder()));
}
private final Json5Options options;
/**
* Constructs a new json5 instance with custom configuration for parsing and serialization.
+ *
* @param options Configuration options
* @see #builder(Function)
*/
@@ -67,6 +108,7 @@ public Json5(Json5Options options) {
/**
* Constructs a json5 instance by using {@link Json5Options#DEFAULT} as configuration.
+ *
* @see #Json5(Json5Options)
*/
public Json5() {
@@ -74,9 +116,9 @@ public Json5() {
}
/**
- * Parses the data from the {@link InputStream} into a tree of {@link Json5Element}'s. There must be
- * a root element based on a {@link Json5Object} or {@link Json5Array}.
+ * Parses the data from the {@link InputStream} into a tree of {@link Json5Element}'s.
*
Note: The stream must be closed after operation
+ *
* @param in Can be any applicable {@link InputStream}
* @return Parsed json5 tree. Can be {@code null} if the provided stream does not contain any data
* @see #parse(Reader)
@@ -87,12 +129,12 @@ public Json5Element parse(InputStream in) {
}
/**
- * Parses the provided read-stream into a tree of {@link Json5Element}'s. There must be
- * a root element based on a {@link Json5Object} or {@link Json5Array}.
+ * Parses the provided read-stream into a tree of {@link Json5Element}'s.
*
Note: The reader must be closed after operation
+ *
* @param reader Can be any applicable {@link Reader}
* @return Parsed json5 tree. Can be {@code null} if the provided stream does not contain any data
- * @see Json5Parser#parse(Json5Lexer)
+ * @see Json5Parser#parse(Json5Lexer)
*/
public Json5Element parse(Reader reader) {
Objects.requireNonNull(reader);
@@ -103,15 +145,15 @@ public Json5Element parse(Reader reader) {
/**
* Parses the provided json5-encoded {@link String} into a parse tree of {@link Json5Element}'s.
- * There must be a root element based on a {@link Json5Object} or {@link Json5Array}.
- * @param jsonString Json5 encoded {@link String}
+ *
+ * @param string Json5 encoded {@link String}
* @return Parsed json5 tree. Can be {@code null} if the provided {@link String} is empty
- * @see #parse(Reader)
+ * @see #parse(Reader)
*/
- public Json5Element parse(String jsonString) {
- Objects.requireNonNull(jsonString);
+ public Json5Element parse(String string) {
+ Objects.requireNonNull(string);
- StringReader reader = new StringReader(jsonString);
+ StringReader reader = new StringReader(string);
Json5Element element = this.parse(reader);
reader.close();
return element;
@@ -120,8 +162,9 @@ public Json5Element parse(String jsonString) {
/**
* Encodes the provided element into its character literal representation by using an output-stream.
*
Note: The stream must be closed after operation ({@link OutputStream#close()})!
+ *
* @param element {@link Json5Element} to serialize
- * @param out Can be any applicable {@link OutputStream}
+ * @param out Can be any applicable {@link OutputStream}
* @throws IOException If an I/O error occurs
* @see #serialize(Json5Element, Writer)
*/
@@ -135,10 +178,11 @@ public void serialize(Json5Element element, OutputStream out) throws IOException
/**
* Encodes the provided element into its character literal representation by using a write-stream.
*
Note: The writer must be closed after operation ({@link Writer#close()})!
+ *
* @param element {@link Json5Element} to serialize
- * @param writer Can be any applicable {@link Writer}
+ * @param writer Can be any applicable {@link Writer}
* @throws IOException If an I/O error occurs
- * @see Json5Writer#write(Json5Element)
+ * @see Json5Writer#write(Json5Element)
*/
public void serialize(Json5Element element, Writer writer) throws IOException {
Objects.requireNonNull(element);
@@ -150,10 +194,11 @@ public void serialize(Json5Element element, Writer writer) throws IOException {
/**
* Encodes the provided element into its character literal representation.
+ *
* @param element {@link Json5Element} to serialize
* @return Json5 encoded {@link String}
* @throws IOException If an I/O error occurs
- * @see #serialize(Json5Element, Writer)
+ * @see #serialize(Json5Element, Writer)
*/
public String serialize(Json5Element element) throws IOException {
Objects.requireNonNull(element);
@@ -163,4 +208,4 @@ public String serialize(Json5Element element) throws IOException {
writer.close();
return writer.toString();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/de/marhali/json5/Json5Array.java b/src/main/java/de/marhali/json5/Json5Array.java
index 86349e9..9110c50 100644
--- a/src/main/java/de/marhali/json5/Json5Array.java
+++ b/src/main/java/de/marhali/json5/Json5Array.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008 Google Inc.
- * Copyright (C) 2022 Marcel Haßlinger
+ * Copyright (C) 2022 - 2025 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.
@@ -17,48 +17,73 @@
package de.marhali.json5;
+import de.marhali.json5.internal.NonNullElementWrapperList;
+import de.marhali.json5.internal.RadixNumber;
+
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
/**
* A class representing an array type in Json5. An array is a list of {@link Json5Element}s each of
* which can be of a different type. This is an ordered list, meaning that the order in which
- * elements are added is preserved.
+ * elements are added is preserved. This class does not support {@code null} values. If {@code null}
+ * is provided as element argument to any of the methods, it is converted to a {@link Json5Null}.
+ *
+ *
{@code Json5Array} only implements the {@link Iterable} interface but not the {@link List}
+ * interface. A {@code List} view of it can be obtained with {@link #asList()}.
+ *
+ *
See the {@link Json5} documentation for details on how to convert {@code Json5Array} and
+ * generally any {@code Json5Element} from and to Json5.
*
- * @author Marcel Haßlinger
* @author Inderjeet Singh
* @author Joel Leitch
+ * @author Marcel Haßlinger
*/
public final class Json5Array extends Json5Element implements Iterable {
- private final List elements;
+ private final ArrayList elements;
/**
* Creates an empty Json5Array.
*/
public Json5Array() {
- elements = new ArrayList();
+ elements = new ArrayList<>();
}
+ /**
+ * Creates an empty Json5Array with the desired initial capacity.
+ *
+ * @param capacity initial capacity.
+ * @throws IllegalArgumentException if the {@code capacity} is negative
+ */
public Json5Array(int capacity) {
- elements = new ArrayList(capacity);
+ elements = new ArrayList<>(capacity);
}
/**
- * Creates a deep copy of this element and all its children
+ * Creates a deep copy of this element and all its children.
*/
@Override
public Json5Array deepCopy() {
- if (!elements.isEmpty()) {
- Json5Array result = new Json5Array(elements.size());
- for (Json5Element element : elements) {
- result.add(element.deepCopy());
- }
- return result;
+ Json5Array result = new Json5Array(elements.size());
+ for (Json5Element element : elements) {
+ result.add(element.deepCopy());
}
- return new Json5Array();
+ result.setComment(comment);
+ return result;
+ }
+
+ /**
+ * Adds the specified {@link Instant} to self.
+ *
+ * @param instant the {@link Instant} that needs to be added to the array.
+ */
+ public void add(Instant instant) {
+ elements.add(instant == null ? Json5Primitive.fromNull() : Json5Primitive.fromInstant(instant));
}
/**
@@ -67,7 +92,7 @@ public Json5Array deepCopy() {
* @param bool the boolean that needs to be added to the array.
*/
public void add(Boolean bool) {
- elements.add(bool == null ? Json5Null.INSTANCE : new Json5Boolean(bool));
+ elements.add(bool == null ? Json5Primitive.fromNull() : Json5Primitive.fromBoolean(bool));
}
/**
@@ -76,7 +101,7 @@ public void add(Boolean bool) {
* @param character the character that needs to be added to the array.
*/
public void add(Character character) {
- elements.add(character == null ? Json5Null.INSTANCE : new Json5String(character.toString()));
+ elements.add(character == null ? Json5Primitive.fromNull() : Json5Primitive.fromCharacter(character));
}
/**
@@ -85,7 +110,11 @@ public void add(Character character) {
* @param number the number that needs to be added to the array.
*/
public void add(Number number) {
- elements.add(number == null ? Json5Null.INSTANCE : new Json5Number(number));
+ elements.add(number == null ? Json5Primitive.fromNull() : Json5Primitive.fromNumber(number));
+ }
+
+ public void add(Number number, int radix) {
+ elements.add(number == null ? Json5Primitive.fromNull() : Json5Primitive.fromNumber(number, radix));
}
/**
@@ -94,7 +123,7 @@ public void add(Number number) {
* @param string the string that needs to be added to the array.
*/
public void add(String string) {
- elements.add(string == null ? Json5Null.INSTANCE : new Json5String(string));
+ elements.add(string == null ? Json5Primitive.fromNull() : Json5Primitive.fromString(string));
}
/**
@@ -104,7 +133,7 @@ public void add(String string) {
*/
public void add(Json5Element element) {
if (element == null) {
- element = Json5Null.INSTANCE;
+ element = Json5Primitive.fromNull();
}
elements.add(element);
}
@@ -120,19 +149,20 @@ public void addAll(Json5Array array) {
/**
* Replaces the element at the specified position in this array with the specified element.
- * Element can be null.
- * @param index index of the element to replace
+ *
+ * @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
* @throws IndexOutOfBoundsException if the specified index is outside the array bounds
*/
public Json5Element set(int index, Json5Element element) {
- return elements.set(index, element);
+ return elements.set(index, element == null ? Json5Primitive.fromNull() : 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.
+ * 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
*/
@@ -141,9 +171,10 @@ 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.
+ * 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
@@ -154,8 +185,9 @@ public Json5Element remove(int 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,9 +203,9 @@ public int size() {
}
/**
- * Returns true if the array is empty
+ * Returns true if the array is empty.
*
- * @return true if the array is empty
+ * @return true if the array is empty.
*/
public boolean isEmpty() {
return elements.isEmpty();
@@ -185,197 +217,268 @@ public boolean isEmpty() {
*
* @return an iterator to navigate the elements of the array.
*/
+ @Override
public Iterator iterator() {
return elements.iterator();
}
/**
- * Returns the ith element of the array.
+ * Returns the i-th element of the array.
*
* @param i the index of the element that is being sought.
- * @return the element present at the ith index.
- * @throws IndexOutOfBoundsException if i is negative or greater than or equal to the
- * {@link #size()} of the array.
+ * @return the element present at the i-th index.
+ * @throws IndexOutOfBoundsException if {@code i} is negative or greater than or equal to the
+ * {@link #size()} of the array.
*/
public Json5Element get(int i) {
return elements.get(i);
}
+ private Json5Element getAsSingleElement() {
+ int size = elements.size();
+ if (size == 1) {
+ return elements.get(0);
+ }
+ throw new IllegalStateException("Array must have size 1, but has size " + size);
+ }
+
+ @Override
+ public Json5Null getAsJson5Null() {
+ return getAsSingleElement().getAsJson5Null();
+ }
+
/**
- * convenience method to get this array as a {@link Number} if it contains a single element.
+ * Convenience method to get this array as a {@link Number} if it contains a single element. This
+ * method calls {@link Json5Element#getAsNumber()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
*
- * @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.
+ * @return this element as a number if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public Number getAsNumber() {
- if (elements.size() == 1) {
- return elements.get(0).getAsNumber();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsNumber();
+ }
+
+ /**
+ * Convenience method to get this array as a {@link RadixNumber} if it contains a single element. This
+ * method calls {@link Json5Element#getAsRadixNumber()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
+ *
+ * @return this element as a radix number if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
+ @Override
+ public RadixNumber getAsRadixNumber() {
+ return getAsSingleElement().getAsRadixNumber();
+ }
+
+ /**
+ * Convenience method to get this array as a binary number string if it contains a single element. This
+ * method calls {@link Json5Element#getAsBinaryString()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
+ *
+ * @return this element as a binary number string if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
+ @Override
+ public String getAsBinaryString() {
+ return getAsSingleElement().getAsBinaryString();
+ }
+
+ /**
+ * Convenience method to get this array as a octal number string if it contains a single element. This
+ * method calls {@link Json5Element#getAsOctalString()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
+ *
+ * @return this element as a octal number string if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
+ @Override
+ public String getAsOctalString() {
+ return getAsSingleElement().getAsOctalString();
+ }
+
+ /**
+ * Convenience method to get this array as a hex number string if it contains a single element. This
+ * method calls {@link Json5Element#getAsHexString()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
+ *
+ * @return this element as a hex number string if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
+ @Override
+ public String getAsHexString() {
+ return getAsSingleElement().getAsHexString();
}
/**
- * convenience method to get this array as a {@link String} if it contains a single element.
+ * Convenience method to get this array as a {@link String} if it contains a single element. This
+ * method calls {@link Json5Element#getAsString()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
*
- * @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.
+ * @return this element as a String if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public String getAsString() {
- if (elements.size() == 1) {
- return elements.get(0).getAsString();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsString();
}
/**
- * convenience method to get this array as a double if it contains a single element.
+ * Convenience method to get this array as a double if it contains a single element. This method
+ * calls {@link Json5Element#getAsDouble()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
*
- * @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.
+ * @return this element as a double if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public double getAsDouble() {
- if (elements.size() == 1) {
- return elements.get(0).getAsDouble();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsDouble();
}
/**
- * convenience method to get this array as a {@link BigDecimal} if it contains a single element.
+ * Convenience method to get this array as a {@link BigDecimal} if it contains a single element.
+ * This method calls {@link Json5Element#getAsBigDecimal()} on the element, therefore any of the
+ * exceptions declared by that method can occur.
*
- * @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.
+ * @return this element as a {@link BigDecimal} if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public BigDecimal getAsBigDecimal() {
- if (elements.size() == 1) {
- return elements.get(0).getAsBigDecimal();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsBigDecimal();
}
/**
- * convenience method to get this array as a {@link BigInteger} if it contains a single element.
+ * Convenience method to get this array as a {@link BigInteger} if it contains a single element.
+ * This method calls {@link Json5Element#getAsBigInteger()} on the element, therefore any of the
+ * exceptions declared by that method can occur.
*
- * @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.
+ * @return this element as a {@link BigInteger} if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public BigInteger getAsBigInteger() {
- if (elements.size() == 1) {
- return elements.get(0).getAsBigInteger();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsBigInteger();
}
/**
- * convenience method to get this array as a float if it contains a single element.
+ * Convenience method to get this array as a float if it contains a single element. This method
+ * calls {@link Json5Element#getAsFloat()} on the element, therefore any of the exceptions declared
+ * by that method can occur.
*
- * @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.
+ * @return this element as a float if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public float getAsFloat() {
- if (elements.size() == 1) {
- return elements.get(0).getAsFloat();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsFloat();
}
/**
- * convenience method to get this array as a long if it contains a single element.
+ * Convenience method to get this array as a long if it contains a single element. This method
+ * calls {@link Json5Element#getAsLong()} on the element, therefore any of the exceptions declared
+ * by that method can occur.
*
- * @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.
+ * @return this element as a long if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public long getAsLong() {
- if (elements.size() == 1) {
- return elements.get(0).getAsLong();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsLong();
}
/**
- * convenience method to get this array as an integer if it contains a single element.
+ * Convenience method to get this array as an integer if it contains a single element. This method
+ * calls {@link Json5Element#getAsInt()} on the element, therefore any of the exceptions declared
+ * by that method can occur.
*
- * @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.
+ * @return this element as an integer if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public int getAsInt() {
- if (elements.size() == 1) {
- return elements.get(0).getAsInt();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsInt();
}
+ /**
+ * Convenience method to get this array as a primitive byte if it contains a single element. This
+ * method calls {@link Json5Element#getAsByte()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
+ *
+ * @return this element as a primitive byte if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
@Override
public byte getAsByte() {
- if (elements.size() == 1) {
- return elements.get(0).getAsByte();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsByte();
}
/**
- * convenience method to get this array as a primitive short if it contains a single element.
+ * Convenience method to get this array as a primitive short if it contains a single element. This
+ * method calls {@link Json5Element#getAsShort()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
*
- * @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.
+ * @return this element as a primitive short if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public short getAsShort() {
- if (elements.size() == 1) {
- return elements.get(0).getAsShort();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsShort();
}
/**
- * convenience method to get this array as a boolean if it contains a single element.
+ * Convenience method to get this array as a Insta if it contains a single element. This method
+ * calls {@link Json5Element#getAsBoolean()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
*
- * @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.
+ * @return this element as a boolean if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
+ @Override
+ public Instant getAsInstant() {
+ return getAsSingleElement().getAsInstant();
+ }
+
+ /**
+ * Convenience method to get this array as a boolean if it contains a single element. This method
+ * calls {@link Json5Element#getAsBoolean()} on the element, therefore any of the exceptions
+ * declared by that method can occur.
+ *
+ * @return this element as a boolean if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public boolean getAsBoolean() {
- if (elements.size() == 1) {
- return elements.get(0).getAsBoolean();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsBoolean();
+ }
+
+ /**
+ * Returns a mutable {@link List} view of this {@code Json5Array}. Changes to the {@code List} are
+ * visible in this {@code Json5Array} and the other way around.
+ *
+ *
The {@code List} does not permit {@code null} elements. Unlike {@code Json5Array}'s {@code
+ * null} handling, a {@link NullPointerException} is thrown when trying to add {@code null}. Use
+ * {@link Json5Null} for Json5 null values.
+ *
+ * @return mutable {@code List} view
+ */
+ public List asList() {
+ return new NonNullElementWrapperList<>(elements);
}
@Override
public boolean equals(Object o) {
- return (o == this) || (o instanceof Json5Array && ((Json5Array) o).elements.equals(elements));
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ Json5Array that = (Json5Array) o;
+ return Objects.equals(elements, that.elements);
}
@Override
public int hashCode() {
- return elements.hashCode();
+ return Objects.hash(super.hashCode(), elements);
}
-}
\ 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
deleted file mode 100644
index 374fc0b..0000000
--- a/src/main/java/de/marhali/json5/Json5Boolean.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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;
-
-/**
- * A class representing a json5 boolean value.
- *
- * @author Marcel Haßlinger
- */
-public final class Json5Boolean extends Json5Primitive {
- public Json5Boolean(Boolean value) {
- super(value);
- }
-}
diff --git a/src/main/java/de/marhali/json5/Json5Element.java b/src/main/java/de/marhali/json5/Json5Element.java
index bd7167a..db1305c 100644
--- a/src/main/java/de/marhali/json5/Json5Element.java
+++ b/src/main/java/de/marhali/json5/Json5Element.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008 Google Inc.
- * Copyright (C) 2022 Marcel Haßlinger
+ * Copyright (C) 2022 - 2025 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.
@@ -17,31 +17,77 @@
package de.marhali.json5;
+import de.marhali.json5.config.Json5Options;
+import de.marhali.json5.internal.RadixNumber;
import de.marhali.json5.stream.Json5Writer;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.time.Instant;
import java.util.Objects;
/**
- * A class representing an element of Json5. It could either be a {@link Json5Object}, a
- * {@link Json5Array}, a {@link Json5Primitive} or a {@link Json5Null}.
+ * A class representing an element of Json5. It could either be a {@link Json5Object}, a {@link
+ * Json5Array}, a {@link Json5Primitive} or a {@link Json5Null}.
+ *
+ *
This class provides multiple {@code getAs} methods which allow
+ *
+ *
+ *
obtaining the represented primitive value, for example {@link #getAsString()}
+ *
casting to the {@code Json5Element} subclasses in a convenient way, for example {@link
+ * #getAsJson5Object()}
+ *
*
- * @author Marcel Haßlinger
* @author Inderjeet Singh
* @author Joel Leitch
+ * @author Marcel Haßlinger
*/
public abstract class Json5Element {
/**
- * @return A deep copy of this element. Immutable elements like primitives
- * and nulls are not copied.
+ * Associated comment on this element. Can be null to omit.
+ * Supports multi-line comments by using the break-line control character \n.
+ */
+ protected String comment;
+
+ /**
+ * Provides a check for verifying if this element has an associated comment.
+ *
+ * @return true if this element has an associated comment, false otherwise.
+ */
+ public boolean hasComment() {
+ return this.comment != null;
+ }
+
+ /**
+ * Returns the associated comment on this element. Can be null if not set.
+ *
+ * @return optional comment string
+ */
+ public String getComment() {
+ return comment;
+ }
+
+ /**
+ * Updates the associated comment on this element.
+ * Supports multi-line comments with break-line control character.
+ *
+ * @param comment Comment to set. Can be null to omit.
+ */
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ /**
+ * Returns a deep copy of this element.
+ *
+ * @return Deep copy
*/
public abstract Json5Element deepCopy();
/**
- * provides check for verifying if this element is an array or not.
+ * Provides a check for verifying if this element is a Json5 array or not.
*
* @return true if this element is of type {@link Json5Array}, false otherwise.
*/
@@ -50,7 +96,7 @@ public boolean isJson5Array() {
}
/**
- * provides check for verifying if this element is a Json object or not.
+ * Provides a check for verifying if this element is a Json5 object or not.
*
* @return true if this element is of type {@link Json5Object}, false otherwise.
*/
@@ -59,7 +105,7 @@ public boolean isJson5Object() {
}
/**
- * provides check for verifying if this element is a primitive or not.
+ * Provides a check for verifying if this element is a primitive or not.
*
* @return true if this element is of type {@link Json5Primitive}, false otherwise.
*/
@@ -68,7 +114,7 @@ public boolean isJson5Primitive() {
}
/**
- * provides check for verifying if this element represents a null value or not.
+ * Provides a check for verifying if this element represents a null value or not.
*
* @return true if this element is of type {@link Json5Null}, false otherwise.
*/
@@ -77,226 +123,297 @@ public boolean isJson5Null() {
}
/**
- * 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
+ * Convenience method to get this element as a {@link Json5Object}. If this element is of some
+ * other type, an {@link IllegalStateException} will result. Hence it is best to use this method
* after ensuring that this element is of the desired type by calling {@link #isJson5Object()}
* first.
*
- * @return get this element as a {@link Json5Object}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link Json5Object}.
+ * @throws IllegalStateException if this element is of another type.
*/
public Json5Object getAsJson5Object() {
if (isJson5Object()) {
return (Json5Object) this;
}
- throw new IllegalStateException("Not a JSON Object: " + this);
+ throw new IllegalStateException("Not a Json5Object: " + this);
}
/**
- * convenience method to get this element as a {@link Json5Array}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
- * after ensuring that this element is of the desired type by calling {@link #isJson5Array()}
- * first.
+ * Convenience method to get this element as a {@link Json5Array}. If this element is of some other
+ * type, an {@link IllegalStateException} will result. Hence it is best to use this method after
+ * ensuring that this element is of the desired type by calling {@link #isJson5Array()} first.
*
- * @return get this element as a {@link Json5Array}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link Json5Array}.
+ * @throws IllegalStateException if this element is of another type.
*/
public Json5Array getAsJson5Array() {
if (isJson5Array()) {
return (Json5Array) this;
}
- throw new IllegalStateException("Not a JSON Array: " + this);
+ throw new IllegalStateException("Not a Json5Array: " + this);
}
/**
- * convenience method to get this element as a {@link Json5Primitive}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
+ * Convenience method to get this element as a {@link Json5Primitive}. If this element is of some
+ * other type, an {@link IllegalStateException} will result. Hence it is best to use this method
* after ensuring that this element is of the desired type by calling {@link #isJson5Primitive()}
* first.
*
- * @return get this element as a {@link Json5Primitive}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link Json5Primitive}.
+ * @throws IllegalStateException if this element is of another type.
*/
public Json5Primitive getAsJson5Primitive() {
if (isJson5Primitive()) {
return (Json5Primitive) this;
}
- throw new IllegalStateException("Not a JSON Primitive: " + this);
+ throw new IllegalStateException("Not a Json5Primitive: " + this);
}
/**
- * convenience method to get this element as a {@link Json5Null}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
- * after ensuring that this element is of the desired type by calling {@link #isJson5Null()}
- * first.
+ * Convenience method to get this element as a {@link Json5Null}. If this element is of some other
+ * type, an {@link IllegalStateException} will result. Hence it is best to use this method after
+ * ensuring that this element is of the desired type by calling {@link #isJson5Null()} first.
*
- * @return get this element as a {@link Json5Null}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link Json5Null}.
+ * @throws IllegalStateException if this element is of another type.
*/
public Json5Null getAsJson5Null() {
if (isJson5Null()) {
return (Json5Null) this;
}
- throw new IllegalStateException("Not a JSON Null: " + this);
+ throw new IllegalStateException("Not a Json5Null: " + this);
}
/**
- * convenience method to get this element as a boolean value.
+ * Convenience method to get this element as a boolean value.
*
- * @return get this element as a primitive boolean value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * boolean value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive boolean value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public boolean getAsBoolean() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a {@link Number}.
+ * Convenience method to get this element as a {@link Instant} value.
+ *
+ * @return this element as a primitive {@link Instant} value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
+ */
+ public Instant getAsInstant() {
+ throw new UnsupportedOperationException(getClass().getSimpleName());
+ }
+
+ /**
+ * Convenience method to get this element as a {@link Number}.
*
- * @return get this element as a {@link Number}.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * number.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a {@link Number}.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}, or cannot be converted to a number.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public Number getAsNumber() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a string value.
+ * Convenience method to get this element as a {@link RadixNumber}.
+ *
+ * @return this element as a {@link RadixNumber}.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}, or cannot be converted to a radix number.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
+ */
+ public RadixNumber getAsRadixNumber() {
+ throw new UnsupportedOperationException(getClass().getSimpleName());
+ }
+
+ /**
+ * Convenience method to get this element as a string value.
*
- * @return get this element as a string value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * string value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a string value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public String getAsString() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a primitive double value.
+ * Convenience method to get this element as a primitive double value.
*
- * @return get this element as a primitive double value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * double value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive double value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid double.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public double getAsDouble() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a primitive float value.
+ * Convenience method to get this element as a primitive float value.
*
- * @return get this element as a primitive float value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * float value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive float value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid float.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public float getAsFloat() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a primitive long value.
+ * Convenience method to get this element as a primitive long value.
*
- * @return get this element as a primitive long value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * long value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive long value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid long.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public long getAsLong() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a primitive integer value.
+ * Convenience method to get this element as a primitive integer value.
*
- * @return get this element as a primitive integer value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * integer value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive integer value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid integer.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public int getAsInt() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a primitive byte value.
+ * Convenience method to get this element as a primitive byte value.
*
- * @return get this element as a primitive byte value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * byte value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive byte value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid byte.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public byte getAsByte() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a {@link BigDecimal}.
+ * Convenience method to get this element as a {@link BigDecimal}.
*
- * @return get this element as a {@link BigDecimal}.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive}.
- * * @throws NumberFormatException if the element is not a valid {@link BigDecimal}.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a {@link BigDecimal}.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if this element is not a valid {@link BigDecimal}.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public BigDecimal getAsBigDecimal() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a {@link BigInteger}.
+ * Convenience method to get this element as a {@link BigInteger}.
*
- * @return get this element as a {@link BigInteger}.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive}.
- * @throws NumberFormatException if the element is not a valid {@link BigInteger}.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a {@link BigInteger}.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if this element is not a valid {@link BigInteger}.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public BigInteger getAsBigInteger() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * convenience method to get this element as a primitive short value.
+ * Convenience method to get this element as a primitive short value.
*
- * @return get this element as a primitive short value.
- * @throws ClassCastException if the element is of not a {@link Json5Primitive} and is not a valid
- * short value.
- * @throws IllegalStateException if the element is of the type {@link Json5Array} but contains
- * more than a single element.
+ * @return this element as a primitive short value.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid short.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
public short getAsShort() {
throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * Returns a simple String representation of this element.
- * For pretty-printing use {@link Json5Writer} with custom configuration options.
- * @see #toString(Json5Options)
+ * Convenience method to get this element as a primitive binary number (radix base {@code 2}) value.
+ *
+ * This is an extension that is not compliant to the official Json5 spec.
+ *
+ * @return this element as a primitive binary number value string.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid short.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
*/
- @Override
- public String toString() {
- return toString(Json5Options.DEFAULT);
+ public String getAsBinaryString() {
+ throw new UnsupportedOperationException(getClass().getSimpleName());
+
+ }
+
+ /**
+ * Convenience method to get this element as a primitive octal number (radix base {@code 8}) value.
+ *
+ * This is an extension that is not compliant to the official Json5 spec.
+ *
+ * @return this element as a primitive octal number value string.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid short.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
+ */
+ public String getAsOctalString() {
+ throw new UnsupportedOperationException(getClass().getSimpleName());
+
+ }
+
+ /**
+ * Convenience method to get this element as a primitive hex number (radix base {@code 16}) value.
+ *
+ * @return this element as a primitive hex number value string.
+ * @throws UnsupportedOperationException if this element is not a {@link Json5Primitive} or {@link
+ * Json5Array}.
+ * @throws NumberFormatException if the value contained is not a valid short.
+ * @throws IllegalStateException if this element is of the type {@link Json5Array} but contains
+ * more than a single element.
+ */
+ public String getAsHexString() {
+ throw new UnsupportedOperationException(getClass().getSimpleName());
}
/**
- * Returns the String representation of this element.
- * @param options Configured serialization behaviour
- * @return Stringified representation of this element
+ * Converts this element to a Json5 string using the provided configuration options for formatting.
+ *
+ * @param options Configuration options.
+ * @return Json5 string representation of this element.
*/
public String toString(Json5Options options) {
Objects.requireNonNull(options);
@@ -306,9 +423,54 @@ public String toString(Json5Options options) {
Json5Writer json5Writer = new Json5Writer(options, stringWriter);
json5Writer.write(this);
return stringWriter.toString();
-
} catch (IOException e) {
throw new AssertionError(e);
}
}
-}
\ No newline at end of file
+
+ /**
+ * Converts this element to a Json5 string.
+ *
+ *
+ *
+ * @see #toString(Json5Options)
+ */
+ @Override
+ public String toString() {
+ return toString(Json5Options.DEFAULT);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ Json5Element that = (Json5Element) o;
+ return Objects.equals(comment, that.comment);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(comment);
+ }
+}
diff --git a/src/main/java/de/marhali/json5/Json5Hexadecimal.java b/src/main/java/de/marhali/json5/Json5Hexadecimal.java
deleted file mode 100644
index 391a905..0000000
--- a/src/main/java/de/marhali/json5/Json5Hexadecimal.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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 java.math.BigInteger;
-import java.util.Objects;
-
-/**
- * A class representing a hexadecimal json5 value. Hex values will be stored as {@link BigInteger} internally.
- *
- * @author Marcel Haßlinger
- */
-public final class Json5Hexadecimal extends Json5Primitive {
-
- /**
- * Converts the provided hex string into it's number representation.
- * Allowed is the character representation of a hex key. Format must be: 0x..., +0x... or -0x...
- *
- * @param hex the hexadecimal value including prefix
- * @return Number representation of hexadecimal string
- */
- public static BigInteger parseHexString(String hex) {
- Objects.requireNonNull(hex);
-
- switch (hex.charAt(0)) {
- case '+': // +0x...
- return new BigInteger(hex.substring(3), 16);
- case '-': // -0x...
- return new BigInteger(hex.substring(3), 16).negate();
- default: // 0x...
- return new BigInteger(hex.substring(2), 16);
- }
- }
-
- /**
- * Converts the provided number into it's hex literal character representation.
- *
- * @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) {
- 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);
- }
-
- /**
- * 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));
- }
-
- /**
- * Constructs the string representation of the stored hex value.
- *
- * @return Hex value as character literal.
- */
- @Override
- public String getAsString() {
- return serializeHexString(super.getAsBigInteger(), false);
- }
-}
diff --git a/src/main/java/de/marhali/json5/Json5Null.java b/src/main/java/de/marhali/json5/Json5Null.java
index 1e1f2bc..a883302 100644
--- a/src/main/java/de/marhali/json5/Json5Null.java
+++ b/src/main/java/de/marhali/json5/Json5Null.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2008 Google Inc.
+ * Copyright (C) 2025 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.
@@ -21,39 +22,21 @@
*
* @author Inderjeet Singh
* @author Joel Leitch
+ * @author Marcel Haßlinger
*/
public final class Json5Null extends Json5Element {
- /**
- * Singleton for json {@code null} literal
- */
- public static final Json5Null INSTANCE = new Json5Null();
-
- /**
- * Constructor for internal use only. Use {@link #INSTANCE} instead.
- */
- private Json5Null() {}
-
- /**
- * Returns the same instance since it is an immutable value
- */
- @Override
- public Json5Null deepCopy() {
- return INSTANCE;
+ public Json5Null() {
}
- /**
- * All instances of JsonNull have the same hash code since they are indistinguishable
- */
@Override
- public int hashCode() {
- return Json5Null.class.hashCode();
+ public Json5Element deepCopy() {
+ Json5Null copy = new Json5Null();
+ copy.setComment(comment);
+ return copy;
}
- /**
- * All instances of JsonNull are the same
- */
@Override
- public boolean equals(Object other) {
- return this == other || other instanceof Json5Null;
+ public String getAsString() {
+ return "null";
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/de/marhali/json5/Json5Object.java b/src/main/java/de/marhali/json5/Json5Object.java
index 88a3341..390d1f9 100644
--- a/src/main/java/de/marhali/json5/Json5Object.java
+++ b/src/main/java/de/marhali/json5/Json5Object.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008 Google Inc.
- * Copyright (C) 2022 Marcel Haßlinger
+ * Copyright (C) 2025 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.
@@ -19,23 +19,39 @@
import de.marhali.json5.internal.LinkedTreeMap;
+import java.time.Instant;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
- * A class representing an object type in Json. An object consists of name-value pairs where names
+ * A class representing an object type in Json5. 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
* tree of Json5Elements. The member elements of this object are maintained in order they were added.
+ * This class does not support {@code null} values. If {@code null} is provided as value argument to
+ * any of the methods, it is converted to a {@link Json5Null}.
+ *
+ *
{@code Json5Object} does not implement the {@link Map} interface, but a {@code Map} view of it
+ * can be obtained with {@link #asMap()}.
+ *
+ *
See the {@link Json5} documentation for details on how to convert {@code Json5Object} and
+ * generally any {@code Json5Element} from and to Json5.
*
* @author Inderjeet Singh
* @author Joel Leitch
+ * @author Marcel Haßlinger
*/
public final class Json5Object extends Json5Element {
- private final LinkedTreeMap members =
- new LinkedTreeMap();
+ private final LinkedTreeMap members = new LinkedTreeMap<>(false);
+
+ /**
+ * Creates a new instance of a {@link Json5Object}.
+ */
+ public Json5Object() {
+ }
/**
- * Creates a deep copy of this element and all its children
+ * Creates a deep copy of this element and all its children.
*/
@Override
public Json5Object deepCopy() {
@@ -43,73 +59,90 @@ public Json5Object deepCopy() {
for (Map.Entry entry : members.entrySet()) {
result.add(entry.getKey(), entry.getValue().deepCopy());
}
+ result.setComment(comment);
return result;
}
/**
* 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
- * rooted at this node.
+ * can be an arbitrary {@link Json5Element}, thereby allowing you to build a full tree of
+ * {@link Json5Element Json5Elements} rooted at this node.
*
* @param property name of the member.
- * @param value the member object.
+ * @param value the member object.
*/
public void add(String property, Json5Element value) {
- members.put(property, value == null ? Json5Null.INSTANCE : value);
+ members.put(property, value == null ? Json5Primitive.fromNull() : value);
}
/**
- * Removes the {@code property} from this {@link Json5Object}.
+ * Removes the {@code property} from this object.
*
* @param property name of the member that should be removed.
- * @return the {@link Json5Element} object that is being removed.
+ * @return the {@link Json5Element} object that is being removed, or {@code null} if no member with
+ * this name exists.
*/
public Json5Element remove(String property) {
return members.remove(property);
}
/**
- * Convenience method to add a primitive member. The specified value is converted to a
- * Json5Primitive of String.
+ * Convenience method to add a char member. The specified value is converted to a {@link
+ * Json5Primitive} of Character.
*
* @param property name of the member.
- * @param value the string value associated with the member.
+ * @param value the char value associated with the member.
+ */
+ public void addProperty(String property, Character value) {
+ add(property, value == null ? Json5Primitive.fromNull() : Json5Primitive.fromCharacter(value));
+ }
+
+ /**
+ * Convenience method to add a string member. The specified value is converted to a {@link
+ * Json5Primitive} of String.
+ *
+ * @param property name of the member.
+ * @param value the string value associated with the member.
*/
public void addProperty(String property, String value) {
- add(property, value == null ? Json5Null.INSTANCE : new Json5String(value));
+ add(property, value == null ? Json5Primitive.fromNull() : Json5Primitive.fromString(value));
}
/**
- * Convenience method to add a primitive member. The specified value is converted to a
- * Json5Primitive of Number.
+ * Convenience method to add a number member. The specified value is converted to a {@link
+ * Json5Primitive} of Number.
*
* @param property name of the member.
- * @param value the number value associated with the member.
+ * @param value the number value associated with the member.
*/
public void addProperty(String property, Number value) {
- add(property, value == null ? Json5Null.INSTANCE : new Json5Number(value));
+ add(property, value == null ? Json5Primitive.fromNull() : Json5Primitive.fromNumber(value));
+ }
+
+ public void addProperty(String property, Number value, int radix) {
+ add(property, value == null ? Json5Primitive.fromNull() : Json5Primitive.fromNumber(value, radix));
}
/**
- * Convenience method to add a boolean member. The specified value is converted to a
- * Json5Primitive of Boolean.
+ * Convenience method to add a {@link Instant} member. The specified value is converted to a {@link
+ * Json5Primitive} of {@link Instant}.
*
* @param property name of the member.
- * @param value the number value associated with the member.
+ * @param value the {@link Instant} value associated with the member.
*/
- public void addProperty(String property, Boolean value) {
- add(property, value == null ? Json5Null.INSTANCE : new Json5Boolean(value));
+ public void addProperty(String property, Instant value) {
+ add(property, value == null ? Json5Primitive.fromNull() : Json5Primitive.fromInstant(value));
}
/**
- * Convenience method to add a char member. The specified value is converted to a
- * Json5Primitive of Character.
+ * Convenience method to add a boolean member. The specified value is converted to a {@link
+ * Json5Primitive} of Boolean.
*
* @param property name of the member.
- * @param value the number value associated with the member.
+ * @param value the boolean value associated with the member.
*/
- public void addProperty(String property, Character value) {
- add(property, value == null ? Json5Null.INSTANCE : new Json5String(value.toString()));
+ public void addProperty(String property, Boolean value) {
+ add(property, value == null ? Json5Primitive.fromNull() : Json5Primitive.fromBoolean(value));
}
/**
@@ -140,6 +173,15 @@ public int size() {
return members.size();
}
+ /**
+ * Returns true if the number of key/value pairs in the object is zero.
+ *
+ * @return true if the number of key/value pairs in the object is zero.
+ */
+ public boolean isEmpty() {
+ return members.isEmpty();
+ }
+
/**
* Convenience method to check if a member with the specified name is present in this object.
*
@@ -154,50 +196,73 @@ public boolean has(String memberName) {
* Returns the member with the specified name.
*
* @param memberName name of the member that is being requested.
- * @return the member matching the name. Null if no such member exists.
+ * @return the member matching the name, or {@code null} if no such member exists.
*/
public Json5Element get(String memberName) {
return members.get(memberName);
}
/**
- * Convenience method to get the specified member as a Json5Primitive element.
+ * Convenience method to get the specified member as a {@link Json5Primitive}.
*
* @param memberName name of the member being requested.
- * @return the Json5Primitive corresponding to the specified member.
+ * @return the {@code Json5Primitive} corresponding to the specified member, or {@code null} if no
+ * member with this name exists.
+ * @throws ClassCastException if the member is not of type {@code Json5Primitive}.
*/
public Json5Primitive getAsJson5Primitive(String memberName) {
return (Json5Primitive) members.get(memberName);
}
/**
- * Convenience method to get the specified member as a Json5Array.
+ * Convenience method to get the specified member as a {@link Json5Array}.
*
* @param memberName name of the member being requested.
- * @return the Json5Array corresponding to the specified member.
+ * @return the {@code Json5Array} corresponding to the specified member, or {@code null} if no
+ * member with this name exists.
+ * @throws ClassCastException if the member is not of type {@code Json5Array}.
*/
public Json5Array getAsJson5Array(String memberName) {
return (Json5Array) members.get(memberName);
}
/**
- * Convenience method to get the specified member as a Json5Object.
+ * Convenience method to get the specified member as a {@link Json5Object}.
*
* @param memberName name of the member being requested.
- * @return the Json5Object corresponding to the specified member.
+ * @return the {@code Json5Object} corresponding to the specified member, or {@code null} if no
+ * member with this name exists.
+ * @throws ClassCastException if the member is not of type {@code Json5Object}.
*/
public Json5Object getAsJson5Object(String memberName) {
return (Json5Object) members.get(memberName);
}
+ /**
+ * Returns a mutable {@link Map} view of this {@code Json5Object}. Changes to the {@code Map} are
+ * visible in this {@code Json5Object} and the other way around.
+ *
+ *
The {@code Map} does not permit {@code null} keys or values. Unlike {@code Json5Object}'s
+ * {@code null} handling, a {@link NullPointerException} is thrown when trying to add {@code
+ * null}. Use {@link Json5Null} for Json5 null values.
+ *
+ * @return mutable {@code Map} view
+ */
+ public Map asMap() {
+ // It is safe to expose the underlying map because it disallows null keys and values
+ return members;
+ }
+
@Override
public boolean equals(Object o) {
- return (o == this) || (o instanceof Json5Object
- && ((Json5Object) o).members.equals(members));
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ Json5Object that = (Json5Object) o;
+ return Objects.equals(members, that.members);
}
@Override
public int hashCode() {
- return members.hashCode();
+ return Objects.hash(super.hashCode(), 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
deleted file mode 100644
index c73704f..0000000
--- a/src/main/java/de/marhali/json5/Json5Options.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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;
-
-/**
- * Configuration options for Json5 parsing and serialization.
- *
- * @author Marcel Haßlinger
- */
-public class Json5Options {
-
- /**
- * Default configuration options. Allow invalid surrogate, disabled pretty-printing.
- */
- public static final Json5Options DEFAULT = new Json5OptionsBuilder()
- .allowInvalidSurrogate()
- .trailingComma()
- .prettyPrinting()
- .build();
-
- /**
- * @return Builder pattern to configure json5 behaviour.
- */
- public static Json5OptionsBuilder builder() {
- return new Json5OptionsBuilder();
- }
-
- /**
- * Whether invalid unicode surrogate pairs should be allowed
- */
- private final boolean allowInvalidSurrogates;
-
- /**
- * Whether strings should be single-quoted ({@code '}) instead of double-quoted ({@code "}).
- * This also includes all member names of {@link Json5Object}.
- */
- private final boolean quoteSingle;
-
- /**
- * Whether all Json5 values should be marked with a trailing comma ({@code ,}) or only where it is mandatory.
- */
- private final boolean trailingComma;
-
- /**
- * Defines how many spaces ({@code ' '}) should be placed before each key/value pair.
- * A factor of {@code < 1} disables pretty-printing and discards any optional whitespace characters.
- */
- private final int indentFactor;
-
- public Json5Options(boolean allowInvalidSurrogates, boolean quoteSingle, boolean trailingComma, int indentFactor) {
- this.allowInvalidSurrogates = allowInvalidSurrogates;
- this.quoteSingle = quoteSingle;
- this.trailingComma = trailingComma;
- this.indentFactor = Math.max(0, indentFactor);
- }
-
- public boolean isAllowInvalidSurrogates() {
- return allowInvalidSurrogates;
- }
-
- public boolean isQuoteSingle() {
- return quoteSingle;
- }
-
- public boolean isTrailingComma() {
- return trailingComma;
- }
-
- public int getIndentFactor() {
- return indentFactor;
- }
-}
\ 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
deleted file mode 100644
index 69dedf2..0000000
--- a/src/main/java/de/marhali/json5/Json5OptionsBuilder.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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;
-
-/**
- * Options builder to configure behaviour of json5 parsing and serialization.
- *
- * @author Marcel Haßlinger
- * @see Json5Options
- */
-public class Json5OptionsBuilder {
-
- private boolean allowInvalidSurrogates = false;
- private boolean quoteSingle = false;
- private boolean trailingComma = false;
-
- private int indentFactor = 0;
-
- /**
- * Constructs a new builder instance.
- */
- public Json5OptionsBuilder() {}
-
- /**
- * @see Json5Options#isAllowInvalidSurrogates()
- * @return Current builder instance
- */
- public Json5OptionsBuilder allowInvalidSurrogate() {
- this.allowInvalidSurrogates = true;
- return this;
- }
-
- /**
- * @see Json5Options#isQuoteSingle()
- * @return Current builder instance
- */
- public Json5OptionsBuilder quoteSingle() {
- this.quoteSingle = true;
- return this;
- }
-
- /**
- * @see Json5Options#isTrailingComma()
- * @return Current builder instance
- */
- public Json5OptionsBuilder trailingComma() {
- this.trailingComma = true;
- return this;
- }
-
- /**
- * The indentation factor enables pretty-printing and defines
- * how many spaces ( {@code ' '}) should be placed before each key/value pair.
- * A factor of {@code < 1} disables pretty-printing and discards
- * any optional whitespace characters.
- * @see Json5Options#getIndentFactor()
- * @param indentFactor Indent factor to apply
- * @return Current builder instance
- */
- public Json5OptionsBuilder indentFactor(int indentFactor) {
- this.indentFactor = indentFactor;
- return this;
- }
-
- /**
- * Configures to output Json5 that fits in a page for pretty printing. This option only affects Json serialization.
- * Applies an indent factor of 2.
- * @see #indentFactor(int)
- * @return Current builder instance
- */
- public Json5OptionsBuilder prettyPrinting() {
- this.indentFactor = 2;
- return this;
- }
-
- /**
- * @return Configured {@link Json5Options}
- */
- public Json5Options build() {
- return new Json5Options(allowInvalidSurrogates, quoteSingle, trailingComma, indentFactor);
- }
-}
diff --git a/src/main/java/de/marhali/json5/Json5Primitive.java b/src/main/java/de/marhali/json5/Json5Primitive.java
index f442a53..316f7d1 100644
--- a/src/main/java/de/marhali/json5/Json5Primitive.java
+++ b/src/main/java/de/marhali/json5/Json5Primitive.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008 Google Inc.
- * Copyright (C) 2022 Marcel Haßlinger
+ * Copyright (C) 2022 - 2025 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.
@@ -18,72 +18,215 @@
package de.marhali.json5;
import de.marhali.json5.internal.LazilyParsedNumber;
+import de.marhali.json5.internal.NumberLimits;
+import de.marhali.json5.internal.RadixNumber;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.time.Instant;
import java.util.Objects;
/**
- * A class representing a Json primitive value. A primitive value
- * is either a String, a Hexadecimal, a Java primitive, or a Java primitive
- * wrapper type.
+ * A class representing a Json5 primitive value. A primitive value is either a String, a Java
+ * primitive, or a Java primitive wrapper type.
+ *
+ *
See the {@link Json5Element} documentation for details on how to convert {@code Json5Primitive}
+ * and generally any {@code Json5Element} from and to Json5.
*
- * @author Marcel Haßlinger
* @author Inderjeet Singh
* @author Joel Leitch
+ * @author Marcel Haßlinger
*/
-public abstract class Json5Primitive extends Json5Element {
+public final class Json5Primitive extends Json5Element {
+
+ private final Object value;
/**
- * Quick creator for a primitive with boolean value.
- * @param value Boolean value to apply.
- * @return Corresponding primitive with provided value.
+ * Create a primitive containing a {@code null} value.
+ *
+ * @return New {@link Json5Null} value
*/
- public static Json5Primitive of(Boolean value) {
- return new Json5Boolean(value);
+ public static Json5Null fromNull() {
+ return new Json5Null();
}
/**
- * Quick creator for a primitive with number value.
- * @param value Number value to apply.
- * @return Corresponding primitive with provided value.
+ * Create a primitive containing a boolean value.
+ *
+ * @param bool the value to create the primitive with.
+ * @return Json5Primitive containing the provided {@link Boolean} value
*/
- public static Json5Primitive of(Number value) {
- return new Json5Number(value);
+ public static Json5Primitive fromBoolean(Boolean bool) {
+ return new Json5Primitive(Objects.requireNonNull(bool));
}
/**
- * 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.
+ * Create a primitive containing a {@link Instant} value.
+ *
+ * @param instant the value to create the primitive with.
+ * @return Json5Primitive containing the provided {@link Instant} value
+ *
+ * This is an extension that is not compliant to the official Json5 spec.
*/
- public static Json5Primitive of(String value, boolean hexadecimal) {
- return hexadecimal ? new Json5Hexadecimal(value) : new Json5String(value);
+ public static Json5Primitive fromInstant(Instant instant) {
+ return new Json5Primitive(Objects.requireNonNull(instant));
}
/**
- * Quick creator for a primitive with string value.
- * @param value String value to apply.
- * @return Corresponding primitive with provided value.
+ * Creates a primitive containing a {@link Number} with specified radix base.
+ * If a radix base of {@code 2}, {@code 8} or {@code 16} is set,
+ * this method will ensure that the underlying number implementation is a {@link BigInteger}.
+ *
+ * @param number The number
+ * @param radix Radix base
+ * @return Json5Primitive containing the provided {@link Number} with specified radix base
*/
- public static Json5Primitive of(String value) {
- return new Json5String(value);
+ public static Json5Primitive fromNumber(Number number, int radix) {
+ Objects.requireNonNull(number);
+
+ if ((radix == 2 || radix == 8 || radix == 16) && !(number instanceof BigInteger)) {
+ // Ensure that every binary, octal or hex number is stored as a big integer
+ return new Json5Primitive(new RadixNumber(BigInteger.valueOf(number.longValue()), radix));
+ }
+
+ return new Json5Primitive(new RadixNumber(number, radix));
}
- protected final Object value;
+ /**
+ * Create a primitive containing a decimal {@link Number} (radix base {@code 10}).
+ *
+ * @param number the value to create the primitive with.
+ * @return Json5Primitive containing the provided {@link Number} with radix base {@code 10}
+ */
+ public static Json5Primitive fromNumber(Number number) {
+ return Json5Primitive.fromNumber(Objects.requireNonNull(number), 10);
+ }
- public Json5Primitive(Object value) {
- this.value = Objects.requireNonNull(value);
+ /**
+ * Create a primitive containing a binary number (radix base {@code 2}).
+ * For example {@code +0b1010...}, {@code 0b1010...} or {@code -0b1010...}.
+ *
+ * This is an extension that is not compliant to the official Json5 spec.
+ *
+ * @param binaryString the value to create the primitive with.
+ * @return Json5Primitive containing the provided binary number with radix base {@code 2}
+ */
+ public static Json5Primitive fromBinaryString(String binaryString) {
+ Objects.requireNonNull(binaryString);
+
+ BigInteger hexInteger;
+
+ switch (binaryString.charAt(0)) {
+ case '+': // +0b...
+ hexInteger = new BigInteger(binaryString.substring(3), 2);
+ break;
+ case '-': // -0b...
+ hexInteger = new BigInteger(binaryString.substring(3), 2).negate();
+ break;
+ default: // 0b...
+ hexInteger = new BigInteger(binaryString.substring(2), 2);
+ break;
+ }
+
+ return new Json5Primitive(new RadixNumber(hexInteger, 2));
+ }
+
+ /**
+ * Create a primitive containing an octal number (radix base {@code 8}).
+ * For example {@code +0o107...}, {@code 0o107...} or {@code -0o107...}.
+ *
+ * This is an extension that is not compliant to the official Json5 spec.
+ *
+ * @param octalString the value to create the primitive with.
+ * @return Json5Primitive containing the provided octal number with radix base {@code 8}
+ */
+ public static Json5Primitive fromOctalString(String octalString) {
+ Objects.requireNonNull(octalString);
+
+ BigInteger hexInteger;
+
+ switch (octalString.charAt(0)) {
+ case '+': // +0b...
+ hexInteger = new BigInteger(octalString.substring(3), 8);
+ break;
+ case '-': // -0b...
+ hexInteger = new BigInteger(octalString.substring(3), 8).negate();
+ break;
+ default: // 0b...
+ hexInteger = new BigInteger(octalString.substring(2), 8);
+ break;
+ }
+
+ return new Json5Primitive(new RadixNumber(hexInteger, 8));
+ }
+
+ /**
+ * Create a primitive containing a binary number (radix base {@code 16}).
+ * For example {@code +0x09af...}, {@code 0x09af...} or {@code -0x09af...}.
+ *
+ * @param hexString the value to create the primitive with.
+ * @return Json5Primitive containing the provided hex number with radix base {@code 16}
+ */
+ public static Json5Primitive fromHexString(String hexString) {
+ Objects.requireNonNull(hexString);
+
+ BigInteger hexInteger;
+
+ switch (hexString.charAt(0)) {
+ case '+': // +0x...
+ hexInteger = new BigInteger(hexString.substring(3), 16);
+ break;
+ case '-': // -0x...
+ hexInteger = new BigInteger(hexString.substring(3), 16).negate();
+ break;
+ default: // 0x...
+ hexInteger = new BigInteger(hexString.substring(2), 16);
+ break;
+ }
+
+ return new Json5Primitive(new RadixNumber(hexInteger, 16));
+ }
+
+ /**
+ * Create a primitive containing a String value.
+ *
+ * @param string the value to create the primitive with.
+ * @return Json5Primitive containing the provided {@link String}
+ */
+ public static Json5Primitive fromString(String string) {
+ return new Json5Primitive(Objects.requireNonNull(string));
+ }
+
+ /**
+ * Create a primitive containing a character. The character is turned into a one character String
+ * since Json5 only supports String.
+ *
+ * @param c the value to create the primitive with.
+ * @return Json5Primitive containing the provided {@link Character}
+ */
+ public static Json5Primitive fromCharacter(Character c) {
+ // convert characters to strings since in Json5, characters are represented as a single
+ // character string
+ return new Json5Primitive(Objects.requireNonNull(c).toString());
+ }
+
+ /**
+ * Internal constructor with primitive value
+ *
+ * @param value Internal value
+ */
+ private Json5Primitive(Object value) {
+ this.value = value;
}
/**
* Returns the same value as primitives are immutable.
*/
@Override
- public Json5Element deepCopy() {
- return this;
+ public Json5Primitive deepCopy() {
+ Json5Primitive copy = new Json5Primitive(value);
+ copy.setComment(comment);
+ return copy;
}
/**
@@ -96,37 +239,129 @@ public boolean isBoolean() {
}
/**
- * convenience method to get this element as a boolean value.
- *
- * @return get this element as a primitive boolean value.
+ * Convenience method to get this element as a boolean value. If this primitive {@linkplain
+ * #isBoolean() is not a boolean}, the string value is parsed using {@link
+ * Boolean#parseBoolean(String)}. This means {@code "true"} (ignoring case) is considered {@code
+ * true} and any other value is considered {@code false}.
*/
@Override
public boolean getAsBoolean() {
if (isBoolean()) {
- return ((Boolean) value).booleanValue();
+ return (Boolean) value;
}
// Check to see if the value as a String is "true" in any case.
return Boolean.parseBoolean(getAsString());
}
+ /**
+ * Check whether this primitive contains a {@link Instant} value.
+ *
+ * @return true if this primitive contains a {@link Instant} value, false otherwise.
+ */
+ public boolean isInstant() {
+ return value instanceof Instant;
+ }
+
+ @Override
+ public Instant getAsInstant() {
+ if (isInstant()) {
+ return (Instant) value;
+ } else if (isString()) {
+ return Instant.parse((String) value);
+ } else if (isNumber()) {
+ var radixNumber = getAsRadixNumber();
+ var number = radixNumber.getNumber();
+
+ if (number instanceof Byte || number instanceof Short || number instanceof Integer || number instanceof Long)
+ return Instant.ofEpochSecond((long) value);
+
+ if (number instanceof BigInteger)
+ return Instant.ofEpochSecond(((BigInteger) number).longValueExact());
+ }
+ throw new UnsupportedOperationException("Primitive is neither a number nor a string");
+ }
+
/**
* Check whether this primitive contains a Number.
*
* @return true if this primitive contains a Number, false otherwise.
*/
public boolean isNumber() {
- return value instanceof Number;
+ return value instanceof RadixNumber;
+ }
+
+ @Override
+ public RadixNumber getAsRadixNumber() {
+ if (isNumber()) {
+ return (RadixNumber) value;
+ }
+ throw new UnsupportedOperationException("Primitive is not a number");
+ }
+
+ public int getNumberRadix() {
+ return getAsRadixNumber().getRadix();
+ }
+
+ public boolean isBinaryNumber() {
+ return isNumber() && getNumberRadix() == 2;
+ }
+
+ public boolean isOctalNumber() {
+ return isNumber() && getNumberRadix() == 8;
+ }
+
+ public boolean isHexNumber() {
+ return isNumber() && getNumberRadix() == 16;
}
/**
- * convenience method to get this element as a Number.
+ * Convenience method to get this element as a {@link Number}. If this primitive {@linkplain
+ * #isString() is a string}, a lazily parsed {@code Number} is constructed which parses the string
+ * when any of its methods are called (which can lead to a {@link NumberFormatException}).
*
- * @return get this element as a Number.
- * @throws NumberFormatException if the value contained is not a valid Number.
+ * @throws UnsupportedOperationException if this primitive is neither a number nor a string.
*/
@Override
public Number getAsNumber() {
- return value instanceof String ? new LazilyParsedNumber((String) value) : (Number) value;
+ if (isNumber()) {
+ return getAsRadixNumber().getNumber();
+ } else if (isString()) {
+ return new LazilyParsedNumber((String) value);
+ }
+ throw new UnsupportedOperationException("Primitive is neither a number nor a string");
+ }
+
+ @Override
+ public String getAsBinaryString() {
+ BigInteger bigInteger = getAsBigInteger();
+
+ if (bigInteger.signum() >= 0) {
+ return "0b" + bigInteger.toString(2);
+ } else {
+ return "-0b" + bigInteger.abs().toString(2);
+ }
+ }
+
+ @Override
+ public String getAsOctalString() {
+ BigInteger bigInteger = getAsBigInteger();
+
+ if (bigInteger.signum() >= 0) {
+ return "0o" + bigInteger.toString(8);
+ } else {
+ return "-0o" + bigInteger.abs().toString(8);
+ }
+ }
+
+ @Override
+ public String getAsHexString() {
+ BigInteger bigInteger = getAsBigInteger();
+
+ if (bigInteger.signum() >= 0) {
+ return "0x" + bigInteger.toString(16);
+ } else {
+ return "-0x" + bigInteger.abs().toString(16);
+ }
}
/**
@@ -138,27 +373,32 @@ public boolean isString() {
return value instanceof String;
}
- /**
- * convenience method to get this element as a String.
- *
- * @return get this element as a String.
- */
+ // Don't add Javadoc, inherit it from super implementation; no exceptions are thrown here
@Override
public String getAsString() {
- if (isNumber()) {
- return getAsNumber().toString();
+ if (isString()) {
+ return (String) value;
+ } else if (isInstant()) {
+ return ((Instant) value).toString();
} else if (isBoolean()) {
return ((Boolean) value).toString();
- } else {
- return (String) value;
+ } else if (isNumber()) {
+ if (isBinaryNumber()) {
+ return getAsBinaryString();
+ } else if (isOctalNumber()) {
+ return getAsOctalString();
+ } else if (isHexNumber()) {
+ return getAsHexString();
+ } else {
+ return getAsNumber().toString();
+ }
}
+
+ throw new AssertionError("Unexpected value type: " + value.getClass());
}
/**
- * convenience method to get this element as a primitive double.
- *
- * @return get this element as a primitive double.
- * @throws NumberFormatException if the value contained is not a valid double.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public double getAsDouble() {
@@ -166,33 +406,39 @@ public double getAsDouble() {
}
/**
- * convenience method to get this element as a {@link BigDecimal}.
- *
- * @return get this element as a {@link BigDecimal}.
- * @throws NumberFormatException if the value contained is not a valid {@link BigDecimal}.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public BigDecimal getAsBigDecimal() {
- return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
+ if (isNumber()) {
+ var number = getAsRadixNumber().getNumber();
+ if (number instanceof BigDecimal) {
+ return (BigDecimal) number;
+ }
+ }
+
+ return NumberLimits.parseBigDecimal(getAsString());
}
/**
- * 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}.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public BigInteger getAsBigInteger() {
- return value instanceof BigInteger ?
- (BigInteger) value : new BigInteger(value.toString());
+ if (isNumber()) {
+ var number = getAsRadixNumber().getNumber();
+ if (number instanceof BigInteger) {
+ if (isIntegral(this)) {
+ return BigInteger.valueOf(number.longValue());
+ }
+ }
+ }
+
+ return NumberLimits.parseBigInteger(getAsString());
}
/**
- * convenience method to get this element as a float.
- *
- * @return get this element as a float.
- * @throws NumberFormatException if the value contained is not a valid float.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public float getAsFloat() {
@@ -200,10 +446,10 @@ public float getAsFloat() {
}
/**
- * convenience method to get this element as a primitive long.
+ * Convenience method to get this element as a primitive long.
*
- * @return get this element as a primitive long.
- * @throws NumberFormatException if the value contained is not a valid long.
+ * @return this element as a primitive long.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public long getAsLong() {
@@ -211,10 +457,7 @@ public long getAsLong() {
}
/**
- * convenience method to get this element as a primitive short.
- *
- * @return get this element as a primitive short.
- * @throws NumberFormatException if the value contained is not a valid short value.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public short getAsShort() {
@@ -222,16 +465,16 @@ public short getAsShort() {
}
/**
- * convenience method to get this element as a primitive integer.
- *
- * @return get this element as a primitive integer.
- * @throws NumberFormatException if the value contained is not a valid integer.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public int getAsInt() {
return isNumber() ? getAsNumber().intValue() : Integer.parseInt(getAsString());
}
+ /**
+ * @throws NumberFormatException {@inheritDoc}
+ */
@Override
public byte getAsByte() {
return isNumber() ? getAsNumber().byteValue() : Byte.parseByte(getAsString());
@@ -239,55 +482,29 @@ public byte getAsByte() {
@Override
public int hashCode() {
- if (value == null) {
- return 31;
- }
- // Using recommended hashing algorithm from Effective Java for longs and doubles
- if (isIntegral(this)) {
- long value = getAsNumber().longValue();
- return (int) (value ^ (value >>> 32));
- }
- if (value instanceof Number) {
- long value = Double.doubleToLongBits(getAsNumber().doubleValue());
- return (int) (value ^ (value >>> 32));
- }
- return value.hashCode();
+ return Objects.hash(super.hashCode(), value);
}
@Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null || getClass() != obj.getClass()) {
- return false;
- }
- Json5Primitive other = (Json5Primitive)obj;
- if (value == null) {
- return other.value == null;
- }
- if (isIntegral(this) && isIntegral(other)) {
- return getAsNumber().longValue() == other.getAsNumber().longValue();
- }
- if (value instanceof Number && other.value instanceof Number) {
- double a = getAsNumber().doubleValue();
- // Java standard types other than double return true for two NaN. So, need
- // special handling for double.
- double b = other.getAsNumber().doubleValue();
- return a == b || (Double.isNaN(a) && Double.isNaN(b));
- }
- return value.equals(other.value);
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ Json5Primitive that = (Json5Primitive) o;
+ return Objects.equals(value, that.value);
}
/**
- * Returns true if the specified number is an integral type
- * (Long, Integer, Short, Byte, BigInteger)
+ * Returns true if the specified number is an integral type (Long, Integer, Short, Byte,
+ * BigInteger)
*/
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;
+ if (primitive.value instanceof RadixNumber) {
+ Number number = ((RadixNumber) primitive.value).getNumber();
+ return number instanceof BigInteger
+ || number instanceof Long
+ || number instanceof Integer
+ || 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/config/DigitSeparatorStrategy.java
similarity index 57%
rename from src/main/java/de/marhali/json5/Json5String.java
rename to src/main/java/de/marhali/json5/config/DigitSeparatorStrategy.java
index d774bd3..a1f9c46 100644
--- a/src/main/java/de/marhali/json5/Json5String.java
+++ b/src/main/java/de/marhali/json5/config/DigitSeparatorStrategy.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 Marcel Haßlinger
+ * Copyright (C) 2022 - 2025 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.
@@ -14,15 +14,28 @@
* limitations under the License.
*/
-package de.marhali.json5;
+package de.marhali.json5.config;
/**
- * A class representing a json5 string value.
+ * An enum containing all supported behaviors for handling digit separators.
*
* @author Marcel Haßlinger
*/
-public final class Json5String extends Json5Primitive {
- public Json5String(String string) {
- super(string);
- }
+public enum DigitSeparatorStrategy {
+
+ /**
+ * Expect no digit separators
+ */
+ NONE,
+
+ /**
+ * Uses Java-style digit separators (e.g. {@code 123_456}).
+ */
+ JAVA_STYLE,
+
+ /**
+ * Uses C-style digit separators (e.g. {@code 123'456}).
+ */
+ C_STYLE,
+
}
diff --git a/src/main/java/de/marhali/json5/config/DuplicateKeyStrategy.java b/src/main/java/de/marhali/json5/config/DuplicateKeyStrategy.java
new file mode 100644
index 0000000..70df845
--- /dev/null
+++ b/src/main/java/de/marhali/json5/config/DuplicateKeyStrategy.java
@@ -0,0 +1,54 @@
+/*
+ * MIT License
+ *
+ * Copyright (C) 2021 SyntaxError404
+ * Copyright (C) 2025 Marcel Haßlinger
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package de.marhali.json5.config;
+
+/**
+ * An enum containing all supported behaviors for duplicate keys
+ *
+ * @author SyntaxError404
+ * @author Marcel Haßlinger
+ */
+public enum DuplicateKeyStrategy {
+
+ /**
+ * Throws an {@link de.marhali.json5.exception.Json5Exception exception} when a key
+ * is encountered multiple times within the same object
+ */
+ UNIQUE,
+
+ /**
+ * Only the last encountered value is significant,
+ * all previous occurrences are silently discarded
+ */
+ LAST_WINS,
+
+ /**
+ * Wraps duplicate values inside an {@link de.marhali.json5.Json5Array array},
+ * effectively treating them as if they were declared as one
+ */
+ DUPLICATE
+
+}
diff --git a/src/main/java/de/marhali/json5/config/Json5Options.java b/src/main/java/de/marhali/json5/config/Json5Options.java
new file mode 100644
index 0000000..0c3df92
--- /dev/null
+++ b/src/main/java/de/marhali/json5/config/Json5Options.java
@@ -0,0 +1,573 @@
+/*
+ * MIT License
+ *
+ * Copyright (C) 2021 SyntaxError404
+ * Copyright (C) 2022 - 2025 Marcel Haßlinger
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package de.marhali.json5.config;
+
+import de.marhali.json5.Json5Array;
+import de.marhali.json5.Json5Object;
+
+import java.util.Objects;
+
+/**
+ * Definition of all configuration options for parsing and writing Json5 data.
+ *
+ * @author SyntaxError404
+ * @author Marcel Haßlinger
+ */
+public final class Json5Options {
+
+ /**
+ * Whether instants should be stringifyed as unix timestamps.
+ * If this is {@code false}, instants will be stringifyed as strings
+ * (according to RFC 3339, Section 5.6).
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean stringifyUnixInstants;
+
+ /**
+ * Whether stringifying should only yield ASCII strings.
+ * All non-ASCII characters will be converted to their
+ * Unicode escape sequence (\uXXXX).
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean stringifyAscii;
+
+ /**
+ * Whether {@code NaN} should be allowed as a number
+ */
+ private final boolean allowNaN;
+
+ /**
+ * Whether {@code Infinity} should be allowed as a number.
+ * This applies to both {@code +Infinity} and {@code -Infinity}
+ */
+ private final boolean allowInfinity;
+
+ /**
+ * Whether invalid unicode surrogate pairs should be allowed
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean allowInvalidSurrogates;
+
+ /**
+ * Whether strings should be single-quoted ({@code '}) instead of double-quoted ({@code "}).
+ * This also includes a {@link Json5Object JSON5Object's} member names
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean quoteSingle;
+
+ /**
+ * Whether member names of {@link Json5Object Json5Object's} should be quoteless.
+ * E.g. { enabled: true } instead of { "enabled": true }.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean quoteless;
+
+ /**
+ * Whether binary literals ({@code 0b10101...}) should be allowed
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean allowBinaryLiterals;
+
+ /**
+ * Whether octal literals ({@code 0o567...}) should be allowed
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean allowOctalLiterals;
+
+ /**
+ * Whether hexadecimal floating-point literals (e.g. {@code 0xA.BCp+12}) should be allowed
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean allowHexFloatingLiterals;
+
+ /**
+ * Whether 32-bit unicode escape sequences ({@code \U00123456}) should be allowed
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean allowLongUnicodeEscapes;
+
+ /**
+ * Specifies whether trailing data should be allowed.
+ * If {@code false}, parsing the following will produce an error
+ * due to the trailing {@code abc}:
+ *
+ *
{ }abc
+ *
+ * If {@code true}, however, this will be interpreted as an empty
+ * {@link Json5Object} and any trailing will be ignored.
+ *
+ * Whitespace never counts as trailing data.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean allowTrailingData;
+
+ /**
+ * Specifies whether comments on {@link de.marhali.json5.Json5Element Json5Element's} should be parsed.
+ * If {@code false}, no comments will be parsed.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final boolean parseComments;
+
+ /**
+ * Specifies whether comments on {@link de.marhali.json5.Json5Element Json5Element's} should be written.
+ * If {@code false}, no set comments will be written.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean writeComments;
+
+ /**
+ * Specifies whether to apply trailing commas whenever possible or not.
+ * If {@code false}, commas are only written when necessary.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean trailingComma;
+
+ /**
+ * Specifies whether a final newline (empty line) is appended at the end of written Json5 data.
+ * If {@code true}, this option inserts the final newline after a root {@link Json5Object} or {@link Json5Array}.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final boolean insertFinalNewline;
+
+ /**
+ * Specifies the behaviour for digit separator's on numbers.
+ *
+ * This option applies to both {@link de.marhali.json5.stream.Json5Parser parsing} and {@link de.marhali.json5.stream.Json5Writer writing}.
+ */
+ private final DigitSeparatorStrategy digitSeparatorStrategy;
+
+ /**
+ * Specifies the behaviour when the same key is encountered multiple times within the same {@link Json5Object}
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Parser parser}-only option
+ */
+ private final DuplicateKeyStrategy duplicateBehaviour;
+
+
+ /**
+ * Defines the amount of whitespace's to use for the indentation of {@link Json5Object}'s or {@link Json5Array}'s.
+ * A factor of {@code < 1} disables pretty-printing and discards any optional whitespace characters.
+ *
+ * This is a {@link de.marhali.json5.stream.Json5Writer writer}-only option
+ */
+ private final int indentFactor;
+
+ /**
+ * Configure options using the builder pattern.
+ *
+ * @return builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Recommended default configuration options.
+ *
+ * Defaults:
+ *
+ *
allowNaN: true
+ *
allowInfinity: true
+ *
allowInvalidSurrogates: true
+ *
quoteless: true
+ *
parseComments: true
+ *
writeComments: true
+ *
trailingComma: true
+ *
digitSeparatorStrategy: NONE
+ *
duplicateKeyStrategy: UNIQUE
+ *
prettyPrinting: true
+ *
+ */
+ public static Json5Options DEFAULT = builder()
+ .allowNaN()
+ .allowInfinity()
+ .allowInvalidSurrogates()
+ .quoteless()
+ .parseComments()
+ .writeComments()
+ .trailingComma()
+ .digitSeparatorStrategy(DigitSeparatorStrategy.NONE)
+ .duplicateKeyStrategy(DuplicateKeyStrategy.UNIQUE)
+ .prettyPrinting()
+ .build();
+
+ private Json5Options(Builder builder) {
+ this.stringifyUnixInstants = builder.stringifyUnixInstants;
+ this.stringifyAscii = builder.stringifyAscii;
+ this.allowNaN = builder.allowNaN;
+ this.allowInfinity = builder.allowInfinity;
+ this.allowInvalidSurrogates = builder.allowInvalidSurrogates;
+ this.quoteSingle = builder.quoteSingle;
+ this.quoteless = builder.quoteless;
+ this.allowBinaryLiterals = builder.allowBinaryLiterals;
+ this.allowOctalLiterals = builder.allowOctalLiterals;
+ this.allowHexFloatingLiterals = builder.allowHexFloatingLiterals;
+ this.allowLongUnicodeEscapes = builder.allowLongUnicodeEscapes;
+ this.allowTrailingData = builder.allowTrailingData;
+ this.parseComments = builder.parseComments;
+ this.writeComments = builder.writeComments;
+ this.trailingComma = builder.trailingComma;
+ this.insertFinalNewline = builder.insertFinalNewline;
+ this.digitSeparatorStrategy = builder.digitSeparatorStrategy;
+ this.duplicateBehaviour = builder.duplicateKeyStrategy;
+ this.indentFactor = builder.indentFactor;
+ }
+
+ public boolean isStringifyUnixInstants() {
+ return stringifyUnixInstants;
+ }
+
+ public boolean isStringifyAscii() {
+ return stringifyAscii;
+ }
+
+ public boolean isAllowNaN() {
+ return allowNaN;
+ }
+
+ public boolean isAllowInfinity() {
+ return allowInfinity;
+ }
+
+ public boolean isAllowInvalidSurrogates() {
+ return allowInvalidSurrogates;
+ }
+
+ public boolean isQuoteSingle() {
+ return quoteSingle;
+ }
+
+ public boolean isQuoteless() {
+ return quoteless;
+ }
+
+ public boolean isAllowBinaryLiterals() {
+ return allowBinaryLiterals;
+ }
+
+ public boolean isAllowOctalLiterals() {
+ return allowOctalLiterals;
+ }
+
+ public boolean isAllowHexFloatingLiterals() {
+ return allowHexFloatingLiterals;
+ }
+
+ public boolean isAllowLongUnicodeEscapes() {
+ return allowLongUnicodeEscapes;
+ }
+
+ public boolean isAllowTrailingData() {
+ return allowTrailingData;
+ }
+
+ public boolean isWriteComments() {
+ return writeComments;
+ }
+
+ public boolean isParseComments() {
+ return parseComments;
+ }
+
+ public boolean isTrailingComma() {
+ return trailingComma;
+ }
+
+ public boolean isInsertFinalNewline() {
+ return insertFinalNewline;
+ }
+
+ public DigitSeparatorStrategy getDigitSeparatorStrategy() {
+ return digitSeparatorStrategy;
+ }
+
+ public DuplicateKeyStrategy getDuplicateBehaviour() {
+ return duplicateBehaviour;
+ }
+
+ public int getIndentFactor() {
+ return indentFactor;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ Json5Options that = (Json5Options) o;
+ return stringifyUnixInstants == that.stringifyUnixInstants && stringifyAscii == that.stringifyAscii && allowNaN == that.allowNaN && allowInfinity == that.allowInfinity && allowInvalidSurrogates == that.allowInvalidSurrogates && quoteSingle == that.quoteSingle && quoteless == that.quoteless && allowBinaryLiterals == that.allowBinaryLiterals && allowOctalLiterals == that.allowOctalLiterals && allowHexFloatingLiterals == that.allowHexFloatingLiterals && allowLongUnicodeEscapes == that.allowLongUnicodeEscapes && allowTrailingData == that.allowTrailingData && parseComments == that.parseComments && writeComments == that.writeComments && trailingComma == that.trailingComma && insertFinalNewline == that.insertFinalNewline && indentFactor == that.indentFactor && digitSeparatorStrategy == that.digitSeparatorStrategy && duplicateBehaviour == that.duplicateBehaviour;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(stringifyUnixInstants, stringifyAscii, allowNaN, allowInfinity, allowInvalidSurrogates, quoteSingle, quoteless, allowBinaryLiterals, allowOctalLiterals, allowHexFloatingLiterals, allowLongUnicodeEscapes, allowTrailingData, parseComments, writeComments, trailingComma, insertFinalNewline, digitSeparatorStrategy, duplicateBehaviour, indentFactor);
+ }
+
+ public static final class Builder {
+ private boolean stringifyUnixInstants = false;
+ private boolean stringifyAscii = false;
+ private boolean allowNaN = false;
+ private boolean allowInfinity = false;
+ private boolean allowInvalidSurrogates = false;
+ private boolean quoteSingle = false;
+ private boolean quoteless = false;
+ private boolean allowBinaryLiterals = false;
+ private boolean allowOctalLiterals = false;
+ private boolean allowHexFloatingLiterals = false;
+ private boolean allowLongUnicodeEscapes = false;
+ private boolean allowTrailingData = false;
+ private boolean parseComments = false;
+ private boolean writeComments = false;
+ private boolean trailingComma = false;
+ private boolean insertFinalNewline = false;
+ private DigitSeparatorStrategy digitSeparatorStrategy = DigitSeparatorStrategy.NONE;
+ private DuplicateKeyStrategy duplicateKeyStrategy = DuplicateKeyStrategy.UNIQUE;
+ private int indentFactor = 0;
+
+ private Builder() {
+ }
+
+ /**
+ * @return built {@link Json5Options}
+ */
+ public Json5Options build() {
+ return new Json5Options(this);
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#stringifyUnixInstants
+ */
+ public Builder stringifyUnixInstants() {
+ this.stringifyUnixInstants = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#stringifyAscii
+ */
+ public Builder stringifyAscii() {
+ this.stringifyAscii = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowNaN
+ */
+ public Builder allowNaN() {
+ this.allowNaN = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowInfinity
+ */
+ public Builder allowInfinity() {
+ this.allowInfinity = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowInvalidSurrogates
+ */
+ public Builder allowInvalidSurrogates() {
+ this.allowInvalidSurrogates = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#quoteSingle
+ */
+ public Builder quoteSingle() {
+ this.quoteSingle = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#quoteless
+ */
+ public Builder quoteless() {
+ this.quoteless = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowBinaryLiterals
+ */
+ public Builder allowBinaryLiterals() {
+ this.allowBinaryLiterals = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowOctalLiterals
+ */
+ public Builder allowOctalLiterals() {
+ this.allowOctalLiterals = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowHexFloatingLiterals
+ */
+ public Builder allowHexFloatingLiterals() {
+ this.allowHexFloatingLiterals = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowLongUnicodeEscapes
+ */
+ public Builder allowLongUnicodeEscapes() {
+ this.allowLongUnicodeEscapes = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#allowTrailingData
+ */
+ public Builder allowTrailingData() {
+ this.allowTrailingData = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#parseComments
+ */
+ public Builder parseComments() {
+ this.parseComments = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#writeComments
+ */
+ public Builder writeComments() {
+ this.writeComments = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#trailingComma
+ */
+ public Builder trailingComma() {
+ this.trailingComma = true;
+ return this;
+ }
+
+ /**
+ * @return builder
+ * @see Json5Options#insertFinalNewline
+ */
+ public Builder insertFinalNewline() {
+ this.insertFinalNewline = true;
+ return this;
+ }
+
+ /**
+ * @param digitSeparatorStrategy Strategy to apply
+ * @return builder
+ * @see Json5Options#digitSeparatorStrategy
+ */
+ public Builder digitSeparatorStrategy(DigitSeparatorStrategy digitSeparatorStrategy) {
+ this.digitSeparatorStrategy = digitSeparatorStrategy;
+ return this;
+ }
+
+ /**
+ * @param duplicateKeyStrategy Strategy to apply
+ * @return builder
+ * @see Json5Options#duplicateBehaviour
+ */
+ public Builder duplicateKeyStrategy(DuplicateKeyStrategy duplicateKeyStrategy) {
+ this.duplicateKeyStrategy = duplicateKeyStrategy;
+ return this;
+ }
+
+ /**
+ * @param indentFactor Indent factor to apply
+ * @return builder
+ * @see Json5Options#indentFactor
+ */
+ public Builder indentFactor(int indentFactor) {
+ this.indentFactor = indentFactor;
+ return this;
+ }
+
+ /**
+ * Configures pretty printing using 2 whitespaces for serialization (writing).
+ * Shorthand for {@code indentFactor(2)}.
+ *
+ * @return builder
+ * @see Json5Options#indentFactor
+ * @see #indentFactor(int)
+ */
+ public Builder prettyPrinting() {
+ return indentFactor(2);
+ }
+ }
+}
diff --git a/src/main/java/de/marhali/json5/exception/Json5Exception.java b/src/main/java/de/marhali/json5/exception/Json5Exception.java
index faa04cf..2a47f7e 100644
--- a/src/main/java/de/marhali/json5/exception/Json5Exception.java
+++ b/src/main/java/de/marhali/json5/exception/Json5Exception.java
@@ -59,10 +59,10 @@ public Json5Exception(Throwable cause) {
* 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
*/
public Json5Exception(String message, Throwable cause) {
super(message, cause);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/de/marhali/json5/internal/EcmaScriptIdentifier.java b/src/main/java/de/marhali/json5/internal/EcmaScriptIdentifier.java
new file mode 100644
index 0000000..403a3b1
--- /dev/null
+++ b/src/main/java/de/marhali/json5/internal/EcmaScriptIdentifier.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2025 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.internal;
+
+/**
+ * @author Marcel Haßlinger
+ */
+public class EcmaScriptIdentifier {
+ // Zero Width Non-Joiner / Joiner
+ private static final int ZWNJ = 0x200C;
+ private static final int ZWJ = 0x200D;
+
+ private EcmaScriptIdentifier() {
+ }
+
+ /**
+ * Checks whether the provided {@link String} is a valid ES5.1 IdentifierName.
+ *
+ * @param raw Input to check
+ * @return true if valid identifier, otherwise false
+ * @see https://262.ecma-international.org/5.1/#sec-7.6
+ */
+ public static boolean isValid(String raw) {
+ if (raw == null || raw.isEmpty()) return false;
+
+ // Transform \\uXXXX-Escapes into real codepoints (ES5.1 allows escape in IdentifierName
+ String unescaped = decodeEs5UnicodeEscapes(raw);
+ if (unescaped == null || unescaped.isEmpty()) return false;
+
+ int i = 0;
+ int cp = unescaped.codePointAt(i);
+ if (!isIdentifierStartES5(cp)) return false;
+ i += Character.charCount(cp);
+
+ while (i < unescaped.length()) {
+ cp = unescaped.codePointAt(i);
+ if (!isIdentifierPartES5(cp)) return false;
+ i += Character.charCount(cp);
+ }
+ return true;
+ }
+
+ private static boolean isIdentifierStartES5(int cp) {
+ // '$' and '_' explicit
+ if (cp == '$' || cp == '_') return true;
+
+ int t = Character.getType(cp);
+ // Unicode categories: Lu, Ll, Lt, Lm, Lo, Nl
+ switch (t) {
+ case Character.UPPERCASE_LETTER: // Lu
+ case Character.LOWERCASE_LETTER: // Ll
+ case Character.TITLECASE_LETTER: // Lt
+ case Character.MODIFIER_LETTER: // Lm
+ case Character.OTHER_LETTER: // Lo
+ case Character.LETTER_NUMBER: // Nl
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static boolean isIdentifierPartES5(int cp) {
+ if (isIdentifierStartES5(cp)) return true;
+ if (cp == ZWNJ || cp == ZWJ) return true; // U+200C/U+200D are alowed
+
+ int t = Character.getType(cp);
+ // Additional categories: Mn, Mc, Nd, Pc
+ switch (t) {
+ case Character.NON_SPACING_MARK: // Mn
+ case Character.COMBINING_SPACING_MARK: // Mc
+ case Character.DECIMAL_DIGIT_NUMBER: // Nd
+ case Character.CONNECTOR_PUNCTUATION: // Pc (e.g. underline, but already covered)
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Decodes ES5-style Unicode-Escapes \\uXXXX inside a Identifier.
+ *
+ * @return {@code null}, if an escape is syntactically invalid
+ */
+ private static String decodeEs5UnicodeEscapes(String s) {
+ StringBuilder out = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); ) {
+ char ch = s.charAt(i);
+ if (ch == '\\') {
+ if (i + 1 < s.length() && s.charAt(i + 1) == 'u') {
+ // Expect 4 hex chars (ES5.1; no \\u{...} syntax)
+ if (i + 6 > s.length()) return null;
+ String hex = s.substring(i + 2, i + 6);
+ int codeUnit = parse4Hex(hex);
+ if (codeUnit < 0) return null;
+ out.append((char) codeUnit);
+ i += 6;
+ } else {
+ // Other backslashes are not allowed
+ return null;
+ }
+ } else {
+ out.append(ch);
+ i++;
+ }
+ }
+ // Maybe Surrogates...
+ return out.toString();
+ }
+
+ private static int parse4Hex(String hex4) {
+ if (hex4.length() != 4) return -1;
+ int val = 0;
+ for (int i = 0; i < 4; i++) {
+ char c = hex4.charAt(i);
+ int d = Character.digit(c, 16);
+ if (d < 0) return -1;
+ val = (val << 4) | d;
+ }
+ return val;
+ }
+}
diff --git a/src/main/java/de/marhali/json5/internal/LazilyParsedNumber.java b/src/main/java/de/marhali/json5/internal/LazilyParsedNumber.java
index 19f1b9a..be57aef 100644
--- a/src/main/java/de/marhali/json5/internal/LazilyParsedNumber.java
+++ b/src/main/java/de/marhali/json5/internal/LazilyParsedNumber.java
@@ -29,7 +29,9 @@
public final class LazilyParsedNumber extends Number {
private final String value;
- /** @param value must not be null */
+ /**
+ * @param value must not be null
+ */
public LazilyParsedNumber(String value) {
this.value = value;
}
@@ -75,6 +77,7 @@ public String toString() {
* If somebody is unlucky enough to have to serialize one of these, serialize
* it as a BigDecimal so that they won't need Gson on the other side to
* deserialize it.
+ *
* @return Value as {@link BigDecimal}
* @throws ObjectStreamException Stream exception
*/
@@ -103,4 +106,4 @@ public boolean equals(Object obj) {
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/de/marhali/json5/internal/LinkedTreeMap.java b/src/main/java/de/marhali/json5/internal/LinkedTreeMap.java
index edd7ad7..1dba093 100644
--- a/src/main/java/de/marhali/json5/internal/LinkedTreeMap.java
+++ b/src/main/java/de/marhali/json5/internal/LinkedTreeMap.java
@@ -29,78 +29,103 @@
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.NoSuchElementException;
+import java.util.Objects;
import java.util.Set;
/**
- * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses
- * insertion order for iteration order. Comparison order is only used as an
- * optimization for efficient insertion and removal.
+ * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses insertion order for
+ * iteration order. Comparison order is only used as an optimization for efficient insertion and
+ * removal.
*
*
This implementation was derived from Android 4.1's TreeMap class.
*/
+@SuppressWarnings("serial") // ignore warning about missing serialVersionUID
public final class LinkedTreeMap extends AbstractMap implements Serializable {
- @SuppressWarnings({ "unchecked", "rawtypes" }) // to avoid Comparable>>
- private static final Comparator NATURAL_ORDER = new Comparator() {
- public int compare(Comparable a, Comparable b) {
- return a.compareTo(b);
- }
- };
+ @SuppressWarnings({"unchecked", "rawtypes"}) // to avoid Comparable>>
+ private static final Comparator NATURAL_ORDER =
+ new Comparator() {
+ @Override
+ public int compare(Comparable a, Comparable b) {
+ return a.compareTo(b);
+ }
+ };
- Comparator super K> comparator;
+ private final Comparator super K> comparator;
+ private final boolean allowNullValues;
Node root;
int size = 0;
int modCount = 0;
// Used to preserve iteration order
- final Node header = new Node();
+ final Node header;
/**
- * Create a natural order, empty tree map whose keys must be mutually
- * comparable and non-null.
+ * Create a natural order, empty tree map whose keys must be mutually comparable and non-null, and
+ * whose values can be {@code null}.
*/
@SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
public LinkedTreeMap() {
- this((Comparator super K>) NATURAL_ORDER);
+ this((Comparator super K>) NATURAL_ORDER, true);
+ }
+
+ /**
+ * Create a natural order, empty tree map whose keys must be mutually comparable and non-null.
+ *
+ * @param allowNullValues whether {@code null} is allowed as entry value
+ */
+ @SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
+ public LinkedTreeMap(boolean allowNullValues) {
+ this((Comparator super K>) NATURAL_ORDER, allowNullValues);
}
/**
- * Create a tree map ordered by {@code comparator}. This map's keys may only
- * be null if {@code comparator} permits.
+ * Create a tree map ordered by {@code comparator}. This map's keys may only be null if {@code
+ * comparator} permits.
*
- * @param comparator the comparator to order elements with, or {@code null} to
- * use the natural ordering.
+ * @param comparator the comparator to order elements with, or {@code null} to use the natural
+ * ordering.
+ * @param allowNullValues whether {@code null} is allowed as entry value
*/
- @SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
- public LinkedTreeMap(Comparator super K> comparator) {
- this.comparator = comparator != null
- ? comparator
- : (Comparator) NATURAL_ORDER;
+ // unsafe! if comparator is null, this assumes K is comparable
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public LinkedTreeMap(Comparator super K> comparator, boolean allowNullValues) {
+ this.comparator = comparator != null ? comparator : (Comparator) NATURAL_ORDER;
+ this.allowNullValues = allowNullValues;
+ this.header = new Node<>(allowNullValues);
}
- @Override public int size() {
+ @Override
+ public int size() {
return size;
}
- @Override public V get(Object key) {
+ @Override
+ public V get(Object key) {
Node node = findByObject(key);
return node != null ? node.value : null;
}
- @Override public boolean containsKey(Object key) {
+ @Override
+ public boolean containsKey(Object key) {
return findByObject(key) != null;
}
- @Override public V put(K key, V value) {
+ @Override
+ public V put(K key, V value) {
if (key == null) {
throw new NullPointerException("key == null");
}
+ if (value == null && !allowNullValues) {
+ throw new NullPointerException("value == null");
+ }
Node created = find(key, true);
V result = created.value;
created.value = value;
return result;
}
- @Override public void clear() {
+ @Override
+ public void clear() {
root = null;
size = 0;
modCount++;
@@ -110,7 +135,8 @@ public LinkedTreeMap(Comparator super K> comparator) {
header.next = header.prev = header;
}
- @Override public V remove(Object key) {
+ @Override
+ public V remove(Object key) {
Node node = removeInternalByKey(key);
return node != null ? node.value : null;
}
@@ -118,8 +144,7 @@ public LinkedTreeMap(Comparator super K> comparator) {
/**
* Returns the node at or adjacent to the given key, creating it if requested.
*
- * @throws ClassCastException if {@code key} and the tree's keys aren't
- * mutually comparable.
+ * @throws ClassCastException if {@code key} and the tree's keys aren't mutually comparable.
*/
Node find(K key, boolean create) {
Comparator super K> comparator = this.comparator;
@@ -129,12 +154,12 @@ Node find(K key, boolean create) {
if (nearest != null) {
// Micro-optimization: avoid polymorphic calls to Comparator.compare().
@SuppressWarnings("unchecked") // Throws a ClassCastException below if there's trouble.
- Comparable