diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index e2725b7a9..743362b0b 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -60,7 +60,10 @@ * @author JSON.org * @version 2016-08/15 */ -public class JSONArray implements Iterable { +public class JSONArray implements Iterable, JSONSimilar { + + private static final int commaMultiplier = 2; // Each value requires a comma in output + private static final int minimumBufferSize = 16; // Minimum reasonable buffer size for small arrays /** * The arrayList where the JSONArray's properties are kept. @@ -325,27 +328,46 @@ public Object get(int index) throws JSONException { * Get the boolean value associated with an index. The string values "true" * and "false" are converted to boolean. * - * @param index - * The index must be between 0 and length() - 1. + * @param index The index must be between 0 and length() - 1. * @return The truth. - * @throws JSONException - * If there is no value for the index or if the value is not - * convertible to boolean. + * @throws JSONException If there is no value for the index or if the value is not + * convertible to boolean. */ public boolean getBoolean(int index) throws JSONException { Object object = this.get(index); - if (object.equals(Boolean.FALSE) - || (object instanceof String && ((String) object) - .equalsIgnoreCase("false"))) { + + if (isFalse(object)) { return false; - } else if (object.equals(Boolean.TRUE) - || (object instanceof String && ((String) object) - .equalsIgnoreCase("true"))) { + } + if (isTrue(object)) { return true; } + throw wrongValueFormatException(index, "boolean", object, null); } + /** + * Checks if the object represents a false value + * @param object The object to check + * @return true if the object represents false + */ + private boolean isFalse(Object object) { + if (object.equals(Boolean.FALSE)) return true; + if (!(object instanceof String)) return false; + return ((String) object).equalsIgnoreCase("false"); + } + + /** + * Checks if the object represents a true value + * @param object The object to check + * @return true if the object represents true + */ + private boolean isTrue(Object object) { + if (object.equals(Boolean.TRUE)) return true; + if (!(object instanceof String)) return false; + return ((String) object).equalsIgnoreCase("true"); + } + /** * Get the double value associated with an index. * @@ -1624,44 +1646,21 @@ public Object remove(int index) { /** * Determine if two JSONArrays are similar. * They must contain similar sequences. - * * @param other The other JSONArray * @return true if they are equal */ + @Override public boolean similar(Object other) { if (!(other instanceof JSONArray)) { return false; } + JSONArray otherArray = (JSONArray)other; int len = this.length(); - if (len != ((JSONArray)other).length()) { + if (len != otherArray.length()) { return false; } for (int i = 0; i < len; i += 1) { - Object valueThis = this.myArrayList.get(i); - Object valueOther = ((JSONArray)other).myArrayList.get(i); - if(valueThis == valueOther) { - continue; - } - if(valueThis == null) { - return false; - } - if (valueThis instanceof JSONObject) { - if (!((JSONObject)valueThis).similar(valueOther)) { - return false; - } - } else if (valueThis instanceof JSONArray) { - if (!((JSONArray)valueThis).similar(valueOther)) { - return false; - } - } else if (valueThis instanceof Number && valueOther instanceof Number) { - if (!JSONObject.isNumberSimilar((Number)valueThis, (Number)valueOther)) { - return false; - } - } else if (valueThis instanceof JSONString && valueOther instanceof JSONString) { - if (!((JSONString) valueThis).toJSONString().equals(((JSONString) valueOther).toJSONString())) { - return false; - } - } else if (!valueThis.equals(valueOther)) { + if (!JSONSimilar.compare(this.myArrayList.get(i), otherArray.myArrayList.get(i))) { return false; } } @@ -1743,8 +1742,8 @@ public String toString() { public String toString(int indentFactor) throws JSONException { // each value requires a comma, so multiply the count by 2 // We don't want to oversize the initial capacity - int initialSize = myArrayList.size() * 2; - Writer sw = new StringBuilderWriter(Math.max(initialSize, 16)); + int initialSize = myArrayList.size() * commaMultiplier; + Writer sw = new StringBuilderWriter(Math.max(initialSize, minimumBufferSize)); return this.write(sw, indentFactor, 0).toString(); } @@ -1763,79 +1762,135 @@ public Writer write(Writer writer) throws JSONException { } /** - * Write the contents of the JSONArray as JSON text to a writer. - * - *

If

{@code indentFactor > 0}
and the {@link JSONArray} has only - * one element, then the array will be output on a single line: - *
{@code [1]}
- * - *

If an array has 2 or more elements, then it will be output across - * multiple lines:

{@code
-     * [
-     * 1,
-     * "value 2",
-     * 3
-     * ]
-     * }
- *

- * Warning: This method assumes that the data structure is acyclical. - * + * Writes the contents of the JSONArray as JSON text to a writer. * - * @param writer - * Writes the serialized JSON - * @param indentFactor - * The number of spaces to add to each level of indentation. - * @param indent - * The indentation of the top level. + *

If {@code indentFactor > 0} and the JSONArray has only one element, + * the array will be output on a single line (e.g., {@code [1]}). If the array + * has 2 or more elements, it will be output across multiple lines with proper + * indentation.

+ * + *

Warning: This method assumes that the data structure is acyclical.

+ * + * @param writer The writer to which the JSON text is written. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indentation level of the top level. * @return The writer. - * @throws JSONException if a called function fails or unable to write + * @throws JSONException If an error occurs while writing the JSON text. */ @SuppressWarnings("resource") - public Writer write(Writer writer, int indentFactor, int indent) - throws JSONException { + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { try { - boolean needsComma = false; - int length = this.length(); - writer.write('['); - - if (length == 1) { - try { - JSONObject.writeValue(writer, this.myArrayList.get(0), - indentFactor, indent); - } catch (Exception e) { - throw new JSONException("Unable to write JSONArray value at index: 0", e); - } - } else if (length != 0) { - final int newIndent = indent + indentFactor; - - for (int i = 0; i < length; i += 1) { - if (needsComma) { - writer.write(','); - } - if (indentFactor > 0) { - writer.write('\n'); - } - JSONObject.indent(writer, newIndent); - try { - JSONObject.writeValue(writer, this.myArrayList.get(i), - indentFactor, newIndent); - } catch (Exception e) { - throw new JSONException("Unable to write JSONArray value at index: " + i, e); - } - needsComma = true; - } - if (indentFactor > 0) { - writer.write('\n'); - } - JSONObject.indent(writer, indent); - } - writer.write(']'); + writeArrayStart(writer); + writeArrayContents(writer, indentFactor, indent); + writeArrayEnd(writer); return writer; } catch (IOException e) { throw new JSONException(e); } } + /** + * Writes the opening bracket '[' of the JSONArray to the writer. + * + * @param writer The writer to which the opening bracket is written. + * @throws IOException If an I/O error occurs while writing. + */ + private void writeArrayStart(Writer writer) throws IOException { + writer.write('['); + } + + /** + * Writes the contents of the JSONArray to the writer, handling single and multiple elements. + * + * @param writer The writer to which the contents are written. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indentation level of the top level. + * @throws JSONException If an error occurs while writing the contents. + * @throws IOException If an I/O error occurs while writing. + */ + private void writeArrayContents(Writer writer, int indentFactor, int indent) throws JSONException, IOException { + int length = this.length(); + if (length == 1) { + writeSingleElement(writer, indentFactor, indent); + } else if (length != 0) { + writeMultipleElements(writer, indentFactor, indent); + } + } + + /** + * Writes a single element of the JSONArray to the writer. + * + * @param writer The writer to which the element is written. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indentation level of the top level. + * @throws JSONException If an error occurs while writing the element. + */ + private void writeSingleElement(Writer writer, int indentFactor, int indent) throws JSONException { + try { + JSONObject.writeValue(writer, this.myArrayList.get(0), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: 0", e); + } + } + + /** + * Writes multiple elements of the JSONArray to the writer, formatting them with proper indentation. + * + * @param writer The writer to which the elements are written. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indentation level of the top level. + * @throws JSONException If an error occurs while writing the elements. + * @throws IOException If an I/O error occurs while writing. + */ + private void writeMultipleElements(Writer writer, int indentFactor, int indent) throws JSONException, IOException { + final int newIndent = indent + indentFactor; + boolean needsComma = false; + + for (int i = 0; i < this.length(); i += 1) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newIndent); + writeElement(writer, i, indentFactor, newIndent); + needsComma = true; + } + + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + + /** + * Writes a specific element of the JSONArray to the writer. + * + * @param writer The writer to which the element is written. + * @param index The index of the element to write. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indentation level of the top level. + * @throws JSONException If an error occurs while writing the element. + */ + private void writeElement(Writer writer, int index, int indentFactor, int indent) throws JSONException { + try { + JSONObject.writeValue(writer, this.myArrayList.get(index), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + index, e); + } + } + + /** + * Writes the closing bracket ']' of the JSONArray to the writer. + * + * @param writer The writer to which the closing bracket is written. + * @throws IOException If an I/O error occurs while writing. + */ + private void writeArrayEnd(Writer writer) throws IOException { + writer.write(']'); + } + /** * Returns a java.util.List containing all of the elements in this array. * If an element in the array is a JSONArray or JSONObject it will also diff --git a/src/main/java/org/json/JSONML.java b/src/main/java/org/json/JSONML.java index 7b53e4da7..dcc8b1dfe 100644 --- a/src/main/java/org/json/JSONML.java +++ b/src/main/java/org/json/JSONML.java @@ -38,7 +38,7 @@ private static Object parse( int currentNestingDepth ) throws JSONException { return parse(x,arrayForm, ja, - keepStrings ? JSONMLParserConfiguration.KEEP_STRINGS : JSONMLParserConfiguration.ORIGINAL, + keepStrings ? JSONMLParserConfiguration.getKeepStringsConfiguration() : JSONMLParserConfiguration.getOriginalConfiguration(), currentNestingDepth); } @@ -81,10 +81,10 @@ private static Object parse( throw x.syntaxError("Bad XML"); } token = x.nextContent(); - if (token == XML.LT) { + if (token == XMLConstants.LT) { token = x.nextToken(); if (token instanceof Character) { - if (token == XML.SLASH) { + if (token == XMLConstants.SLASH) { // Close tag ' after ' 0); } - } else if (token == XML.QUEST) { + } else if (token == XMLConstants.QUEST) { // - if (token == XML.SLASH) { - if (x.nextToken() != XML.GT) { + if (token == XMLConstants.SLASH) { + if (x.nextToken() != XMLConstants.GT) { throw x.syntaxError("Misshaped tag"); } if (ja == null) { @@ -210,7 +210,7 @@ private static Object parse( // Content, between <...> and } else { - if (token != XML.GT) { + if (token != XMLConstants.GT) { throw x.syntaxError("Misshaped tag"); } @@ -261,7 +261,7 @@ private static Object parse( * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, JSONMLParserConfiguration.ORIGINAL, 0); + return (JSONArray)parse(new XMLTokener(string), true, null, JSONMLParserConfiguration.getOriginalConfiguration(), 0); } diff --git a/src/main/java/org/json/JSONMLParserConfiguration.java b/src/main/java/org/json/JSONMLParserConfiguration.java index 43ba0db62..877e7550a 100644 --- a/src/main/java/org/json/JSONMLParserConfiguration.java +++ b/src/main/java/org/json/JSONMLParserConfiguration.java @@ -1,4 +1,5 @@ package org.json; + /* Public Domain. */ @@ -10,16 +11,43 @@ public class JSONMLParserConfiguration extends ParserConfiguration { /** - * We can override the default maximum nesting depth if needed. + * Default maximum nesting depth for the XML to JSONML parser. */ - public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = ParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH; + private static final int DEFAULT_MAXIMUM_NESTING_DEPTH = ParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH; /** Original Configuration of the XML to JSONML Parser. */ - public static final JSONMLParserConfiguration ORIGINAL - = new JSONMLParserConfiguration(); + private static final JSONMLParserConfiguration ORIGINAL + = new JSONMLParserConfiguration(); /** Original configuration of the XML to JSONML Parser except that values are kept as strings. */ - public static final JSONMLParserConfiguration KEEP_STRINGS - = new JSONMLParserConfiguration().withKeepStrings(true); + private static final JSONMLParserConfiguration KEEP_STRINGS + = new JSONMLParserConfiguration().withKeepStrings(true); + + /** + * Returns the default maximum nesting depth for the XML to JSONML parser. + * + * @return The default maximum nesting depth. + */ + public static int getDefaultMaximumNestingDepth() { + return DEFAULT_MAXIMUM_NESTING_DEPTH; + } + + /** + * Returns the original configuration of the XML to JSONML parser. + * + * @return The original configuration. + */ + public static JSONMLParserConfiguration getOriginalConfiguration() { + return ORIGINAL; + } + + /** + * Returns the configuration of the XML to JSONML parser that keeps values as strings. + * + * @return The configuration that keeps values as strings. + */ + public static JSONMLParserConfiguration getKeepStringsConfiguration() { + return KEEP_STRINGS; + } /** * Default parser configuration. Does not keep strings (tries to implicitly convert values). @@ -31,6 +59,7 @@ public JSONMLParserConfiguration() { /** * Configure the parser string processing and use the default CDATA Tag Name as "content". + * * @param keepStrings true to parse all values as string. * false to try and convert XML string values into a JSON value. * @param maxNestingDepth int to limit the nesting depth @@ -44,11 +73,6 @@ protected JSONMLParserConfiguration(final boolean keepStrings, final int maxNest */ @Override protected JSONMLParserConfiguration clone() { - // future modifications to this method should always ensure a "deep" - // clone in the case of collections. i.e. if a Map is added as a configuration - // item, a new map instance should be created and if possible each value in the - // map should be cloned as well. If the values of the map are known to also - // be immutable, then a shallow clone of the map is acceptable. return new JSONMLParserConfiguration( this.keepStrings, this.maxNestingDepth @@ -66,4 +90,4 @@ public JSONMLParserConfiguration withKeepStrings(final boolean newVal) { public JSONMLParserConfiguration withMaxNestingDepth(int maxNestingDepth) { return super.withMaxNestingDepth(maxNestingDepth); } -} +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index d50fff73b..86b0ae264 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -71,7 +71,7 @@ * @author JSON.org * @version 2016-08-15 */ -public class JSONObject { +public class JSONObject implements JSONSimilar { /** * JSONObject.NULL is equivalent to the value that JavaScript calls null, * whilst Java's null is equivalent to the value that JavaScript calls @@ -2358,45 +2358,21 @@ public Object remove(String key) { * Determine if two JSONObjects are similar. * They must contain the same set of names which must be associated with * similar values. - * * @param other The other JSONObject * @return true if they are equal */ + @Override public boolean similar(Object other) { try { if (!(other instanceof JSONObject)) { return false; } - if (!this.keySet().equals(((JSONObject)other).keySet())) { + JSONObject otherObj = (JSONObject)other; + if (!this.keySet().equals(otherObj.keySet())) { return false; } - for (final Entry entry : this.entrySet()) { - String name = entry.getKey(); - Object valueThis = entry.getValue(); - Object valueOther = ((JSONObject)other).get(name); - if(valueThis == valueOther) { - continue; - } - if(valueThis == null) { - return false; - } - if (valueThis instanceof JSONObject) { - if (!((JSONObject)valueThis).similar(valueOther)) { - return false; - } - } else if (valueThis instanceof JSONArray) { - if (!((JSONArray)valueThis).similar(valueOther)) { - return false; - } - } else if (valueThis instanceof Number && valueOther instanceof Number) { - if (!isNumberSimilar((Number)valueThis, (Number)valueOther)) { - return false; - } - } else if (valueThis instanceof JSONString && valueOther instanceof JSONString) { - if (!((JSONString) valueThis).toJSONString().equals(((JSONString) valueOther).toJSONString())) { - return false; - } - } else if (!valueThis.equals(valueOther)) { + for (Map.Entry entry : this.entrySet()) { + if (!JSONSimilar.compare(entry.getValue(), otherObj.get(entry.getKey()))) { return false; } } diff --git a/src/main/java/org/json/JSONSimilar.java b/src/main/java/org/json/JSONSimilar.java new file mode 100644 index 000000000..21685d0b3 --- /dev/null +++ b/src/main/java/org/json/JSONSimilar.java @@ -0,0 +1,33 @@ +package org.json; + +/** + * Interface for comparing JSON entities for semantic similarity. + * @author JSON.org + * @version 2023-07-20 + */ +public interface JSONSimilar { + /** + * Determine if this JSON entity is similar to another object. + * @param other The object to compare with + * @return true if they are semantically similar + */ + boolean similar(Object other); + + /** + * Helper method to compare two arbitrary values according to JSON similarity rules. + * @param a First value to compare + * @param b Second value to compare + * @return true if values are semantically similar + */ + static boolean compare(Object a, Object b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a instanceof JSONSimilar) { + return ((JSONSimilar)a).similar(b); + } + if (a instanceof Number && b instanceof Number) { + return JSONSimilarUtils.areNumbersSimilar((Number)a, (Number)b); + } + return a.equals(b); + } +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONSimilarUtils.java b/src/main/java/org/json/JSONSimilarUtils.java new file mode 100644 index 000000000..7cd2ec182 --- /dev/null +++ b/src/main/java/org/json/JSONSimilarUtils.java @@ -0,0 +1,27 @@ +package org.json; + +/** + * Utility class for JSON similarity comparisons. + * @author JSON.org + * @version 2023-07-20 + */ +public final class JSONSimilarUtils { + private static final double FLOATING_POINT_TOLERANCE = 0.000001d; + + /** + * Compare two numbers for JSON similarity with proper handling of different numeric types. + * @param a First number to compare + * @param b Second number to compare + * @return true if numbers are semantically equivalent in JSON context + */ + static boolean areNumbersSimilar(Number a, Number b) { + if (a.equals(b)) { + return true; + } + if (a instanceof Double || a instanceof Float || + b instanceof Double || b instanceof Float) { + return Math.abs(a.doubleValue() - b.doubleValue()) < FLOATING_POINT_TOLERANCE; + } + return a.longValue() == b.longValue(); + } +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONString.java b/src/main/java/org/json/JSONString.java index cd8d1847d..353ca1dd7 100644 --- a/src/main/java/org/json/JSONString.java +++ b/src/main/java/org/json/JSONString.java @@ -12,7 +12,7 @@ * toJSONString method will be used instead of the default behavior * of using the Object's toString() method and quoting the result. */ -public interface JSONString { +public interface JSONString extends JSONSimilar { /** * The toJSONString method allows a class to produce its own JSON * serialization. @@ -20,4 +20,17 @@ public interface JSONString { * @return A strictly syntactically correct JSON text. */ public String toJSONString(); + + /** + * Determine if two JSONStrings are similar by comparing their serialized forms. + * @param other The other JSONString + * @return true if their JSON representations are equal + */ + @Override + default boolean similar(Object other) { + if (!(other instanceof JSONString)) { + return false; + } + return this.toJSONString().equals(((JSONString)other).toJSONString()); + } } diff --git a/src/main/java/org/json/JSONStringer.java b/src/main/java/org/json/JSONStringer.java index 2f6cf9ed8..d65cbcc61 100644 --- a/src/main/java/org/json/JSONStringer.java +++ b/src/main/java/org/json/JSONStringer.java @@ -7,53 +7,147 @@ import java.io.StringWriter; /** - * JSONStringer provides a quick and convenient way of producing JSON text. - * The texts produced strictly conform to JSON syntax rules. No whitespace is - * added, so the results are ready for transmission or storage. Each instance of - * JSONStringer can produce one JSON text. + * JSONStringer provides a quick and convenient way of producing JSON text + * using a StringWriter. The texts produced strictly conform to JSON syntax rules. + * No whitespace is added, so the results are ready for transmission or storage. + * Each instance of JSONStringer can produce one JSON text. + *

+ * This class delegates to a {@link JSONWriter} instance while providing a + * simplified interface for string-based JSON generation. It maintains the same + * fluent API as JSONWriter but removes direct inheritance where it wasn't fully + * utilized. *

* A JSONStringer instance provides a value method for appending - * values to the - * text, and a key - * method for adding keys before values in objects. There are array - * and endArray methods that make and bound array values, and - * object and endObject methods which make and bound - * object values. All of these methods return the JSONWriter instance, - * permitting cascade style. For example,

+ * values to the text, and a key method for adding keys before
+ * values in objects. There are array and endArray
+ * methods that make and bound array values, and object and
+ * endObject methods which make and bound object values. All of
+ * these methods return the JSONStringer instance, permitting cascade style.
+ * For example:
+ * 
  * myString = new JSONStringer()
  *     .object()
  *         .key("JSON")
  *         .value("Hello, World!")
  *     .endObject()
- *     .toString();
which produces the string
+ *     .toString();
+ * produces the string: + *
  * {"JSON":"Hello, World!"}
*

* The first method called must be array or object. - * There are no methods for adding commas or colons. JSONStringer adds them for - * you. Objects and arrays can be nested up to 200 levels deep. + * Commas and colons are automatically added by the delegate JSONWriter. + * Objects and arrays can be nested up to 200 levels deep. *

- * This can sometimes be easier than using a JSONObject to build a string. + * This is often simpler than using JSONObject directly for string generation. + * * @author JSON.org - * @version 2015-12-09 + * @version 2023-07-20 (Refactored to use delegation) */ -public class JSONStringer extends JSONWriter { +public class JSONStringer { + private final JSONWriter writer; + private final StringWriter stringWriter; + /** - * Make a fresh JSONStringer. It can be used to build one JSON text. + * Constructs a fresh JSONStringer instance. Each instance can build one + * complete JSON text. The underlying StringWriter and JSONWriter are + * initialized automatically. */ public JSONStringer() { - super(new StringWriter()); + this.stringWriter = new StringWriter(); + this.writer = new JSONWriter(stringWriter); + } + + /** + * Begins appending a new array. All values until the balancing + * {@link #endArray()} will be appended to this array. + * + * @return this JSONStringer for method chaining + * @throws JSONException If nesting is too deep or called in invalid context + * @see JSONWriter#array() + */ + public JSONStringer array() throws JSONException { + writer.array(); + return this; + } + + /** + * Ends the current array. Must be called to balance array starts. + * + * @return this JSONStringer for method chaining + * @throws JSONException If incorrectly nested + * @see JSONWriter#endArray() + */ + public JSONStringer endArray() throws JSONException { + writer.endArray(); + return this; + } + + /** + * Begins appending a new object. All keys and values until {@link #endObject()} + * will be appended to this object. + * + * @return this JSONStringer for method chaining + * @throws JSONException If nesting is too deep or called in invalid context + * @see JSONWriter#object() + */ + public JSONStringer object() throws JSONException { + writer.object(); + return this; + } + + /** + * Ends the current object. Must be called to balance object starts. + * + * @return this JSONStringer for method chaining + * @throws JSONException If incorrectly nested + * @see JSONWriter#endObject() + */ + public JSONStringer endObject() throws JSONException { + writer.endObject(); + return this; + } + + /** + * Appends a key to the current object. The next value will be associated + * with this key. + * + * @param key The key string (must not be null) + * @return this JSONStringer for method chaining + * @throws JSONException If key is null or called in invalid context + * @see JSONWriter#key(String) + */ + public JSONStringer key(String key) throws JSONException { + writer.key(key); + return this; + } + + /** + * Appends a value to the current array or object. + * + * @param value The value to append (may be null, Boolean, Number, String, + * JSONObject, JSONArray, or JSONString) + * @return this JSONStringer for method chaining + * @throws JSONException If value is invalid or called in wrong context + * @see JSONWriter#value(Object) + */ + public JSONStringer value(Object value) throws JSONException { + writer.value(value); + return this; } /** - * Return the JSON text. This method is used to obtain the product of the - * JSONStringer instance. It will return null if there was a - * problem in the construction of the JSON text (such as the calls to - * array were not properly balanced with calls to - * endArray). - * @return The JSON text. + * Returns the generated JSON text. Returns null if: + *

    + *
  • No array/object was started + *
  • The JSON structure is incomplete (unbalanced arrays/objects) + *
+ * + * @return The JSON text or null if incomplete + * @see JSONWriter#mode */ @Override public String toString() { - return this.mode == 'd' ? this.writer.toString() : null; + return writer.mode == 'd' ? stringWriter.toString() : null; } -} +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index a90d51ae3..91c3732f8 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -400,26 +400,15 @@ public String nextString(char quote) throws JSONException { /** * Get the text up but not including the specified character or the * end of line, whichever comes first. - * @param delimiter A delimiter character. - * @return A string. + * @param delimiter A delimiter character. + * @return A string. * @throws JSONException Thrown if there is an error while searching * for the delimiter */ public String nextTo(char delimiter) throws JSONException { - StringBuilder sb = new StringBuilder(); - for (;;) { - char c = this.next(); - if (c == delimiter || c == 0 || c == '\n' || c == '\r') { - if (c != 0) { - this.back(); - } - return sb.toString().trim(); - } - sb.append(c); - } + return nextToInternal(c -> c == delimiter); } - /** * Get the text up but not including one of the specified delimiter * characters or the end of line, whichever comes first. @@ -429,13 +418,22 @@ public String nextTo(char delimiter) throws JSONException { * for the delimiter */ public String nextTo(String delimiters) throws JSONException { - char c; + return nextToInternal(c -> delimiters.indexOf(c) >= 0); + } + + /** + * Internal implementation for nextTo operations. + * @param delimiterCheck Function to check if character is a delimiter + * @return Collected string up to delimiter + * @throws JSONException Thrown if there is an error while reading + */ + private String nextToInternal(java.util.function.Predicate delimiterCheck) + throws JSONException { StringBuilder sb = new StringBuilder(); for (;;) { - c = this.next(); - if (delimiters.indexOf(c) >= 0 || c == 0 || - c == '\n' || c == '\r') { - if (c != 0) { + char c = this.next(); + if (shouldStopReading(c, delimiterCheck)) { + if (!isEndOfInput(c)) { this.back(); } return sb.toString().trim(); @@ -444,6 +442,36 @@ public String nextTo(String delimiters) throws JSONException { } } + /** + * Determines whether reading should stop based on the current character. + * @param c The current character to check + * @param delimiterCheck Predicate to test for delimiter matches + * @return true if reading should stop, false otherwise + */ + private boolean shouldStopReading(char c, java.util.function.Predicate delimiterCheck) { + boolean isEndOfInput = isEndOfInput(c); + boolean isNewLine = isNewLineCharacter(c); + return delimiterCheck.test(c) || isEndOfInput || isNewLine; + } + + /** + * Checks if a character represents the end of input. + * @param c The character to check + * @return true if the character is the end-of-input marker (0), false otherwise + */ + private boolean isEndOfInput(char c) { + return c == 0; + } + + /** + * Checks if a character represents a newline. + * @param c The character to check + * @return true if the character is a newline (\n or \r), false otherwise + */ + private boolean isNewLineCharacter(char c) { + return c == '\n' || c == '\r'; + } + /** * Get the next value. The value can be a Boolean, Double, Integer, diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index e59ec7a4a..7cd2c2f89 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -26,33 +26,6 @@ public class XML { public XML() { } - /** The Character '&'. */ - public static final Character AMP = '&'; - - /** The Character '''. */ - public static final Character APOS = '\''; - - /** The Character '!'. */ - public static final Character BANG = '!'; - - /** The Character '='. */ - public static final Character EQ = '='; - - /** The Character
{@code '>'. }
*/ - public static final Character GT = '>'; - - /** The Character '<'. */ - public static final Character LT = '<'; - - /** The Character '?'. */ - public static final Character QUEST = '?'; - - /** The Character '"'. */ - public static final Character QUOT = '"'; - - /** The Character '/'. */ - public static final Character SLASH = '/'; - /** * Null attribute name */ @@ -268,7 +241,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP // ' after ' 0); return false; - } else if (token == QUEST) { + } else if (token == XMLConstants.QUEST) { // "); return false; - } else if (token == SLASH) { + } else if (token == XMLConstants.SLASH) { // Close tag - if (x.nextToken() != GT) { + if (x.nextToken() != XMLConstants.GT) { throw x.syntaxError("Misshaped tag"); } if (config.getForceList().contains(tagName)) { @@ -391,7 +364,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP } return false; - } else if (token == GT) { + } else if (token == XMLConstants.GT) { // Content, between <...> and for (;;) { token = x.nextContent(); @@ -412,7 +385,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP } } - } else if (token == LT) { + } else if (token == XMLConstants.LT) { // Nested element if (currentNestingDepth == config.getMaxNestingDepth()) { throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached"); diff --git a/src/main/java/org/json/XMLConstants.java b/src/main/java/org/json/XMLConstants.java new file mode 100644 index 000000000..a21a12d30 --- /dev/null +++ b/src/main/java/org/json/XMLConstants.java @@ -0,0 +1,44 @@ +package org.json; + +/* +Public Domain. +*/ + +/** + * A utility class to hold XML-related constants. + */ +public final class XMLConstants { + /** The Character '&'. */ + public static final Character AMP = '&'; + + /** The Character '''. */ + public static final Character APOS = '\''; + + /** The Character '!'. */ + public static final Character BANG = '!'; + + /** The Character '='. */ + public static final Character EQ = '='; + + /** The Character
{@code '>'. }
*/ + public static final Character GT = '>'; + + /** The Character '<'. */ + public static final Character LT = '<'; + + /** The Character '?'. */ + public static final Character QUEST = '?'; + + /** The Character '"'. */ + public static final Character QUOT = '"'; + + /** The Character '/'. */ + public static final Character SLASH = '/'; + + /** + * Private constructor to prevent instantiation. + */ + private XMLConstants() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } +} \ No newline at end of file diff --git a/src/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java index bc18b31c9..3d6b405af 100644 --- a/src/main/java/org/json/XMLTokener.java +++ b/src/main/java/org/json/XMLTokener.java @@ -5,6 +5,7 @@ */ import java.io.Reader; +import java.util.HashMap; /** * The XMLTokener extends the JSONTokener to provide additional methods @@ -14,22 +15,21 @@ */ public class XMLTokener extends JSONTokener { + /** The table of entity values. It initially contains Character values for + * amp, apos, gt, lt, quot. + */ + public static final HashMap entity; - /** The table of entity values. It initially contains Character values for - * amp, apos, gt, lt, quot. - */ - public static final java.util.HashMap entity; - - private XMLParserConfiguration configuration = XMLParserConfiguration.ORIGINAL; + private XMLParserConfiguration configuration = XMLParserConfiguration.ORIGINAL; - static { - entity = new java.util.HashMap(8); - entity.put("amp", XML.AMP); - entity.put("apos", XML.APOS); - entity.put("gt", XML.GT); - entity.put("lt", XML.LT); - entity.put("quot", XML.QUOT); - } + static { + entity = new HashMap(8); + entity.put("amp", XMLConstants.AMP); + entity.put("apos", XMLConstants.APOS); + entity.put("gt", XMLConstants.GT); + entity.put("lt", XMLConstants.LT); + entity.put("quot", XMLConstants.QUOT); + } /** * Construct an XMLTokener from a Reader. @@ -100,7 +100,7 @@ public Object nextContent() throws JSONException { return null; } if (c == '<') { - return XML.LT; + return XMLConstants.LT; } sb = new StringBuilder(); for (;;) { @@ -201,17 +201,17 @@ public Object nextMeta() throws JSONException { case 0: throw syntaxError("Misshaped meta tag"); case '<': - return XML.LT; + return XMLConstants.LT; case '>': - return XML.GT; + return XMLConstants.GT; case '/': - return XML.SLASH; + return XMLConstants.SLASH; case '=': - return XML.EQ; + return XMLConstants.EQ; case '!': - return XML.BANG; + return XMLConstants.BANG; case '?': - return XML.QUEST; + return XMLConstants.QUEST; case '"': case '\'': q = c; @@ -272,15 +272,15 @@ public Object nextToken() throws JSONException { case '<': throw syntaxError("Misplaced '<'"); case '>': - return XML.GT; + return XMLConstants.GT; case '/': - return XML.SLASH; + return XMLConstants.SLASH; case '=': - return XML.EQ; + return XMLConstants.EQ; case '!': - return XML.BANG; + return XMLConstants.BANG; case '?': - return XML.QUEST; + return XMLConstants.QUEST; // Quoted string diff --git a/src/test/java/org/json/junit/JSONMLTest.java b/src/test/java/org/json/junit/JSONMLTest.java index 5a360dd59..55b60977c 100644 --- a/src/test/java/org/json/junit/JSONMLTest.java +++ b/src/test/java/org/json/junit/JSONMLTest.java @@ -841,7 +841,7 @@ public void testToJSONArrayMaxNestingDepthOf42IsRespected() { final int maxNestingDepth = 42; try { - JSONML.toJSONArray(wayTooLongMalformedXML, JSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth)); + JSONML.toJSONArray(wayTooLongMalformedXML, JSONMLParserConfiguration.getOriginalConfiguration().withMaxNestingDepth(maxNestingDepth)); fail("Expecting a JSONException"); } catch (JSONException e) { @@ -864,7 +864,7 @@ public void testToJSONArrayMaxNestingDepthIsRespectedWithValidXML() { final int maxNestingDepth = 1; try { - JSONML.toJSONArray(perfectlyFineXML, JSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth)); + JSONML.toJSONArray(perfectlyFineXML, JSONMLParserConfiguration.getOriginalConfiguration().withMaxNestingDepth(maxNestingDepth)); fail("Expecting a JSONException"); } catch (JSONException e) { @@ -886,7 +886,7 @@ public void testToJSONArrayMaxNestingDepthWithValidFittingXML() { final int maxNestingDepth = 3; try { - JSONML.toJSONArray(perfectlyFineXML, JSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth)); + JSONML.toJSONArray(perfectlyFineXML, JSONMLParserConfiguration.getOriginalConfiguration().withMaxNestingDepth(maxNestingDepth)); } catch (JSONException e) { e.printStackTrace(); fail("XML document should be parsed as its maximum depth fits the maxNestingDepth " + @@ -900,24 +900,24 @@ public void testToJSONObjectMaxDefaultNestingDepthIsRespected() { final String wayTooLongMalformedXML = new String(new char[6000]).replace("\0", ""); try { - JSONML.toJSONObject(wayTooLongMalformedXML, JSONMLParserConfiguration.ORIGINAL); + JSONML.toJSONObject(wayTooLongMalformedXML, JSONMLParserConfiguration.getOriginalConfiguration()); fail("Expecting a JSONException"); } catch (JSONException e) { assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">", - e.getMessage().startsWith("Maximum nesting depth of " + JSONMLParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH)); + e.getMessage().startsWith("Maximum nesting depth of " + JSONMLParserConfiguration.getDefaultMaximumNestingDepth())); } } @Test public void testToJSONObjectUnlimitedNestingDepthIsPossible() { - int actualDepth = JSONMLParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH +10; + int actualDepth = JSONMLParserConfiguration.getDefaultMaximumNestingDepth() +10; final String deeperThanDefaultMax = new String(new char[actualDepth]).replace("\0", "") + "value" + new String(new char[actualDepth]).replace("\0", ""); try { - JSONML.toJSONObject(deeperThanDefaultMax, JSONMLParserConfiguration.ORIGINAL + JSONML.toJSONObject(deeperThanDefaultMax, JSONMLParserConfiguration.getOriginalConfiguration() .withMaxNestingDepth(JSONMLParserConfiguration.UNDEFINED_MAXIMUM_NESTING_DEPTH)); } catch (JSONException e) { e.printStackTrace(); @@ -934,7 +934,7 @@ public void testToJSONObjectMaxNestingDepthOf42IsRespected() { final int maxNestingDepth = 42; try { - JSONML.toJSONObject(wayTooLongMalformedXML, JSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth)); + JSONML.toJSONObject(wayTooLongMalformedXML, JSONMLParserConfiguration.getOriginalConfiguration().withMaxNestingDepth(maxNestingDepth)); fail("Expecting a JSONException"); } catch (JSONException e) { @@ -956,7 +956,7 @@ public void testToJSONObjectMaxNestingDepthIsRespectedWithValidXML() { final int maxNestingDepth = 1; try { - JSONML.toJSONObject(perfectlyFineXML, JSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth)); + JSONML.toJSONObject(perfectlyFineXML, JSONMLParserConfiguration.getOriginalConfiguration().withMaxNestingDepth(maxNestingDepth)); fail("Expecting a JSONException"); } catch (JSONException e) { @@ -978,7 +978,7 @@ public void testToJSONObjectMaxNestingDepthWithValidFittingXML() { final int maxNestingDepth = 3; try { - JSONML.toJSONObject(perfectlyFineXML, JSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth)); + JSONML.toJSONObject(perfectlyFineXML, JSONMLParserConfiguration.getOriginalConfiguration().withMaxNestingDepth(maxNestingDepth)); } catch (JSONException e) { e.printStackTrace(); fail("XML document should be parsed as its maximum depth fits the maxNestingDepth " +