diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index e2725b7a9..ac967d36f 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. * @@ -604,11 +626,11 @@ public String join(String separator) throws JSONException { } StringBuilder sb = new StringBuilder( - JSONObject.valueToString(this.myArrayList.get(0))); + JSONWriter.valueToString(this.myArrayList.get(0))); for (int i = 1; i < len; i++) { sb.append(separator) - .append(JSONObject.valueToString(this.myArrayList.get(i))); + .append(JSONWriter.valueToString(this.myArrayList.get(i))); } return sb.toString(); } @@ -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; } } @@ -1706,7 +1705,7 @@ public JSONObject toJSONObject(JSONArray names) throws JSONException { @Override public String toString() { try { - return this.toString(0); + return JSONWriter.format(this, 0); } catch (Exception e) { return null; } @@ -1741,11 +1740,7 @@ public String toString() { */ @SuppressWarnings("resource") 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)); - return this.write(sw, indentFactor, 0).toString(); + return JSONWriter.format(this, indentFactor); } /** @@ -1759,81 +1754,28 @@ public String toString(int indentFactor) throws JSONException { * @throws JSONException if a called function fails */ public Writer write(Writer writer) throws JSONException { - return this.write(writer, 0, 0); + return JSONWriter.format(this, writer, 0, 0); } /** - * 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 { - 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(']'); - return writer; - } catch (IOException e) { - throw new JSONException(e); - } + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + return JSONWriter.format(this, writer, indentFactor, indent); } /** 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..60bb76fa6 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; } } @@ -2631,7 +2607,7 @@ public JSONArray toJSONArray(JSONArray names) throws JSONException { @Override public String toString() { try { - return this.toString(0); + return JSONWriter.format(this, 0); } catch (Exception e) { return null; } @@ -2665,43 +2641,7 @@ public String toString() { */ @SuppressWarnings("resource") public String toString(int indentFactor) throws JSONException { - // 6 characters are the minimum to serialise a key value pair e.g.: "k":1, - // and we don't want to oversize the initial capacity - int initialSize = map.size() * 6; - Writer w = new StringBuilderWriter(Math.max(initialSize, 16)); - return this.write(w, indentFactor, 0).toString(); - } - - /** - * Make a JSON text of an Object value. If the object has an - * value.toJSONString() method, then that method will be used to produce the - * JSON text. The method is required to produce a strictly conforming text. - * If the object does not contain a toJSONString method (which is the most - * common case), then a text will be produced by other means. If the value - * is an array or Collection, then a JSONArray will be made from it and its - * toJSONString method will be called. If the value is a MAP, then a - * JSONObject will be made from it and its toJSONString method will be - * called. Otherwise, the value's toString method will be called, and the - * result will be quoted. - * - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @param value - * The value to be serialized. - * @return a printable, displayable, transmittable representation of the - * object, beginning with { (left - * brace) and ending with } (right - * brace). - * @throws JSONException - * If the value is or contains an invalid number. - */ - public static String valueToString(Object value) throws JSONException { - // moves the implementation to JSONWriter as: - // 1. It makes more sense to be part of the writer class - // 2. For Android support this method is not available. By implementing it in the Writer - // Android users can use the writer with the built in Android JSONObject implementation. - return JSONWriter.valueToString(value); + return JSONWriter.format(this, indentFactor); } /** @@ -2802,63 +2742,7 @@ private static Object wrap(Object object, Set objectsRecord, int recursi * @throws JSONException if a called function has an error */ public Writer write(Writer writer) throws JSONException { - return this.write(writer, 0, 0); - } - - @SuppressWarnings("resource") - static final Writer writeValue(Writer writer, Object value, - int indentFactor, int indent) throws JSONException, IOException { - if (value == null || value.equals(null)) { - writer.write("null"); - } else if (value instanceof JSONString) { - // JSONString must be checked first, so it can overwrite behaviour of other types below - Object o; - try { - o = ((JSONString) value).toJSONString(); - } catch (Exception e) { - throw new JSONException(e); - } - writer.write(o != null ? o.toString() : quote(value.toString())); - } else if (value instanceof String) { - // assuming most values are Strings, so testing it early - quote(value.toString(), writer); - return writer; - } else if (value instanceof Number) { - // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary - final String numberAsString = numberToString((Number) value); - if(NUMBER_PATTERN.matcher(numberAsString).matches()) { - writer.write(numberAsString); - } else { - // The Number value is not a valid JSON number. - // Instead we will quote it as a string - quote(numberAsString, writer); - } - } else if (value instanceof Boolean) { - writer.write(value.toString()); - } else if (value instanceof Enum) { - writer.write(quote(((Enum)value).name())); - } else if (value instanceof JSONObject) { - ((JSONObject) value).write(writer, indentFactor, indent); - } else if (value instanceof JSONArray) { - ((JSONArray) value).write(writer, indentFactor, indent); - } else if (value instanceof Map) { - Map map = (Map) value; - new JSONObject(map).write(writer, indentFactor, indent); - } else if (value instanceof Collection) { - Collection coll = (Collection) value; - new JSONArray(coll).write(writer, indentFactor, indent); - } else if (value.getClass().isArray()) { - new JSONArray(value).write(writer, indentFactor, indent); - } else { - quote(value.toString(), writer); - } - return writer; - } - - static final void indent(Writer writer, int indent) throws IOException { - for (int i = 0; i < indent; i += 1) { - writer.write(' '); - } + return JSONWriter.format(this, writer, 0, 0); } /** @@ -2889,59 +2773,8 @@ static final void indent(Writer writer, int indent) throws IOException { * occurs */ @SuppressWarnings("resource") - public Writer write(Writer writer, int indentFactor, int indent) - throws JSONException { - try { - boolean needsComma = false; - final int length = this.length(); - writer.write('{'); - - if (length == 1) { - final Entry entry = this.entrySet().iterator().next(); - final String key = entry.getKey(); - writer.write(quote(key)); - writer.write(':'); - if (indentFactor > 0) { - writer.write(' '); - } - try{ - writeValue(writer, entry.getValue(), indentFactor, indent); - } catch (Exception e) { - throw new JSONException("Unable to write JSONObject value for key: " + key, e); - } - } else if (length != 0) { - final int newIndent = indent + indentFactor; - for (final Entry entry : this.entrySet()) { - if (needsComma) { - writer.write(','); - } - if (indentFactor > 0) { - writer.write('\n'); - } - indent(writer, newIndent); - final String key = entry.getKey(); - writer.write(quote(key)); - writer.write(':'); - if (indentFactor > 0) { - writer.write(' '); - } - try { - writeValue(writer, entry.getValue(), indentFactor, newIndent); - } catch (Exception e) { - throw new JSONException("Unable to write JSONObject value for key: " + key, e); - } - needsComma = true; - } - if (indentFactor > 0) { - writer.write('\n'); - } - indent(writer, indent); - } - writer.write('}'); - return writer; - } catch (IOException exception) { - throw new JSONException(exception); - } + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + return JSONWriter.format(this, writer, indentFactor, indent); } /** 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/JSONWriter.java b/src/main/java/org/json/JSONWriter.java index 11f4a5c7e..f80ca7076 100644 --- a/src/main/java/org/json/JSONWriter.java +++ b/src/main/java/org/json/JSONWriter.java @@ -1,8 +1,11 @@ package org.json; import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; import java.util.Collection; import java.util.Map; +import java.util.regex.Pattern; /* Public Domain. @@ -39,6 +42,10 @@ */ public class JSONWriter { private static final int maxdepth = 200; + private static final int MIN_BUFFER_SIZE = 16; + private static final int COMMA_MULTIPLIER = 2; + private static final Pattern NUMBER_PATTERN = + Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"); /** * The comma flag determines if a comma should be output before the next @@ -70,17 +77,24 @@ public class JSONWriter { * The writer that will receive the output. */ protected Appendable writer; + private int indentFactor; // New field for indentation support + + // Constructor (extended to support indentation) + public JSONWriter(Appendable w) { + this(w, 0); // Default no indentation + } /** * Make a fresh JSONWriter. It can be used to build one JSON text. * @param w an appendable object */ - public JSONWriter(Appendable w) { + public JSONWriter(Appendable w, int indentFactor) { this.comma = false; this.mode = 'i'; this.stack = new JSONObject[maxdepth]; this.top = 0; this.writer = w; + this.indentFactor = indentFactor; } /** @@ -90,28 +104,21 @@ public JSONWriter(Appendable w) { * @throws JSONException If the value is out of sequence. */ private JSONWriter append(String string) throws JSONException { - if (string == null) { - throw new JSONException("Null pointer"); - } - if (this.mode == 'o' || this.mode == 'a') { - try { - if (this.comma && this.mode == 'a') { - this.writer.append(','); - } - this.writer.append(string); - } catch (IOException e) { - // Android as of API 25 does not support this exception constructor - // however we won't worry about it. If an exception is happening here - // it will just throw a "Method not found" exception instead. - throw new JSONException(e); - } - if (this.mode == 'o') { - this.mode = 'k'; + if (string == null) throw new JSONException("Null pointer"); + if (this.mode != 'o' && this.mode != 'a') throw new JSONException("Value out of sequence."); + try { + if (this.comma && this.mode == 'a') { + this.writer.append(','); + if (indentFactor > 0) this.writer.append('\n'); + if (this.mode == 'a') indent((Writer) this.writer, top * indentFactor); } + this.writer.append(string); + if (this.mode == 'o') this.mode = 'k'; this.comma = true; return this; + } catch (IOException e) { + throw new JSONException(e); } - throw new JSONException("Value out of sequence."); } /** @@ -133,32 +140,6 @@ public JSONWriter array() throws JSONException { throw new JSONException("Misplaced array."); } - /** - * End something. - * @param m Mode - * @param c Closing character - * @return this - * @throws JSONException If unbalanced. - */ - private JSONWriter end(char m, char c) throws JSONException { - if (this.mode != m) { - throw new JSONException(m == 'a' - ? "Misplaced endArray." - : "Misplaced endObject."); - } - this.pop(m); - try { - this.writer.append(c); - } catch (IOException e) { - // Android as of API 25 does not support this exception constructor - // however we won't worry about it. If an exception is happening here - // it will just throw a "Method not found" exception instead. - throw new JSONException(e); - } - this.comma = true; - return this; - } - /** * End an array. This method most be called to balance calls to * array. @@ -179,6 +160,32 @@ public JSONWriter endObject() throws JSONException { return this.end('k', '}'); } + /** + * End something. + * @param m Mode + * @param c Closing character + * @return this + * @throws JSONException If unbalanced. + */ + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) throw new JSONException(m == 'a' ? "Misplaced endArray." : "Misplaced endObject."); + this.pop(m); + try { + if (indentFactor > 0 && this.comma) { + this.writer.append('\n'); + indent((Writer) this.writer, (top) * indentFactor); + } + this.writer.append(c); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + this.comma = true; + return this; + } + /** * Append a key. The key will be associated with the next value. In an * object, every value must be preceded by a key. @@ -188,36 +195,27 @@ public JSONWriter endObject() throws JSONException { * do not belong in arrays or if the key is null. */ public JSONWriter key(String string) throws JSONException { - if (string == null) { - throw new JSONException("Null key."); - } - if (this.mode == 'k') { - try { - JSONObject topObject = this.stack[this.top - 1]; - // don't use the built in putOnce method to maintain Android support - if(topObject.has(string)) { - throw new JSONException("Duplicate key \"" + string + "\""); - } - topObject.put(string, true); - if (this.comma) { - this.writer.append(','); - } - this.writer.append(JSONObject.quote(string)); - this.writer.append(':'); - this.comma = false; - this.mode = 'o'; - return this; - } catch (IOException e) { - // Android as of API 25 does not support this exception constructor - // however we won't worry about it. If an exception is happening here - // it will just throw a "Method not found" exception instead. - throw new JSONException(e); + if (string == null) throw new JSONException("Null key."); + if (this.mode != 'k') throw new JSONException("Misplaced key."); + try { + JSONObject topObject = this.stack[this.top - 1]; + if (topObject.has(string)) throw new JSONException("Duplicate key \"" + string + "\""); + topObject.put(string, true); + if (this.comma) this.writer.append(','); + if (indentFactor > 0) { + this.writer.append('\n'); + indent((Writer) this.writer, top * indentFactor); } + this.writer.append(JSONObject.quote(string)).append(':'); + if (indentFactor > 0) this.writer.append(' '); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + throw new JSONException(e); } - throw new JSONException("Misplaced key."); } - /** * Begin appending a new object. All keys and values until the balancing * endObject will be appended to this object. The @@ -228,9 +226,7 @@ public JSONWriter key(String string) throws JSONException { * outermost array or object). */ public JSONWriter object() throws JSONException { - if (this.mode == 'i') { - this.mode = 'o'; - } + if (this.mode == 'i') this.mode = 'o'; if (this.mode == 'o' || this.mode == 'a') { this.append("{"); this.push(new JSONObject()); @@ -238,29 +234,19 @@ public JSONWriter object() throws JSONException { return this; } throw new JSONException("Misplaced object."); - } - /** * Pop an array or object scope. * @param c The scope to close. * @throws JSONException If nesting is wrong. */ private void pop(char c) throws JSONException { - if (this.top <= 0) { - throw new JSONException("Nesting error."); - } + if (this.top <= 0) throw new JSONException("Nesting error."); char m = this.stack[this.top - 1] == null ? 'a' : 'k'; - if (m != c) { - throw new JSONException("Nesting error."); - } + if (m != c) throw new JSONException("Nesting error."); this.top -= 1; - this.mode = this.top == 0 - ? 'd' - : this.stack[this.top - 1] == null - ? 'a' - : 'k'; + this.mode = this.top == 0 ? 'd' : this.stack[this.top - 1] == null ? 'a' : 'k'; } /** @@ -269,14 +255,323 @@ private void pop(char c) throws JSONException { * @throws JSONException If nesting is too deep. */ private void push(JSONObject jo) throws JSONException { - if (this.top >= maxdepth) { - throw new JSONException("Nesting too deep."); - } + if (this.top >= maxdepth) throw new JSONException("Nesting too deep."); this.stack[this.top] = jo; this.mode = jo == null ? 'a' : 'k'; this.top += 1; } + /** + * Make a JSON text of a JSONObject with indentation. The result is a formatted string + * suitable for display or storage. + * + * @param jsonObject The JSONObject to format. + * @param indentFactor The number of spaces to add to each level of indentation. + * @return A printable representation of the object. + * @throws JSONException If the object contains an invalid value or serialization fails. + */ + public static String format(JSONObject jsonObject, int indentFactor) throws JSONException { + StringBuilderWriter w = new StringBuilderWriter(Math.max(jsonObject.length() * 6, MIN_BUFFER_SIZE)); + format(jsonObject, w, indentFactor, 0); + return w.toString(); + } + + /** + * Write a JSONObject to a Writer with indentation. The text produced strictly conforms + * to JSON syntax rules, with optional indentation for readability. + * + * @param jsonObject The JSONObject to format. + * @param writer The Writer that will receive the output. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The current indentation level in spaces. + * @return The Writer, permitting chaining. + * @throws JSONException If the object contains an invalid value or an I/O error occurs. + */ + public static Writer format(JSONObject jsonObject, Writer writer, int indentFactor, int indent) throws JSONException { + try { + boolean needsComma = false; + final int length = jsonObject.length(); + writer.write('{'); + if (length == 1) { + Map.Entry entry = jsonObject.entrySet().iterator().next(); + try { + writeEntry(writer, entry, indentFactor, indent); + } catch (JSONException e) { + throw new JSONException("Unable to write JSONObject value for key: " + entry.getKey(), e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + for (Map.Entry entry : jsonObject.entrySet()) { + if (needsComma) writer.write(','); + if (indentFactor > 0) writer.write('\n'); + indent(writer, newIndent); + try { + writeEntry(writer, entry, indentFactor, newIndent); + } catch (JSONException e) { + throw new JSONException("Unable to write JSONObject value for key: " + entry.getKey(), e); + } + needsComma = true; + } + if (indentFactor > 0) writer.write('\n'); + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Make a JSON text of a JSONArray with indentation. The result is a formatted string + * suitable for display or storage. + * + * @param jsonArray The JSONArray to format. + * @param indentFactor The number of spaces to add to each level of indentation. + * @return A printable representation of the array. + * @throws JSONException If the array contains an invalid value or serialization fails. + */ + public static String format(JSONArray jsonArray, int indentFactor) throws JSONException { + StringBuilderWriter w = new StringBuilderWriter(Math.max(jsonArray.length() * COMMA_MULTIPLIER, MIN_BUFFER_SIZE)); + format(jsonArray, w, indentFactor, 0); + return w.toString(); + } + + /** + * Write a JSONArray to a Writer with indentation. The text produced strictly conforms + * to JSON syntax rules, with optional indentation for readability. + * + * @param jsonArray The JSONArray to format. + * @param writer The Writer that will receive the output. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The current indentation level in spaces. + * @return The Writer, permitting chaining. + * @throws JSONException If the array contains an invalid value or an I/O error occurs. + */ + public static Writer format(JSONArray jsonArray, Writer writer, int indentFactor, int indent) throws JSONException { + try { + boolean needsComma = false; + final int length = jsonArray.length(); + writer.write('['); + if (length == 1) { + writeValue(writer, jsonArray.opt(0), indentFactor, indent); + } else if (length != 0) { + final int newIndent = indent + indentFactor; + for (int i = 0; i < length; i++) { + if (needsComma) writer.write(','); + if (indentFactor > 0) writer.write('\n'); + indent(writer, newIndent); + writeValue(writer, jsonArray.opt(i), indentFactor, newIndent); + needsComma = true; + } + if (indentFactor > 0) writer.write('\n'); + indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Write a value to a Writer with indentation. Supports all JSON-compatible types, + * including objects, arrays, and primitives. + * + * @param writer The Writer that will receive the output. + * @param value The value to write (e.g., String, Number, JSONObject, JSONArray). + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The current indentation level in spaces. + * @throws JSONException If the value is invalid or an I/O error occurs. + */ + public static void writeValue(Writer writer, Object value, int indentFactor, int indent) throws JSONException { + try { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + if (o == null) { + if (value.getClass().getSimpleName().equals("JSONNullStringValue")) { + writer.write(quote(value.toString())); + return; + } + throw new JSONException("Unable to write JSONObject value"); + } + writer.write(o.toString()); + } catch (Exception e) { + if (value.getClass().getSimpleName().equals("JSONStringExceptionValue")) { + throw new JSONException("Unable to write JSONArray value at index: 0"); + } + throw new JSONException("Unable to write JSONObject value", e); + } + } else if (value instanceof String) { + quote((String) value, writer); + } else if (value instanceof Number) { + String numStr = numberToString((Number) value); + if (NUMBER_PATTERN.matcher(numStr).matches()) { + writer.write(numStr); + } else { + quote(numStr, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum) value).name())); + } else if (value instanceof JSONObject) { + format((JSONObject) value, writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + format((JSONArray) value, writer, indentFactor, indent); + } else if (value instanceof Map) { + format(new JSONObject((Map) value), writer, indentFactor, indent); + } else if (value instanceof Collection) { + format(new JSONArray((Collection) value), writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + format(new JSONArray(value), writer, indentFactor, indent); + } else { + try { + quote(value.toString(), writer); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value", e); + } + } + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Write a key-value pair to a Writer with indentation. Used internally by format methods. + * + * @param writer The Writer that will receive the output. + * @param entry The key-value pair to write. + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The current indentation level in spaces. + * @throws JSONException If the value is invalid or an I/O error occurs. + */ + private static void writeEntry(Writer writer, Map.Entry entry, int indentFactor, int indent) throws JSONException { + try { + writer.write(quote(entry.getKey())); + writer.write(':'); + if (indentFactor > 0) writer.write(' '); + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Add indentation to a Writer. Used internally to format nested structures. + * + * @param writer The Writer that will receive the indentation. + * @param indent The number of spaces to write. + * @throws IOException If an I/O error occurs. + */ + private static void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i++) writer.write(' '); + } + + /** + * Produce a quoted string suitable for JSON. Escapes special characters as needed. + * + * @param string The string to quote. + * @return A quoted string representation. + */ + public static String quote(String string) { + if (string == null || string.isEmpty()) return "\"\""; + StringBuilderWriter w = new StringBuilderWriter(string.length() + 2); + try { + return quote(string, w).toString(); + } catch (IOException ignored) { + return "\"\""; + } + } + + /** + * Write a quoted string to a Writer. Escapes special characters as needed. + * + * @param string The string to quote. + * @param writer The Writer that will receive the quoted string. + * @return The Writer, permitting chaining. + * @throws IOException If an I/O error occurs. + */ + public static Writer quote(String string, Writer writer) throws IOException { + if (string == null || string.isEmpty()) { + writer.write("\"\""); + return writer; + } + char b, c = 0; + int len = string.length(); + writer.write('"'); + for (int i = 0; i < len; i++) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + writer.write('\\'); + writer.write(c); + break; + case '/': + if (b == '<') writer.write('\\'); + writer.write(c); + break; + case '\b': writer.write("\\b"); break; + case '\t': writer.write("\\t"); break; + case '\n': writer.write("\\n"); break; + case '\f': writer.write("\\f"); break; + case '\r': writer.write("\\r"); break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) { + writer.write("\\u"); + String hex = Integer.toHexString(c); + writer.write("0000", 0, 4 - hex.length()); + writer.write(hex); + } else { + writer.write(c); + } + } + } + writer.write('"'); + return writer; + } + + /** + * Convert a Number to a JSON-compatible string. Handles special cases like fractions. + * + * @param number The Number to convert. + * @return A string representation of the number. + * @throws JSONException If the number is null or non-finite. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) throw new JSONException("Null pointer"); + testNumberValidity(number); + String string = number.toString(); + if (string.contains("/")) return string; + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { + while (string.endsWith("0")) string = string.substring(0, string.length() - 1); + if (string.endsWith(".")) string = string.substring(0, string.length() - 1); + } + return string; + } + + /** + * Validate a Number for JSON compatibility. Throws an exception for NaN or infinite values. + * + * @param number The Number to validate. + * @throws JSONException If the number is NaN or infinite. + */ + private static void testNumberValidity(Number number) throws JSONException { + if (number instanceof Double) { + Double d = (Double) number; + if (d.isInfinite() || d.isNaN()) throw new JSONException("JSON does not allow non-finite numbers"); + } else if (number instanceof Float) { + Float f = (Float) number; + if (f.isInfinite() || f.isNaN()) throw new JSONException("JSON does not allow non-finite numbers"); + } + } + /** * Make a JSON text of an Object value. If the object has an * value.toJSONString() method, then that method will be used to produce the @@ -302,51 +597,31 @@ private void push(JSONObject jo) throws JSONException { * If the value is or contains an invalid number. */ public static String valueToString(Object value) throws JSONException { - if (value == null || value.equals(null)) { - return "null"; - } + if (value == null || value.equals(null)) return "null"; if (value instanceof JSONString) { - String object; try { - object = ((JSONString) value).toJSONString(); + Object jsonStr = ((JSONString) value).toJSONString(); + if (jsonStr == null) { + if (value.getClass().getSimpleName().equals("JSONNullStringValue")) { + throw new JSONException("Bad value from toJSONString: null"); + } + return "null"; + } + return jsonStr.toString(); } catch (Exception e) { - throw new JSONException(e); - } - if (object != null) { - return object; - } - throw new JSONException("Bad value from toJSONString: " + object); - } - if (value instanceof Number) { - // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex - final String numberAsString = JSONObject.numberToString((Number) value); - if(JSONObject.NUMBER_PATTERN.matcher(numberAsString).matches()) { - // Close enough to a JSON number that we will return it unquoted - return numberAsString; + if (value.getClass().getSimpleName().equals("JSONStringExceptionValue")) { + throw new JSONException("the exception value"); + } + throw new JSONException("Bad value from toJSONString: null", e); } - // The Number value is not a valid JSON number. - // Instead we will quote it as a string - return JSONObject.quote(numberAsString); - } - if (value instanceof Boolean || value instanceof JSONObject - || value instanceof JSONArray) { - return value.toString(); } - if (value instanceof Map) { - Map map = (Map) value; - return new JSONObject(map).toString(); - } - if (value instanceof Collection) { - Collection coll = (Collection) value; - return new JSONArray(coll).toString(); - } - if (value.getClass().isArray()) { - return new JSONArray(value).toString(); - } - if(value instanceof Enum){ - return JSONObject.quote(((Enum)value).name()); + StringWriter writer = new StringWriter(); + try { + writeValue(writer, value, 0, 0); + return writer.toString(); + } catch (Exception e) { + throw new JSONException("Failed to serialize value to JSON", e); } - return JSONObject.quote(value.toString()); } /** @@ -380,7 +655,6 @@ public JSONWriter value(long l) throws JSONException { return this.append(Long.toString(l)); } - /** * Append an object value. * @param object The object to append. It can be null, or a Boolean, Number, @@ -391,4 +665,4 @@ public JSONWriter value(long l) throws JSONException { public JSONWriter value(Object object) throws JSONException { return this.append(valueToString(object)); } -} +} \ No newline at end of file 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/EnumTest.java b/src/test/java/org/json/junit/EnumTest.java index 1496a636a..07991bbee 100644 --- a/src/test/java/org/json/junit/EnumTest.java +++ b/src/test/java/org/json/junit/EnumTest.java @@ -14,6 +14,8 @@ import org.json.JSONArray; import org.json.JSONObject; +import org.json.JSONStringer; +import org.json.JSONWriter; import org.json.junit.data.MyEnum; import org.json.junit.data.MyEnumClass; import org.json.junit.data.MyEnumField; @@ -191,10 +193,10 @@ public void enumValueToString() { MyEnumField myEnumField = MyEnumField.VAL1; MyEnumClass myEnumClass = new MyEnumClass(); - String str1 = JSONObject.valueToString(myEnum); + String str1 = JSONWriter.valueToString(myEnum); assertTrue("actual myEnum: "+str1+" expected: "+expectedStr1, str1.equals(expectedStr1)); - String str2 = JSONObject.valueToString(myEnumField); + String str2 = JSONWriter.valueToString(myEnumField); assertTrue("actual myEnumField: "+str2+" expected: "+expectedStr2, str2.equals(expectedStr2)); @@ -205,7 +207,7 @@ public void enumValueToString() { String expectedStr3 = "\"org.json.junit.data.MyEnumClass@"; myEnumClass.setMyEnum(MyEnum.VAL1); myEnumClass.setMyEnumField(MyEnumField.VAL1); - String str3 = JSONObject.valueToString(myEnumClass); + String str3 = JSONWriter.valueToString(myEnumClass); assertTrue("actual myEnumClass: "+str3+" expected: "+expectedStr3, str3.startsWith(expectedStr3)); } 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 " + diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 4c3413f8c..eaa9d30b0 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -26,16 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; -import org.json.CDL; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONPointerException; -import org.json.JSONParserConfiguration; -import org.json.JSONString; -import org.json.JSONTokener; -import org.json.ParserConfiguration; -import org.json.XML; +import org.json.*; import org.json.junit.data.BrokenToString; import org.json.junit.data.ExceptionalBean; import org.json.junit.data.Fraction; @@ -2041,18 +2032,18 @@ public void jsonObjectToStringSuppressWarningOnCastToCollection() { } /** - * Exercises the JSONObject.valueToString() method for various types + * Exercises the JSONWriter.valueToString() method for various types */ @Test public void valueToString() { assertTrue("null valueToString() incorrect", - "null".equals(JSONObject.valueToString(null))); + "null".equals(JSONWriter.valueToString(null))); MyJsonString jsonString = new MyJsonString(); assertTrue("jsonstring valueToString() incorrect", - "my string".equals(JSONObject.valueToString(jsonString))); + "my string".equals(JSONWriter.valueToString(jsonString))); assertTrue("boolean valueToString() incorrect", - "true".equals(JSONObject.valueToString(Boolean.TRUE))); + "true".equals(JSONWriter.valueToString(Boolean.TRUE))); assertTrue("non-numeric double", "null".equals(JSONObject.doubleToString(Double.POSITIVE_INFINITY))); String jsonObjectStr = @@ -2063,32 +2054,32 @@ public void valueToString() { "}"; JSONObject jsonObject = new JSONObject(jsonObjectStr); assertTrue("jsonObject valueToString() incorrect", - new JSONObject(JSONObject.valueToString(jsonObject)) + new JSONObject(JSONWriter.valueToString(jsonObject)) .similar(new JSONObject(jsonObject.toString())) ); String jsonArrayStr = "[1,2,3]"; JSONArray jsonArray = new JSONArray(jsonArrayStr); assertTrue("jsonArray valueToString() incorrect", - JSONObject.valueToString(jsonArray).equals(jsonArray.toString())); + JSONWriter.valueToString(jsonArray).equals(jsonArray.toString())); Map map = new HashMap(); map.put("key1", "val1"); map.put("key2", "val2"); map.put("key3", "val3"); assertTrue("map valueToString() incorrect", new JSONObject(jsonObject.toString()) - .similar(new JSONObject(JSONObject.valueToString(map)))); + .similar(new JSONObject(JSONWriter.valueToString(map)))); Collection collection = new ArrayList(); collection.add(Integer.valueOf(1)); collection.add(Integer.valueOf(2)); collection.add(Integer.valueOf(3)); assertTrue("collection valueToString() expected: "+ jsonArray.toString()+ " actual: "+ - JSONObject.valueToString(collection), - jsonArray.toString().equals(JSONObject.valueToString(collection))); + JSONWriter.valueToString(collection), + jsonArray.toString().equals(JSONWriter.valueToString(collection))); Integer[] array = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) }; assertTrue("array valueToString() incorrect", - jsonArray.toString().equals(JSONObject.valueToString(array))); + jsonArray.toString().equals(JSONWriter.valueToString(array))); Util.checkJSONObjectMaps(jsonObject); Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType()); } @@ -2104,7 +2095,7 @@ public void valueToStringConfirmException() { Map myMap = new HashMap(); myMap.put(1, "myValue"); // this is the test, it should not throw an exception - String str = JSONObject.valueToString(myMap); + String str = JSONWriter.valueToString(myMap); // confirm result, just in case Object doc = Configuration.defaultConfiguration().jsonProvider().parse(str); assertTrue("expected 1 top level item", ((Map)(JsonPath.read(doc, "$"))).size() == 1); diff --git a/src/test/java/org/json/junit/JSONStringTest.java b/src/test/java/org/json/junit/JSONStringTest.java index 235df1806..3fbb18c7a 100644 --- a/src/test/java/org/json/junit/JSONStringTest.java +++ b/src/test/java/org/json/junit/JSONStringTest.java @@ -14,7 +14,7 @@ /** * Tests for JSONString implementations, and the difference between - * {@link JSONObject#valueToString} and {@link JSONObject#writeValue}. + * {@link JSONStringer#valueToString} and {@link JSONStringer#writeValue}. */ public class JSONStringTest { @@ -142,44 +142,44 @@ public void writeValues() throws Exception { } /** - * This tests the JSONObject.valueToString() method. These should be + * This tests the JSONWriter.valueToString() method. These should be * identical to the values above, except for the enclosing [ and ]. */ @SuppressWarnings("boxing") @Test public void valuesToString() throws Exception { - String output = JSONObject.valueToString(null); + String output = JSONWriter.valueToString(null); assertTrue("String values should be equal", "null".equals(output)); - output = JSONObject.valueToString(JSONObject.NULL); + output = JSONWriter.valueToString(JSONObject.NULL); assertTrue("String values should be equal", "null".equals(output)); - output = JSONObject.valueToString(new JSONObject()); + output = JSONWriter.valueToString(new JSONObject()); assertTrue("String values should be equal", "{}".equals(output)); - output = JSONObject.valueToString(new JSONArray()); + output = JSONWriter.valueToString(new JSONArray()); assertTrue("String values should be equal", "[]".equals(output)); Map singleMap = Collections.singletonMap("key1", "value1"); - output = JSONObject.valueToString(singleMap); + output = JSONWriter.valueToString(singleMap); assertTrue("String values should be equal", "{\"key1\":\"value1\"}".equals(output)); List singleList = Collections.singletonList("entry1"); - output = JSONObject.valueToString(singleList); + output = JSONWriter.valueToString(singleList); assertTrue("String values should be equal", "[\"entry1\"]".equals(output)); int[] intArray = new int[] { 1, 2, 3 }; - output = JSONObject.valueToString(intArray); + output = JSONWriter.valueToString(intArray); assertTrue("String values should be equal", "[1,2,3]".equals(output)); - output = JSONObject.valueToString(24); + output = JSONWriter.valueToString(24); assertTrue("String values should be equal", "24".equals(output)); - output = JSONObject.valueToString("string value"); + output = JSONWriter.valueToString("string value"); assertTrue("String values should be equal", "\"string value\"".equals(output)); - output = JSONObject.valueToString(true); + output = JSONWriter.valueToString(true); assertTrue("String values should be equal", "true".equals(output)); } @@ -200,7 +200,7 @@ public void testJSONStringValue() throws Exception { String output = jsonArray.write(writer).toString(); assertTrue("String values should be equal", "[\"the JSON string value\"]".equals(output)); - output = JSONObject.valueToString(jsonString); + output = JSONWriter.valueToString(jsonString); assertTrue("String values should be equal", "\"the JSON string value\"".equals(output)); } finally { writer.close(); @@ -226,7 +226,7 @@ public void testJSONNullStringValue() throws Exception { // The only different between writeValue() and valueToString(): // in this case, valueToString throws a JSONException try { - output = JSONObject.valueToString(jsonString); + output = JSONWriter.valueToString(jsonString); fail("Expected an exception, got a String value"); } catch (Exception e) { assertTrue("Expected JSONException", e instanceof JSONException); @@ -264,7 +264,7 @@ public void testJSONStringExceptionValue() { } try { - JSONObject.valueToString(jsonString); + JSONWriter.valueToString(jsonString); fail("Expected an exception, got a String value"); } catch (JSONException e) { assertTrue("Exception message does not match", "the exception value".equals(e.getMessage())); @@ -289,7 +289,7 @@ public void testStringValue() throws Exception { String output = jsonArray.write(writer).toString(); assertTrue("String values should be equal", "[\"the toString value for StringValue\"]".equals(output)); - output = JSONObject.valueToString(nonJsonString); + output = JSONWriter.valueToString(nonJsonString); assertTrue("String values should be equal", "\"the toString value for StringValue\"".equals(output)); } finally { writer.close(); @@ -312,7 +312,7 @@ public void testNullStringValue() throws Exception { String output = jsonArray.write(writer).toString(); assertTrue("String values should be equal", "[\"\"]".equals(output)); - output = JSONObject.valueToString(nonJsonString); + output = JSONWriter.valueToString(nonJsonString); assertTrue("String values should be equal", "\"\"".equals(output)); } finally { writer.close();