diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index df4bf7981..693699456 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -40,4 +40,4 @@ jobs: - run: "mvn clean compile -Dmaven.test.skip=true -Dmaven.site.skip=true -Dmaven.javadoc.skip=true" - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d59702cae..85aea5501 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -15,7 +15,7 @@ jobs: name: Java 1.6 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup java uses: actions/setup-java@v1 with: @@ -30,7 +30,7 @@ jobs: jar cvf target/org.json.jar -C target/classes . - name: Upload JAR 1.6 if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Create java 1.6 JAR path: target/*.jar @@ -45,9 +45,9 @@ jobs: java: [ 8 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -64,13 +64,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -78,7 +78,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar @@ -93,9 +93,9 @@ jobs: java: [ 11 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -112,13 +112,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -126,7 +126,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar @@ -141,9 +141,9 @@ jobs: java: [ 17 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -160,13 +160,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -174,7 +174,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar @@ -189,9 +189,9 @@ jobs: java: [ 21 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -208,13 +208,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -222,7 +222,56 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar + + build-25: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 1 + matrix: + # build against supported Java LTS versions: + java: [ 25 ] + name: Java ${{ matrix.java }} + steps: + - uses: actions/checkout@v5 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + - name: Compile Java ${{ matrix.java }} + run: mvn clean compile -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true -D maven.javadoc.skip=true + - name: Run Tests ${{ matrix.java }} + run: | + mvn test -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} + - name: Build Test Report ${{ matrix.java }} + if: ${{ always() }} + run: | + mvn surefire-report:report-only -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} + mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} + - name: Upload Test Results ${{ matrix.java }} + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: Test Results ${{ matrix.java }} + path: target/surefire-reports/ + - name: Upload Test Report ${{ matrix.java }} + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: Test Report ${{ matrix.java }} + path: target/site/ + - name: Package Jar ${{ matrix.java }} + run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true + - name: Upload Package Results ${{ matrix.java }} + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: Package Jar ${{ matrix.java }} + path: target/*.jar + diff --git a/.gitignore b/.gitignore index b78af4db7..0e08d645c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ build /gradlew /gradlew.bat .gitmodules + +# ignore compiled class files +*.class diff --git a/README.md b/README.md index 206afbb21..75a5d37a1 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ JSON in Java [package org.json] [![Maven Central](https://img.shields.io/maven-central/v/org.json/json.svg)](https://mvnrepository.com/artifact/org.json/json) [![Java CI with Maven](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml/badge.svg)](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml) [![CodeQL](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml) +[![javadoc](https://javadoc.io/badge2/org.json/json/javadoc.svg)](https://javadoc.io/doc/org.json/json) -**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20250107/json-20250107.jar)** +**[Click here if you just want the latest release jar file.](https://repo1.maven.org/maven2/org/json/json/20260719/json-20260719.jar)** # Overview @@ -19,6 +20,8 @@ JSON in Java [package org.json] The JSON-Java package is a reference implementation that demonstrates how to parse JSON documents into Java objects and how to generate new JSON documents from the Java classes. +The files in this package implement JSON encoders and decoders. The package can also convert between JSON and XML, HTTP headers, Cookies, and CDL. + Project goals include: * Reliable and consistent results * Adherence to the JSON specification @@ -26,10 +29,21 @@ Project goals include: * No external dependencies * Fast execution and low memory footprint * Maintain backward compatibility -* Designed and tested to use on Java versions 1.6 - 21 +* Designed and tested to use on Java versions 1.6 - 25 +# License Clarification +This project is in the public domain. This means: +* You can use this code for any purpose, including commercial projects +* No attribution or credit is required +* You can modify, distribute, and sublicense freely +* There are no conditions or restrictions whatsoever -The files in this package implement JSON encoders and decoders. The package can also convert between JSON and XML, HTTP headers, Cookies, and CDL. +We recognize this can create uncertainty for some corporate legal departments accustomed to standard licenses like MIT or Apache 2.0. +If your organization requires a named license for compliance purposes, public domain is functionally equivalent to the Unlicense or CC0 1.0, both of which have been reviewed and accepted by organizations including the Open Source Initiative and Creative Commons. You may reference either when explaining this project's terms to your legal team. + +# Signing keys used in releases + +The signing keys can be found in [SECURITY.md](https://github.com/stleary/JSON-java/blob/master/docs/SECURITY.md) # If you would like to contribute to this project diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 5af9a566b..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -Please follow the instructions in the ["How are vulnerabilities and exploits handled?"](https://github.com/stleary/JSON-java/wiki/FAQ#how-are-vulnerabilities-and-exploits-handled) section in the FAQ. diff --git a/build.gradle b/build.gradle index 3961e854d..f9251816c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,10 @@ */ apply plugin: 'java' apply plugin: 'eclipse' -// apply plugin: 'jacoco' +apply plugin: 'jacoco' apply plugin: 'maven-publish' +// for now, publishing to maven is still a manual process //plugins { // id 'java' //id 'maven-publish' @@ -19,6 +20,17 @@ repositories { } } +// To view the report open build/reports/jacoco/test/html/index.html +jacocoTestReport { + reports { + html.required = true + } +} + +test { + finalizedBy jacocoTestReport +} + dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'com.jayway.jsonpath:json-path:2.9.0' @@ -30,7 +42,7 @@ subprojects { } group = 'org.json' -version = 'v20250107-SNAPSHOT' +version = 'v20260719-SNAPSHOT' description = 'JSON in Java' sourceCompatibility = '1.8' diff --git a/docs/RELEASES.md b/docs/RELEASES.md index 8e598f929..43edf3ae5 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -5,9 +5,17 @@ and artifactId "json". For example: [https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav](https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav) ~~~ -20250107....Restore moditect in pom.xml +20260719 Fixes CVE-2026-59171 very large BigInteger, BigDecimal -20241224....Strict mode opt-in feature, and recent commits. This release does not contain module-info.class. +20260522 Publish key data, recent commits for minor fixes + +20251224 Records, fromJson(), and recent commits + +20250517 Strict mode hardening and recent commits + +20250107 Restore moditect in pom.xml + +20241224 Strict mode opt-in feature, and recent commits. This release does not contain module-info.class. It is not recommended if you need this feature. 20240303 Revert optLong/getLong changes, and recent commits. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 5af9a566b..3a1e60ebe 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -3,3 +3,58 @@ ## Reporting a Vulnerability Please follow the instructions in the ["How are vulnerabilities and exploits handled?"](https://github.com/stleary/JSON-java/wiki/FAQ#how-are-vulnerabilities-and-exploits-handled) section in the FAQ. + +## Verifying Release Signatures + +All releases of `org.json:json` published to Maven Central are signed with PGP. The fingerprint, keyserver location, and verification procedure below let you confirm that the artifacts you've downloaded were produced by this project and have not been modified in transit. + +### Signing Key + +| | | +| --- | --- | +| **Fingerprint** | `FB35 C8D0 2B47 24DA DA23 DE0A FD11 6C19 69FC CFF3` | +| **Long key ID** | `FD116C1969FCCFF3` | +| **Keyserver** | `hkps://keyserver.ubuntu.com` | + +The full 40-character fingerprint above is the canonical identifier for the key. Always pin or compare against the full fingerprint rather than the long or short key ID. + +### Importing the Key + +```bash +gpg --keyserver hkps://keyserver.ubuntu.com \ + --recv-keys FB35C8D02B4724DADA23DE0AFD116C1969FCCFF3 +``` + +After importing, confirm the fingerprint matches what's published here: + +```bash +gpg --fingerprint FB35C8D02B4724DADA23DE0AFD116C1969FCCFF3 +``` + +### Verifying an Artifact + +Download both the artifact and its detached signature from Maven Central. For example, for version `20251224`: + +```bash +curl -O https://repo1.maven.org/maven2/org/json/json/20251224/json-20251224.jar +curl -O https://repo1.maven.org/maven2/org/json/json/20251224/json-20251224.jar.asc +gpg --verify json-20251224.jar.asc json-20251224.jar +``` + +A successful verification will report `Good signature from ...` and display the same fingerprint shown above. If GPG reports `BAD signature`, a mismatched fingerprint, or `No public key`, do not use the artifact and please open an issue. + +The same procedure applies to the `.pom` and any other signed sidecars in the release directory; substitute the filename you want to verify. + +### Gradle Dependency Verification + +If you are using Gradle's [dependency verification](https://docs.gradle.org/current/userguide/dependency_verification.html) feature, add an entry like the following to `gradle/verification-metadata.xml`: + +```xml + +``` + +Gradle also accepts the long key ID (`FD116C1969FCCFF3`), but pinning the full fingerprint is recommended. + +### Key Rotation + +If the signing key is ever rotated or revoked, this document will be updated in the `master` branch with the new fingerprint, and the change will be visible in the file's commit history. Always check this file directly in the repository for the current authoritative value before trusting any third-party copy of the fingerprint. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 80c9b89c0..d5fb42ddc 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.json json - 20250107 + 20260719 bundle JSON in Java @@ -198,6 +198,15 @@ maven-jar-plugin 3.3.0 + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + + diff --git a/src/main/java/org/json/CDL.java b/src/main/java/org/json/CDL.java index b495de12b..0e8046798 100644 --- a/src/main/java/org/json/CDL.java +++ b/src/main/java/org/json/CDL.java @@ -27,7 +27,9 @@ public class CDL { /** * Constructs a new CDL object. + * @deprecated (Utility class cannot be instantiated) */ + @Deprecated public CDL() { } @@ -100,11 +102,15 @@ public static JSONArray rowToJSONArray(JSONTokener x, char delimiter) throws JSO for (;;) { String value = getValue(x,delimiter); char c = x.next(); - if (value == null || - (ja.length() == 0 && value.length() == 0 && c != delimiter)) { + if (value != null) { + ja.put(value); + } else if (ja.length() == 0 && c != delimiter) { return null; + } else { + // This line accounts for CSV ending with no newline + ja.put(""); } - ja.put(value); + for (;;) { if (c == delimiter) { break; @@ -177,29 +183,71 @@ public static String rowToString(JSONArray ja, char delimiter) { sb.append(delimiter); } Object object = ja.opt(i); - if (object != null) { - String string = object.toString(); - if (string.length() > 0 && (string.indexOf(delimiter) >= 0 || - string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || - string.indexOf(0) >= 0 || string.charAt(0) == '"')) { - sb.append('"'); - int length = string.length(); - for (int j = 0; j < length; j += 1) { - char c = string.charAt(j); - if (c >= ' ' && c != '"') { - sb.append(c); - } - } - sb.append('"'); - } else { - sb.append(string); - } - } + appendRowValue(sb, object, delimiter); } sb.append('\n'); return sb.toString(); } + /** + * Append a single row value, quoting it when required by the delimiter or + * content. + * + * @param sb the destination buffer + * @param object the value to append + * @param delimiter the delimiter used between row values + */ + private static void appendRowValue(StringBuilder sb, Object object, char delimiter) { + if (object == null) { + return; + } + String string = object.toString(); + if (shouldQuoteValue(string, delimiter)) { + appendQuotedValue(sb, string); + } else { + sb.append(string); + } + } + + /** + * Determine whether a row value should be quoted. + * + * @param value the row value to evaluate + * @param delimiter the delimiter used between row values + * @return {@code true} if the value should be quoted + */ + private static boolean shouldQuoteValue(String value, char delimiter) { + if (value.isEmpty()) { + return false; + } + boolean containsDelimiter = value.indexOf(delimiter) >= 0; + boolean containsNewline = value.indexOf('\n') >= 0; + boolean containsCarriageReturn = value.indexOf('\r') >= 0; + boolean containsNullCharacter = value.indexOf(0) >= 0; + boolean startsWithQuote = value.charAt(0) == '"'; + return containsDelimiter || containsNewline || containsCarriageReturn || + containsNullCharacter || startsWithQuote; + } + + /** + * Append a row value surrounded by quotes, omitting characters that should + * not appear inside the quoted value. + * + * @param sb the destination buffer + * @param value the value to append + */ + private static void appendQuotedValue(StringBuilder sb, String value) { + sb.append('"'); + int length = value.length(); + for (int j = 0; j < length; j += 1) { + char c = value.charAt(j); + if (c >= ' ' && c != '"') { + sb.append(c); + } + } + sb.append('"'); + } + /** * Produce a JSONArray of JSONObjects from a comma delimited text string, * using the first row as a source of names. @@ -307,6 +355,17 @@ public static JSONArray toJSONArray(JSONArray names, JSONTokener x, char delimit if (ja.length() == 0) { return null; } + + // The following block accounts for empty datasets (no keys or vals) + if (ja.length() == 1) { + JSONObject j = ja.getJSONObject(0); + if (j.length() == 1) { + String key = j.keys().next(); + if ("".equals(key) && "".equals(j.get(key))) { + return null; + } + } + } return ja; } diff --git a/src/main/java/org/json/Cookie.java b/src/main/java/org/json/Cookie.java index ab908a304..f7bab236f 100644 --- a/src/main/java/org/json/Cookie.java +++ b/src/main/java/org/json/Cookie.java @@ -17,7 +17,9 @@ public class Cookie { /** * Constructs a new Cookie object. + * @deprecated (Utility class cannot be instantiated) */ + @Deprecated() public Cookie() { } diff --git a/src/main/java/org/json/CookieList.java b/src/main/java/org/json/CookieList.java index d1064db52..ce47aee02 100644 --- a/src/main/java/org/json/CookieList.java +++ b/src/main/java/org/json/CookieList.java @@ -13,7 +13,9 @@ public class CookieList { /** * Constructs a new CookieList object. + * @deprecated (Utility class cannot be instantiated) */ + @Deprecated public CookieList() { } diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index e2725b7a9..0d7fde9df 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -105,6 +105,8 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) if (nextChar == 0) { // array is unclosed. No ']' found, instead EOF throw x.syntaxError("Expected a ',' or ']'"); + } else if (nextChar==',' && jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Array content starts with a ','"); } if (nextChar != ']') { x.back(); @@ -116,41 +118,7 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) x.back(); this.myArrayList.add(x.nextValue()); } - switch (x.nextClean()) { - case 0: - // array is unclosed. No ']' found, instead EOF - throw x.syntaxError("Expected a ',' or ']'"); - case ',': - nextChar = x.nextClean(); - if (nextChar == 0) { - // array is unclosed. No ']' found, instead EOF - throw x.syntaxError("Expected a ',' or ']'"); - } - if (nextChar == ']') { - // trailing commas are not allowed in strict mode - if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Strict mode error: Expected another array element"); - } - return; - } - if (nextChar == ',') { - // consecutive commas are not allowed in strict mode - if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Strict mode error: Expected a valid array element"); - } - return; - } - x.back(); - break; - case ']': - if (isInitial && jsonParserConfiguration.isStrictMode() && - x.nextClean() != 0) { - throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); - } - return; - default: - throw x.syntaxError("Expected a ',' or ']'"); - } + if (checkForSyntaxError(x, jsonParserConfiguration, isInitial)) return; } } else { if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { @@ -159,6 +127,52 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) } } + /** Convenience function. Checks for JSON syntax error. + * @param x A JSONTokener instance from which the JSONArray is constructed. + * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. + * @param isInitial Boolean indicating position of char + * @return true if a syntax error has occurred, otherwise false + */ + private boolean checkForSyntaxError(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) { + char nextChar; + switch (x.nextClean()) { + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + // trailing commas are not allowed in strict mode + if (jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Strict mode error: Expected another array element"); + } + return true; + } + if (nextChar == ',') { + // Consecutive commas are not allowed in strict mode. + // Otherwise, the tokener is backed up, and a null object is inserted by the calling code. + if (jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Strict mode error: Expected a valid array element"); + } + } + x.back(); + break; + case ']': + if (isInitial && jsonParserConfiguration.isStrictMode() && + x.nextClean() != 0) { + throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); + } + return true; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + return false; + } + /** * Construct a JSONArray from a source JSON text. * @@ -334,13 +348,11 @@ public Object get(int index) throws JSONException { */ 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 (Boolean.FALSE.equals(object) + || (object instanceof String && "false".equalsIgnoreCase((String) object))) { return false; - } else if (object.equals(Boolean.TRUE) - || (object instanceof String && ((String) object) - .equalsIgnoreCase("true"))) { + } else if (Boolean.TRUE.equals(object) + || (object instanceof String && "true".equalsIgnoreCase((String) object))) { return true; } throw wrongValueFormatException(index, "boolean", object, null); @@ -471,8 +483,29 @@ public BigDecimal getBigDecimal (int index) throws JSONException { * to a BigInteger. */ public BigInteger getBigInteger (int index) throws JSONException { + return this.getBigInteger(index, new JSONParserConfiguration()); + } + + /** + * Get the BigInteger value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible. Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigInteger. + */ + public BigInteger getBigInteger (int index, JSONParserConfiguration jsonParserConfiguration) + throws JSONException { Object object = this.get(index); - BigInteger val = JSONObject.objectToBigInteger(object, null); + BigInteger val = JSONObject.objectToBigInteger(object, null, jsonParserConfiguration); if(val == null) { throw wrongValueFormatException(index, "BigInteger", object, null); } @@ -735,11 +768,7 @@ public double optDouble(int index, double defaultValue) { if (val == null) { return defaultValue; } - final double doubleValue = val.doubleValue(); - // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { - // return defaultValue; - // } - return doubleValue; + return val.doubleValue(); } /** @@ -771,11 +800,7 @@ public Double optDoubleObject(int index, Double defaultValue) { if (val == null) { return defaultValue; } - final Double doubleValue = val.doubleValue(); - // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { - // return defaultValue; - // } - return doubleValue; + return val.doubleValue(); } /** @@ -807,11 +832,7 @@ public float optFloat(int index, float defaultValue) { if (val == null) { return defaultValue; } - final float floatValue = val.floatValue(); - // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { - // return floatValue; - // } - return floatValue; + return val.floatValue(); } /** @@ -843,11 +864,7 @@ public Float optFloatObject(int index, Float defaultValue) { if (val == null) { return defaultValue; } - final Float floatValue = val.floatValue(); - // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { - // return floatValue; - // } - return floatValue; + return val.floatValue(); } /** @@ -964,8 +981,8 @@ public > E optEnum(Class clazz, int index, E defaultValue) } /** - * Get the optional BigInteger value associated with an index. The - * defaultValue is returned if there is no value for the index, or if the + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the * value is not a number and cannot be converted to a number. * * @param index @@ -975,8 +992,31 @@ public > E optEnum(Class clazz, int index, E defaultValue) * @return The value. */ public BigInteger optBigInteger(int index, BigInteger defaultValue) { + return this.optBigInteger(index, defaultValue, new JSONParserConfiguration()); + } + + /** + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible and {@code defaultValue} is returned. + * Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return The value. + */ + public BigInteger optBigInteger(int index, BigInteger defaultValue, + JSONParserConfiguration jsonParserConfiguration) { Object val = this.opt(index); - return JSONObject.objectToBigInteger(val, defaultValue); + return JSONObject.objectToBigInteger(val, defaultValue, jsonParserConfiguration); } /** @@ -1645,25 +1685,40 @@ public boolean similar(Object other) { 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 (!isSimilar(valueThis, valueOther)) { + return false; + } + } + return true; + } + + /** + * Convenience function; checks for object similarity + * @param valueThis + * Initial object to compare + * @param valueOther + * Comparison object + * @return boolean + */ + private boolean isSimilar(Object valueThis, Object valueOther) { + 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)) { + return false; } return true; } @@ -1799,12 +1854,7 @@ public Writer write(Writer writer, int indentFactor, int indent) 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); - } + writeArrayAttempt(writer, indentFactor, indent, 0); } else if (length != 0) { final int newIndent = indent + indentFactor; @@ -1816,12 +1866,7 @@ public Writer write(Writer writer, int indentFactor, int indent) 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); - } + writeArrayAttempt(writer, indentFactor, newIndent, i); needsComma = true; } if (indentFactor > 0) { @@ -1836,6 +1881,26 @@ public Writer write(Writer writer, int indentFactor, int indent) } } + /** + * Convenience function. Attempts to write + * @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. + * @param i + * Index in array to be added + */ + private void writeArrayAttempt(Writer writer, int indentFactor, int indent, int i) { + try { + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + i, e); + } + } + /** * 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..6ec997061 100644 --- a/src/main/java/org/json/JSONML.java +++ b/src/main/java/org/json/JSONML.java @@ -16,10 +16,40 @@ public class JSONML { /** * Constructs a new JSONML object. + * @deprecated (Utility class cannot be instantiated) */ + @Deprecated public JSONML() { } + /** + * Safely cast parse result to JSONArray with proper type checking. + * @param result The result from parse() method + * @return JSONArray if result is a JSONArray + * @throws JSONException if result is not a JSONArray + */ + private static JSONArray toJSONArraySafe(Object result) throws JSONException { + if (result instanceof JSONArray) { + return (JSONArray) result; + } + throw new JSONException("Expected JSONArray but got " + + (result == null ? "null" : result.getClass().getSimpleName())); + } + + /** + * Safely cast parse result to JSONObject with proper type checking. + * @param result The result from parse() method + * @return JSONObject if result is a JSONObject + * @throws JSONException if result is not a JSONObject + */ + private static JSONObject toJSONObjectSafe(Object result) throws JSONException { + if (result instanceof JSONObject) { + return (JSONObject) result; + } + throw new JSONException("Expected JSONObject but got " + + (result == null ? "null" : result.getClass().getSimpleName())); + } + /** * Parse XML values and store them in a JSONArray. * @param x The XMLTokener containing the source string. @@ -111,7 +141,7 @@ private static Object parse( } } else if (c == '[') { token = x.nextToken(); - if (token.equals("CDATA") && x.next() == '[') { + if ("CDATA".equals(token) && x.next() == '[') { if (ja != null) { ja.put(x.nextCDATA()); } @@ -239,9 +269,21 @@ private static Object parse( } } else { if (ja != null) { - ja.put(token instanceof String - ? (config.isKeepStrings() ? XML.unescape((String)token) : XML.stringToValue((String)token)) - : token); + Object value; + + if (token instanceof String) { + String strToken = (String) token; + if (config.isKeepStrings()) { + value = XML.unescape(strToken); + } else { + value = XML.stringToValue(strToken); + } + } else { + value = token; + } + + ja.put(value); + } } } @@ -261,7 +303,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 toJSONArraySafe(parse(new XMLTokener(string), true, null, JSONMLParserConfiguration.ORIGINAL, 0)); } @@ -283,7 +325,7 @@ public static JSONArray toJSONArray(String string) throws JSONException { * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings, 0); + return toJSONArraySafe(parse(new XMLTokener(string), true, null, keepStrings, 0)); } @@ -308,7 +350,7 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string, JSONMLParserConfiguration config) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, config, 0); + return toJSONArraySafe(parse(new XMLTokener(string), true, null, config, 0)); } @@ -332,7 +374,7 @@ public static JSONArray toJSONArray(String string, JSONMLParserConfiguration con * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x, JSONMLParserConfiguration config) throws JSONException { - return (JSONArray)parse(x, true, null, config, 0); + return toJSONArraySafe(parse(x, true, null, config, 0)); } @@ -354,7 +396,7 @@ public static JSONArray toJSONArray(XMLTokener x, JSONMLParserConfiguration conf * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { - return (JSONArray)parse(x, true, null, keepStrings, 0); + return toJSONArraySafe(parse(x, true, null, keepStrings, 0)); } @@ -371,7 +413,7 @@ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JS * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x) throws JSONException { - return (JSONArray)parse(x, true, null, false, 0); + return toJSONArraySafe(parse(x, true, null, false, 0)); } @@ -389,7 +431,7 @@ public static JSONArray toJSONArray(XMLTokener x) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, false, 0); + return toJSONObjectSafe(parse(new XMLTokener(string), false, null, false, 0)); } @@ -409,7 +451,7 @@ public static JSONObject toJSONObject(String string) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings, 0); + return toJSONObjectSafe(parse(new XMLTokener(string), false, null, keepStrings, 0)); } @@ -431,7 +473,7 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string, JSONMLParserConfiguration config) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, config, 0); + return toJSONObjectSafe(parse(new XMLTokener(string), false, null, config, 0)); } @@ -449,7 +491,7 @@ public static JSONObject toJSONObject(String string, JSONMLParserConfiguration c * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x) throws JSONException { - return (JSONObject)parse(x, false, null, false, 0); + return toJSONObjectSafe(parse(x, false, null, false, 0)); } @@ -469,7 +511,7 @@ public static JSONObject toJSONObject(XMLTokener x) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { - return (JSONObject)parse(x, false, null, keepStrings, 0); + return toJSONObjectSafe(parse(x, false, null, keepStrings, 0)); } @@ -491,7 +533,7 @@ public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x, JSONMLParserConfiguration config) throws JSONException { - return (JSONObject)parse(x, false, null, config, 0); + return toJSONObjectSafe(parse(x, false, null, config, 0)); } diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index a1664f708..bcd218e5d 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -17,6 +17,9 @@ import java.util.*; import java.util.Map.Entry; import java.util.regex.Pattern; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.GenericArrayType; /** * A JSONObject is an unordered collection of name/value pairs. Its external @@ -79,17 +82,6 @@ public class JSONObject { */ private static final class Null { - /** - * There is only intended to be a single instance of the NULL object, - * so the clone method returns itself. - * - * @return NULL. - */ - @Override - protected final Object clone() { - return this; - } - /** * A Null object is equal to the null value and to itself. * @@ -152,6 +144,18 @@ public Class getMapType() { */ public static final Object NULL = new Null(); + /** + * Set of method names that should be excluded when identifying record-style accessors. + * These are common bean/Object method names that are not property accessors. + */ + private static final Set EXCLUDED_RECORD_METHOD_NAMES = Collections.unmodifiableSet( + new HashSet(Arrays.asList( + "get", "is", "set", + "toString", "hashCode", "equals", "clone", + "notify", "notifyAll", "wait" + )) + ); + /** * Construct an empty JSONObject. */ @@ -180,7 +184,7 @@ public JSONObject(JSONObject jo, String ... names) { for (int i = 0; i < names.length; i += 1) { try { this.putOnce(names[i], jo.opt(names[i])); - } catch (Exception ignore) { + } catch (Exception ignore) { // exception thrown for missing key } } } @@ -211,79 +215,134 @@ public JSONObject(JSONTokener x) throws JSONException { */ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(); - char c; - String key; - boolean isInitial = x.getPrevious() == 0; if (x.nextClean() != '{') { throw x.syntaxError("A JSONObject text must begin with '{'"); } for (;;) { - c = x.nextClean(); - switch (c) { + if (parseJSONObject(x, jsonParserConfiguration, isInitial)) { + return; + } + } + } + + /** + * Parses entirety of JSON object + * + * @param jsonTokener Parses text as tokens + * @param jsonParserConfiguration Variable to pass parser custom configuration for json parsing. + * @param isInitial True if start of document, else false + * @return True if done building object, else false + */ + private boolean parseJSONObject(JSONTokener jsonTokener, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) { + Object obj; + String key; + boolean doneParsing = false; + char c = jsonTokener.nextClean(); + + switch (c) { case 0: - throw x.syntaxError("A JSONObject text must end with '}'"); + throw jsonTokener.syntaxError("A JSONObject text must end with '}'"); case '}': - if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { - throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); + if (isInitial && jsonParserConfiguration.isStrictMode() && jsonTokener.nextClean() != 0) { + throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text"); } - return; + return true; default: - key = x.nextSimpleValue(c).toString(); - } - - // The key is followed by ':'. + obj = jsonTokener.nextSimpleValue(c); + key = obj.toString(); + } - c = x.nextClean(); - if (c != ':') { - throw x.syntaxError("Expected a ':' after a key"); - } + checkKeyForStrictMode(jsonTokener, jsonParserConfiguration, obj); - // Use syntaxError(..) to include error location + // The key is followed by ':'. + c = jsonTokener.nextClean(); + if (c != ':') { + throw jsonTokener.syntaxError("Expected a ':' after a key"); + } - if (key != null) { - // Check if key exists - boolean keyExists = this.opt(key) != null; - if (keyExists && !jsonParserConfiguration.isOverwriteDuplicateKey()) { - throw x.syntaxError("Duplicate key \"" + key + "\""); - } + // Use syntaxError(..) to include error location + if (key != null) { + // Check if key exists + boolean keyExists = this.opt(key) != null; + if (keyExists && !jsonParserConfiguration.isOverwriteDuplicateKey()) { + throw jsonTokener.syntaxError("Duplicate key \"" + key + "\""); + } - Object value = x.nextValue(); - // Only add value if non-null - if (value != null) { - this.put(key, value); - } + Object value = jsonTokener.nextValue(); + // Only add value if non-null + if (value != null) { + this.put(key, value); } + } - // Pairs are separated by ','. + // Pairs are separated by ','. + if (parseEndOfKeyValuePair(jsonTokener, jsonParserConfiguration, isInitial)) { + doneParsing = true; + } - switch (x.nextClean()) { + return doneParsing; + } + + /** + * Checks for valid end of key:value pair + * @param jsonTokener Parses text as tokens + * @param jsonParserConfiguration Variable to pass parser custom configuration for json parsing. + * @param isInitial True if end of JSON object, else false + * @return + */ + private static boolean parseEndOfKeyValuePair(JSONTokener jsonTokener, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) { + switch (jsonTokener.nextClean()) { case ';': // In strict mode semicolon is not a valid separator if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Strict mode error: Invalid character ';' found"); + throw jsonTokener.syntaxError("Strict mode error: Invalid character ';' found"); } + break; case ',': - if (x.nextClean() == '}') { + if (jsonTokener.nextClean() == '}') { // trailing commas are not allowed in strict mode if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Strict mode error: Expected another object element"); + throw jsonTokener.syntaxError("Strict mode error: Expected another object element"); } - return; + // End of JSON object + return true; } - if (x.end()) { - throw x.syntaxError("A JSONObject text must end with '}'"); + if (jsonTokener.end()) { + throw jsonTokener.syntaxError("A JSONObject text must end with '}'"); } - x.back(); + jsonTokener.back(); break; case '}': - if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { - throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); + if (isInitial && jsonParserConfiguration.isStrictMode() && jsonTokener.nextClean() != 0) { + throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text"); } - return; + // End of JSON object + return true; default: - throw x.syntaxError("Expected a ',' or '}'"); + throw jsonTokener.syntaxError("Expected a ',' or '}'"); + } + // Not at end of JSON object + return false; + } + + /** + * Throws error if key violates strictMode + * @param jsonTokener Parses text as tokens + * @param jsonParserConfiguration Variable to pass parser custom configuration for json parsing. + * @param obj Value to be checked + */ + private static void checkKeyForStrictMode(JSONTokener jsonTokener, JSONParserConfiguration jsonParserConfiguration, Object obj) { + if (jsonParserConfiguration != null && jsonParserConfiguration.isStrictMode()) { + if(obj instanceof Boolean) { + throw jsonTokener.syntaxError(String.format("Strict mode error: key '%s' cannot be boolean", obj.toString())); + } + if(obj == JSONObject.NULL) { + throw jsonTokener.syntaxError(String.format("Strict mode error: key '%s' cannot be null", obj.toString())); + } + if(obj instanceof Number) { + throw jsonTokener.syntaxError(String.format("Strict mode error: key '%s' cannot be number", obj.toString())); } } } @@ -401,12 +460,17 @@ private JSONObject(Map m, int recursionDepth, JSONParserConfiguration json */ public JSONObject(Object bean) { this(); - this.populateMap(bean); + this.populateMap(bean, new JSONParserConfiguration()); + } + + public JSONObject(Object bean, JSONParserConfiguration jsonParserConfiguration) { + this(); + this.populateMap(bean, jsonParserConfiguration); } private JSONObject(Object bean, Set objectsRecord) { this(); - this.populateMap(bean, objectsRecord); + this.populateMap(bean, objectsRecord, new JSONParserConfiguration()); } /** @@ -431,6 +495,7 @@ public JSONObject(Object object, String ... names) { try { this.putOpt(name, c.getField(name).get(object)); } catch (Exception ignore) { + // if invalid, do not include key:value pair in JSONObject } } } @@ -602,9 +667,9 @@ public static String doubleToString(double d) { return "null"; } -// Shave off trailing zeros and decimal point, if possible. - + // Shave off trailing zeros and decimal point, if possible. String string = Double.toString(d); + // idx = 0 case is covered by behavior of Double.toString() if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { while (string.endsWith("0")) { @@ -674,13 +739,11 @@ public > E getEnum(Class clazz, String key) throws JSONExce */ public boolean getBoolean(String key) throws JSONException { Object object = this.get(key); - if (object.equals(Boolean.FALSE) - || (object instanceof String && ((String) object) - .equalsIgnoreCase("false"))) { + if (Boolean.FALSE.equals(object) + || (object instanceof String && "false".equalsIgnoreCase((String) object))) { return false; - } else if (object.equals(Boolean.TRUE) - || (object instanceof String && ((String) object) - .equalsIgnoreCase("true"))) { + } else if (Boolean.TRUE.equals(object) + || (object instanceof String && "true".equalsIgnoreCase((String) object))) { return true; } throw wrongValueFormatException(key, "Boolean", object, null); @@ -697,8 +760,29 @@ public boolean getBoolean(String key) throws JSONException { * be converted to BigInteger. */ public BigInteger getBigInteger(String key) throws JSONException { + return this.getBigInteger(key, new JSONParserConfiguration()); + } + + /** + * Get the BigInteger value associated with a key. + * + * @param key + * A key string. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible. Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value cannot + * be converted to BigInteger. + */ + public BigInteger getBigInteger(String key, JSONParserConfiguration jsonParserConfiguration) + throws JSONException { Object object = this.get(key); - BigInteger ret = objectToBigInteger(object, null); + BigInteger ret = objectToBigInteger(object, null, jsonParserConfiguration); if (ret != null) { return ret; } @@ -1083,8 +1167,8 @@ public static String numberToString(Number number) throws JSONException { testValidity(number); // Shave off trailing zeros and decimal point, if possible. - String string = number.toString(); + // idx = 0 case is covered by behavior of .toString() if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { while (string.endsWith("0")) { @@ -1318,8 +1402,31 @@ static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue, boolea * @return An object which is the value. */ public BigInteger optBigInteger(String key, BigInteger defaultValue) { + return this.optBigInteger(key, defaultValue, new JSONParserConfiguration()); + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible and {@code defaultValue} is returned. + * Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue, + JSONParserConfiguration jsonParserConfiguration) { Object val = this.opt(key); - return objectToBigInteger(val, defaultValue); + return objectToBigInteger(val, defaultValue, jsonParserConfiguration); } /** @@ -1329,35 +1436,83 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { * to convert. */ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + return objectToBigInteger(val, defaultValue, new JSONParserConfiguration()); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @param jsonParserConfiguration parser configuration whose {@code maxNumberLength} + * bounds the number of decimal digits in the resulting integer. Values whose + * integer part would exceed this length are treated as unconvertible and + * {@code defaultValue} is returned. Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable this check. + * @return BigInteger conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue, + JSONParserConfiguration jsonParserConfiguration) { if (NULL.equals(val)) { return defaultValue; } + if (jsonParserConfiguration == null) { + jsonParserConfiguration = new JSONParserConfiguration(); + } + final int maxNumberLength = jsonParserConfiguration.getMaxNumberLength(); if (val instanceof BigInteger){ return (BigInteger) val; } if (val instanceof BigDecimal){ - return ((BigDecimal) val).toBigInteger(); + BigDecimal bd = (BigDecimal) val; + // Same ceiling as the parse-time maxNumberLength guard: refuse to + // materialise an integer whose decimal representation would exceed + // maxNumberLength digits. Prevents DoS via short exponent literals + // like 1e100000000 (CVE-2026-59171, see issue #1063). + if (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH + && (long) bd.precision() - bd.scale() > maxNumberLength) { + return defaultValue; + } + return bd.toBigInteger(); } if (val instanceof Double || val instanceof Float){ if (!numberIsFinite((Number)val)) { return defaultValue; } - return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); + return BigDecimal.valueOf(((Number) val).doubleValue()).toBigInteger(); } if (val instanceof Long || val instanceof Integer || val instanceof Short || val instanceof Byte){ return BigInteger.valueOf(((Number) val).longValue()); } + return attemptConversionToBigInteger(val, defaultValue, maxNumberLength); + } + + /** + * Convenience method to attempt conversion of value to BigInteger. + * Added to reduce complexity of objectToBigInteger() + * @param val the value to be converted + * @param defaultValue the default value to use if conversion is not attempted or fails + * @param maxNumberLength the max length allowed for BigIntegers + * @return the converted value, or the defaultValue + */ + private static BigInteger attemptConversionToBigInteger(Object val, BigInteger defaultValue, int maxNumberLength) { // don't check if it's a string in case of unchecked Number subclasses try { - // the other opt functions handle implicit conversions, i.e. - // jo.put("double",1.1d); - // jo.optInt("double"); -- will return 1, not an error - // this conversion to BigDecimal then to BigInteger is to maintain - // that type cast support that may truncate the decimal. + /** + * the other opt functions handle implicit conversions, i.e. + * jo.put("double",1.1d); + * jo.optInt("double"); -- will return 1, not an error + * this conversion to BigDecimal then to BigInteger is to maintain + * that type cast support that may truncate the decimal. + */ final String valStr = val.toString(); if(isDecimalNotation(valStr)) { - return new BigDecimal(valStr).toBigInteger(); + BigDecimal bd = new BigDecimal(valStr); + if (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH + && (long) bd.precision() - bd.scale() > maxNumberLength) { + return defaultValue; + } + return bd.toBigInteger(); } return new BigInteger(valStr); } catch (Exception e) { @@ -1458,11 +1613,7 @@ public float optFloat(String key, float defaultValue) { if (val == null) { return defaultValue; } - final float floatValue = val.floatValue(); - // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { - // return defaultValue; - // } - return floatValue; + return val.floatValue(); } /** @@ -1494,11 +1645,7 @@ public Float optFloatObject(String key, Float defaultValue) { if (val == null) { return defaultValue; } - final Float floatValue = val.floatValue(); - // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { - // return defaultValue; - // } - return floatValue; + return val.floatValue(); } /** @@ -1764,69 +1911,113 @@ public String optString(String key, String defaultValue) { * @throws JSONException * If a getter returned a non-finite number. */ - private void populateMap(Object bean) { - populateMap(bean, Collections.newSetFromMap(new IdentityHashMap())); + private void populateMap(Object bean, JSONParserConfiguration jsonParserConfiguration) { + populateMap(bean, Collections.newSetFromMap(new IdentityHashMap()), jsonParserConfiguration); } - private void populateMap(Object bean, Set objectsRecord) { + /** + * Convert a bean into a json object + * @param bean object tobe converted + * @param objectsRecord set of all objects for this method + * @param jsonParserConfiguration json parser settings + */ + private void populateMap(Object bean, Set objectsRecord, JSONParserConfiguration jsonParserConfiguration) { Class klass = bean.getClass(); // If klass is a System class then set includeSuperClass to false. + + // Check if this is a Java record type + boolean isRecord = isRecordType(klass); - boolean includeSuperClass = klass.getClassLoader() != null; - - Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + Method[] methods = getMethods(klass); for (final Method method : methods) { - final int modifiers = method.getModifiers(); - if (Modifier.isPublic(modifiers) - && !Modifier.isStatic(modifiers) - && method.getParameterTypes().length == 0 - && !method.isBridge() - && method.getReturnType() != Void.TYPE - && isValidMethodName(method.getName())) { - final String key = getKeyNameFromMethod(method); + if (isValidMethod(method)) { + final String key = getKeyNameFromMethod(method, isRecord); if (key != null && !key.isEmpty()) { - try { - final Object result = method.invoke(bean); - if (result != null) { - // check cyclic dependency and throw error if needed - // the wrap and populateMap combination method is - // itself DFS recursive - if (objectsRecord.contains(result)) { - throw recursivelyDefinedObjectException(key); - } - - objectsRecord.add(result); - - testValidity(result); - this.map.put(key, wrap(result, objectsRecord)); - - objectsRecord.remove(result); - - // we don't use the result anywhere outside of wrap - // if it's a resource we should be sure to close it - // after calling toString - if (result instanceof Closeable) { - try { - ((Closeable) result).close(); - } catch (IOException ignore) { - } - } - } - } catch (IllegalAccessException ignore) { - } catch (IllegalArgumentException ignore) { - } catch (InvocationTargetException ignore) { - } + processMethod(bean, objectsRecord, jsonParserConfiguration, method, key); } } } } + /** + * Processes method into json object entry if appropriate + * @param bean object being processed (owns the method) + * @param objectsRecord set of all objects for this method + * @param jsonParserConfiguration json parser settings + * @param method method being processed + * @param key name of the method + */ + private void processMethod(Object bean, Set objectsRecord, JSONParserConfiguration jsonParserConfiguration, + Method method, String key) { + try { + final Object result = method.invoke(bean); + if (result != null || jsonParserConfiguration.isUseNativeNulls()) { + // check cyclic dependency and throw error if needed + // the wrap and populateMap combination method is + // itself DFS recursive + if (objectsRecord.contains(result)) { + throw recursivelyDefinedObjectException(key); + } + + objectsRecord.add(result); + + testValidity(result); + this.map.put(key, wrap(result, objectsRecord)); + + objectsRecord.remove(result); + + closeClosable(result); + } + } catch (IllegalAccessException ignore) { + // ignore exception + } catch (IllegalArgumentException ignore) { + // ignore exception + } catch (InvocationTargetException ignore) { + // ignore exception + } + } + + /** + * Checks if a class is a Java record type. + * This uses reflection to check for the isRecord() method which was introduced in Java 16. + * This approach works even when running on Java 6+ JVM. + * + * @param klass the class to check + * @return true if the class is a record type, false otherwise + */ + private static boolean isRecordType(Class klass) { + try { + // Use reflection to check if Class has an isRecord() method (Java 16+) + // This allows the code to compile on Java 6 while still detecting records at runtime + Method isRecordMethod = Class.class.getMethod("isRecord"); + return (Boolean) isRecordMethod.invoke(klass); + } catch (NoSuchMethodException e) { + // isRecord() method doesn't exist - we're on Java < 16 + return false; + } catch (Exception e) { + // Any other reflection error - assume not a record + return false; + } + } + + /** + * This is a convenience method to simplify populate maps + * @param klass the name of the object being checked + * @return methods of klass + */ + private static Method[] getMethods(Class klass) { + boolean includeSuperClass = klass.getClassLoader() != null; + + return includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + } + private static boolean isValidMethodName(String name) { - return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + return !"getClass".equals(name) + && !"getDeclaringClass".equals(name); } - private static String getKeyNameFromMethod(Method method) { + private static String getKeyNameFromMethod(Method method, boolean isRecordType) { final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); if (ignoreDepth > 0) { final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); @@ -1837,7 +2028,7 @@ private static String getKeyNameFromMethod(Method method) { } } JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); - if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + if (annotationValueNotEmpty(annotation)) { return annotation.value(); } String key; @@ -1847,12 +2038,17 @@ private static String getKeyNameFromMethod(Method method) { } else if (name.startsWith("is") && name.length() > 2) { key = name.substring(2); } else { + // Only check for record-style accessors if this is actually a record type + // This maintains backward compatibility - classes with lowercase methods won't be affected + if (isRecordType && isRecordStyleAccessor(name, method)) { + return name; + } return null; } // if the first letter in the key is not uppercase, then skip. // This is to maintain backwards compatibility before PR406 // (https://github.com/stleary/JSON-java/pull/406/) - if (key.length() == 0 || Character.isLowerCase(key.charAt(0))) { + if (key.isEmpty() || Character.isLowerCase(key.charAt(0))) { return null; } if (key.length() == 1) { @@ -1863,6 +2059,78 @@ private static String getKeyNameFromMethod(Method method) { return key; } + /** + * Checks if a method is a record-style accessor. + * Record accessors have lowercase names without get/is prefixes and are not inherited from standard Java classes. + * + * @param methodName the name of the method + * @param method the method to check + * @return true if this is a record-style accessor, false otherwise + */ + private static boolean isRecordStyleAccessor(String methodName, Method method) { + if (methodName.isEmpty() || !Character.isLowerCase(methodName.charAt(0))) { + return false; + } + + // Exclude common bean/Object method names + if (EXCLUDED_RECORD_METHOD_NAMES.contains(methodName)) { + return false; + } + + Class declaringClass = method.getDeclaringClass(); + if (declaringClass == null || declaringClass == Object.class) { + return false; + } + + if (Enum.class.isAssignableFrom(declaringClass) || Number.class.isAssignableFrom(declaringClass)) { + return false; + } + + String className = declaringClass.getName(); + return !className.startsWith("java.") && !className.startsWith("javax."); + } + + /** + * checks if the annotation is not null and the {@link JSONPropertyName#value()} is not null and is not empty. + * @param annotation the annotation to check + * @return true if the annotation and the value is not null and not empty, false otherwise. + */ + private static boolean annotationValueNotEmpty(JSONPropertyName annotation) { + return annotation != null && annotation.value() != null && !annotation.value().isEmpty(); + } + + /** + * Checks if the method is valid for the {@link #populateMap(Object, Set, JSONParserConfiguration)} use case + * @param method the Method to check + * @return true, if valid, false otherwise. + */ + private static boolean isValidMethod(Method method) { + final int modifiers = method.getModifiers(); + return Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName()); + } + + /** + * calls {@link Closeable#close()} on the input, if it is an instance of Closable. + * @param input the input to close, if possible. + */ + private static void closeClosable(Object input) { + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (input instanceof Closeable) { + try { + ((Closeable) input).close(); + } catch (IOException ignore) { + // close has failed; best effort has been made + } + } + } + /** * Searches the class hierarchy to see if the method or it's super * implementations and interfaces has the annotation. @@ -1878,7 +2146,7 @@ private static String getKeyNameFromMethod(Method method) { * or one of its super class definitions */ private static A getAnnotation(final Method m, final Class annotationClass) { - // if we have invalid data the result is null + // If we have invalid data the result is null if (m == null || annotationClass == null) { return null; } @@ -1887,7 +2155,7 @@ private static A getAnnotation(final Method m, final Clas return m.getAnnotation(annotationClass); } - // if we've already reached the Object class, return null; + // If we've already reached the Object class, return null; Class c = m.getDeclaringClass(); if (c.getSuperclass() == null) { return null; @@ -1899,14 +2167,14 @@ private static A getAnnotation(final Method m, final Clas Method im = i.getMethod(m.getName(), m.getParameterTypes()); return getAnnotation(im, annotationClass); } catch (final SecurityException ex) { - continue; + // ignore this exception } catch (final NoSuchMethodException ex) { - continue; + // ignore this excpetion } } - //If the superclass is Object, no annotations will be found any more - if (c.getSuperclass().equals(Object.class)) + // If the superclass is Object, no annotations will be found any more + if (Object.class.equals(c.getSuperclass())) return null; try { @@ -1941,7 +2209,7 @@ private static int getAnnotationDepth(final Method m, final Class c = m.getDeclaringClass(); if (c.getSuperclass() == null) { return -1; @@ -1957,14 +2225,14 @@ private static int getAnnotationDepth(final Method m, final Class= '\u0080' && c < '\u00a0') - || (c >= '\u2000' && c < '\u2100')) { - w.write("\\u"); - hhhh = Integer.toHexString(c); - w.write("0000", 0, 4 - hhhh.length()); - w.write(hhhh); - } else { - w.write(c); - } + writeAsHex(w, c); } } w.write('"'); return w; } + /** + * Convenience method to reduce cognitive complexity of quote() + * @param w The Writer to which the quoted string will be appended. + * @param c Character to write + * @throws IOException + */ + private static void writeAsHex(Writer w, char c) throws IOException { + String hhhh; + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + /** * Remove a name and its value, if present. * @@ -2370,40 +2648,50 @@ public boolean similar(Object other) { if (!this.keySet().equals(((JSONObject)other).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)) { - return false; - } + return checkSimilarEntries(other); + } catch (Exception e) { + return false; + } + } + + private boolean checkSimilarEntries(Object other) { + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONObject)other).get(name); + if(valueThis == valueOther) { + continue; } - return true; - } catch (Throwable exception) { + if(valueThis == null) { + return false; + } + + if (!checkObjectType(valueThis, valueOther)) { + return false; + } + } + return true; + } + + /** + * Convenience function. Compares types of two objects. + * @param valueThis Object whose type is being checked + * @param valueOther Reference object + * @return true if match, else false + */ + private boolean checkObjectType(Object valueThis, Object valueOther) { + if (valueThis instanceof JSONObject) { + return ((JSONObject)valueThis).similar(valueOther); + } else if (valueThis instanceof JSONArray) { + return ((JSONArray)valueThis).similar(valueOther); + } else if (valueThis instanceof Number && valueOther instanceof Number) { + return isNumberSimilar((Number)valueThis, (Number)valueOther); + } else if (valueThis instanceof JSONString && valueOther instanceof JSONString) { + return ((JSONString) valueThis).toJSONString().equals(((JSONString) valueOther).toJSONString()); + } else if (!valueThis.equals(valueOther)) { return false; } + return true; } /** @@ -2469,16 +2757,33 @@ protected static boolean isDecimalNotation(final String val) { /** * Try to convert a string into a number, boolean, or null. If the string * can't be converted, return the string. + * Warning! stringToValue(String) uses the default max number length. If you want to override it, + * use a suitable initialized JSONParserConfiguration and the method: stringToValue(String, JSONParserConfiguration). * - * @param string - * A String. can not be null. + * @param str A String. can not be null. * @return A simple JSON value. * @throws NullPointerException * Thrown if the string is null. */ // Changes to this method must be copied to the corresponding method in // the XML class to keep full support for Android - public static Object stringToValue(String string) { + public static Object stringToValue(String str) { + return stringToValue(str, new JSONParserConfiguration()); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string A String. can not be null. + * @param jsonParserConfiguration the parser config + * @return A simple JSON value. If the string represents a number that is too large, + * a string will be returned. + * @throws NullPointerException Thrown if the string is null. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string, JSONParserConfiguration jsonParserConfiguration) { if ("".equals(string)) { return string; } @@ -2502,8 +2807,18 @@ public static Object stringToValue(String string) { char initial = string.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { try { - return stringToNumber(string); + if (jsonParserConfiguration == null) { + jsonParserConfiguration = new JSONParserConfiguration(); + } + // user declines max number checking + if (jsonParserConfiguration.getMaxNumberLength() == ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + return stringToNumber(string); + } + if (string.length() <= jsonParserConfiguration.getMaxNumberLength()) { + return stringToNumber(string); + } } catch (Exception ignore) { + // Do nothing } } return string; @@ -2524,41 +2839,10 @@ protected static Number stringToNumber(final String val) throws NumberFormatExce if ((initial >= '0' && initial <= '9') || initial == '-') { // decimal representation if (isDecimalNotation(val)) { - // Use a BigDecimal all the time so we keep the original - // representation. BigDecimal doesn't support -0.0, ensure we - // keep that by forcing a decimal. - try { - BigDecimal bd = new BigDecimal(val); - if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { - return Double.valueOf(-0.0); - } - return bd; - } catch (NumberFormatException retryAsDouble) { - // this is to support "Hex Floats" like this: 0x1.0P-1074 - try { - Double d = Double.valueOf(val); - if(d.isNaN() || d.isInfinite()) { - throw new NumberFormatException("val ["+val+"] is not a valid number."); - } - return d; - } catch (NumberFormatException ignore) { - throw new NumberFormatException("val ["+val+"] is not a valid number."); - } - } + return getNumber(val, initial); } // block items like 00 01 etc. Java number parsers treat these as Octal. - if(initial == '0' && val.length() > 1) { - char at1 = val.charAt(1); - if(at1 >= '0' && at1 <= '9') { - throw new NumberFormatException("val ["+val+"] is not a valid number."); - } - } else if (initial == '-' && val.length() > 2) { - char at1 = val.charAt(1); - char at2 = val.charAt(2); - if(at1 == '0' && at2 >= '0' && at2 <= '9') { - throw new NumberFormatException("val ["+val+"] is not a valid number."); - } - } + checkForInvalidNumberFormat(val, initial); // integer representation. // This will narrow any values to the smallest reasonable Object representation // (Integer, Long, or BigInteger) @@ -2579,6 +2863,57 @@ protected static Number stringToNumber(final String val) throws NumberFormatExce throw new NumberFormatException("val ["+val+"] is not a valid number."); } + /** + * Convenience function. Block items like 00 01 etc. Java number parsers treat these as Octal. + * @param val value to convert + * @param initial first char of val + * @throws exceptions if numbers are formatted incorrectly + */ + private static void checkForInvalidNumberFormat(String val, char initial) { + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+ val +"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+ val +"] is not a valid number."); + } + } + } + + /** + * Convenience function. Handles val if it is a number + * @param val value to convert + * @param initial first char of val + * @return val as a BigDecimal + */ + private static Number getNumber(String val, char initial) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+ val +"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+ val +"] is not a valid number."); + } + } + } + /** * Throw an exception if the object is a NaN or infinite number. * @@ -2750,13 +3085,13 @@ private static Object wrap(Object object, Set objectsRecord, int recursi return NULL; } if (object instanceof JSONObject || object instanceof JSONArray - || NULL.equals(object) || object instanceof JSONString + || object instanceof JSONString || object instanceof String || object instanceof Byte || object instanceof Character || object instanceof Short || object instanceof Integer || object instanceof Long || object instanceof Boolean || object instanceof Float || object instanceof Double - || object instanceof String || object instanceof BigInteger - || object instanceof BigDecimal || object instanceof Enum) { + || object instanceof BigInteger || object instanceof BigDecimal + || object instanceof Enum) { return object; } @@ -2811,28 +3146,15 @@ static final Writer writeValue(Writer writer, Object value, 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())); + // may throw an exception + processJsonStringToWriteValue(writer, value); } 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); - } + // may throw an exception + processNumberToWriteValue(writer, (Number) value); } else if (value instanceof Boolean) { writer.write(value.toString()); } else if (value instanceof Enum) { @@ -2855,6 +3177,41 @@ static final Writer writeValue(Writer writer, Object value, return writer; } + /** + * Convenience function to reduce cog complexity of calling method; writes value if string is valid + * @param writer Object doing the writing + * @param value Value to be written + * @throws IOException if something goes wrong + */ + private static void processJsonStringToWriteValue(Writer writer, Object value) throws IOException { + // 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())); + } + + /** + * Convenience function to reduce cog complexity of calling method; writes value if number is valid + * @param writer Object doing the writing + * @param value Value to be written + * @throws IOException if something goes wrong + */ + private static void processNumberToWriteValue(Writer writer, Number value) throws IOException { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString(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); + } + } + static final void indent(Writer writer, int indent) throws IOException { for (int i = 0; i < indent; i += 1) { writer.write(' '); @@ -2904,38 +3261,10 @@ public Writer write(Writer writer, int indentFactor, int indent) 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); - } + // might throw an exception + attemptWriteValue(writer, indentFactor, indent, entry, key); } 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); + writeContent(writer, indentFactor, indent, needsComma); } writer.write('}'); return writer; @@ -2944,6 +3273,68 @@ public Writer write(Writer writer, int indentFactor, int indent) } } + /** + * Convenience function. Writer attempts to write formatted content + * @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. + * @param needsComma + * Boolean flag indicating a comma is needed + * @throws IOException + * If something goes wrong + */ + private void writeContent(Writer writer, int indentFactor, int indent, boolean needsComma) throws IOException { + 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(' '); + } + attemptWriteValue(writer, indentFactor, newIndent, entry, key); + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + + /** + * Convenience function. Writer attempts to write a value. + * @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. + * @param entry + * Contains the value being written + * @param key + * Identifies the value + * @throws JSONException if a called function has an error or a write error + * occurs + + */ + private static void attemptWriteValue(Writer writer, int indentFactor, int indent, Entry entry, String key) { + try{ + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } + /** * Returns a java.util.Map containing all of the entries in this object. * If an entry in the object is a JSONArray or JSONObject it will also @@ -3012,22 +3403,257 @@ private static JSONException recursivelyDefinedObjectException(String key) { } /** - * For a prospective number, remove the leading zeros - * @param value prospective number - * @return number without leading zeros + * Helper method to extract the raw Class from Type. + */ + private Class getRawType(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } else if (type instanceof GenericArrayType) { + return Object[].class; // Simplified handling for arrays + } + return Object.class; // Fallback + } + + /** + * Extracts the element Type for a Collection Type. + */ + private Type getElementType(Type type) { + if (type instanceof ParameterizedType) { + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); + return args.length > 0 ? args[0] : Object.class; + } + return Object.class; + } + + /** + * Extracts the key and value Types for a Map Type. */ - private static String removeLeadingZerosOfNumber(String value){ - if (value.equals("-")){return value;} - boolean negativeFirstChar = (value.charAt(0) == '-'); - int counter = negativeFirstChar ? 1:0; - while (counter < value.length()){ - if (value.charAt(counter) != '0'){ - if (negativeFirstChar) {return "-".concat(value.substring(counter));} - return value.substring(counter); + private Type[] getMapTypes(Type type) { + if (type instanceof ParameterizedType) { + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); + if (args.length == 2) { + return args; } - ++counter; } - if (negativeFirstChar) {return "-0";} - return "0"; + return new Type[]{Object.class, Object.class}; // Default: String keys, Object values + } + + /** + * Deserializes a JSON string into an instance of the specified class. + * + *

This method attempts to map JSON key-value pairs to the corresponding fields + * of the given class. It supports basic data types including int, double, float, + * long, and boolean (as well as their boxed counterparts). The class must have a + * no-argument constructor, and the field names in the class must match the keys + * in the JSON string. + * + * @param the type of the object to return + * @param jsonString json in string format + * @param clazz the class of the object to be returned + * @return an instance of Object T with fields populated from the JSON string + */ + public static T fromJson(String jsonString, Class clazz) { + JSONObject jsonObject = new JSONObject(jsonString); + return jsonObject.fromJson(clazz); + } + + /** + * Deserializes a JSON string into an instance of the specified class. + * + *

This method attempts to map JSON key-value pairs to the corresponding fields + * of the given class. It supports basic data types including {@code int}, {@code double}, + * {@code float}, {@code long}, and {@code boolean}, as well as their boxed counterparts. + * The target class must have a no-argument constructor, and its field names must match + * the keys in the JSON string. Static fields are ignored. + * + *

Note: Only classes that are explicitly supported and registered within + * the {@code JSONObject} context can be deserialized. If the provided class is not among those, + * this method will not be able to deserialize it. This ensures that only a limited and + * controlled set of types can be instantiated from JSON for safety and predictability. + * + * @param clazz the class of the object to be returned + * @param the type of the object + * @return an instance of type {@code T} with fields populated from the JSON string + * @throws IllegalArgumentException if the class is not supported for deserialization + */ + @SuppressWarnings("unchecked") + public T fromJson(Class clazz) { + try { + T obj = clazz.getDeclaredConstructor().newInstance(); + for (Field field : clazz.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + field.setAccessible(true); + String fieldName = field.getName(); + if (has(fieldName)) { + Object value = get(fieldName); + Type fieldType = field.getGenericType(); + Object convertedValue = convertValue(value, fieldType); + field.set(obj, convertedValue); + } + } + return obj; + } catch (NoSuchMethodException e) { + throw new JSONException("No no-arg constructor for class: " + clazz.getName(), e); + } catch (Exception e) { + throw new JSONException("Failed to instantiate or set field for class: " + clazz.getName(), e); + } + } + + /** + * Recursively converts a value to the target Type, handling nested generics for Collections and Maps. + */ + private Object convertValue(Object value, Type targetType) throws JSONException { + if (value == null) { + return null; + } + + Class rawType = getRawType(targetType); + + // Direct assignment + if (rawType.isAssignableFrom(value.getClass())) { + return value; + } + + if (rawType == int.class || rawType == Integer.class) { + return ((Number) value).intValue(); + } else if (rawType == double.class || rawType == Double.class) { + return ((Number) value).doubleValue(); + } else if (rawType == float.class || rawType == Float.class) { + return ((Number) value).floatValue(); + } else if (rawType == long.class || rawType == Long.class) { + return ((Number) value).longValue(); + } else if (rawType == boolean.class || rawType == Boolean.class) { + return value; + } else if (rawType == String.class) { + return value; + } else if (rawType == BigDecimal.class) { + return new BigDecimal((String) value); + } else if (rawType == BigInteger.class) { + return new BigInteger((String) value); + } + + // Enum conversion + if (rawType.isEnum() && value instanceof String) { + return stringToEnum(rawType, (String) value); + } + + // Collection handling (e.g., List>>) + if (Collection.class.isAssignableFrom(rawType)) { + if (value instanceof JSONArray) { + Type elementType = getElementType(targetType); + return fromJsonArray((JSONArray) value, rawType, elementType); + } + } + // Map handling (e.g., Map>) + else if (Map.class.isAssignableFrom(rawType) && value instanceof JSONObject) { + Type[] mapTypes = getMapTypes(targetType); + Type keyType = mapTypes[0]; + Type valueType = mapTypes[1]; + return convertToMap((JSONObject) value, keyType, valueType, rawType); + } + // POJO handling (including custom classes like Tuple) + else if (!rawType.isPrimitive() && !rawType.isEnum() && value instanceof JSONObject) { + // Recurse with the raw class for POJO deserialization + return ((JSONObject) value).fromJson(rawType); + } + + // Fallback + return value.toString(); + } + + /** + * Converts a JSONObject to a Map with the specified generic key and value Types. + * Supports nested types via recursive convertValue. + */ + private Map convertToMap(JSONObject jsonMap, Type keyType, Type valueType, Class mapType) throws JSONException { + try { + @SuppressWarnings("unchecked") + Map createdMap = new HashMap(); + + for (Object keyObj : jsonMap.keySet()) { + String keyStr = (String) keyObj; + Object mapValue = jsonMap.get(keyStr); + // Convert key (e.g., String to Integer for Map) + Object convertedKey = convertValue(keyStr, keyType); + // Convert value recursively (handles nesting) + Object convertedValue = convertValue(mapValue, valueType); + createdMap.put(convertedKey, convertedValue); + } + return createdMap; + } catch (Exception e) { + throw new JSONException("Failed to convert JSONObject to Map: " + mapType.getName(), e); + } + } + + /** + * Converts a String to an Enum value. + * The unchecked warning is suppressed when casting valueOf() to E + * @param enumClass enum class + * @param value value of enum + * @param type of enum + */ + @SuppressWarnings("unchecked") + private E stringToEnum(Class enumClass, String value) throws JSONException { + try { + @SuppressWarnings("unchecked") + Class enumType = (Class) enumClass; + Method valueOfMethod = enumType.getMethod("valueOf", String.class); + return (E) valueOfMethod.invoke(null, value); + } catch (Exception e) { + throw new JSONException("Failed to convert string to enum: " + value + " for " + enumClass.getName(), e); + } + } + + /** + * Deserializes a JSONArray into a Collection, supporting nested generics. + * Uses recursive convertValue for elements. + */ + @SuppressWarnings("unchecked") + private Collection fromJsonArray(JSONArray jsonArray, Class collectionType, Type elementType) throws JSONException { + try { + Collection collection = getCollection(collectionType); + + for (int i = 0; i < jsonArray.length(); i++) { + Object jsonElement = jsonArray.get(i); + // Recursively convert each element using the full element Type (handles nesting) + Object convertedValue = convertValue(jsonElement, elementType); + collection.add((T) convertedValue); + } + return collection; + } catch (Exception e) { + throw new JSONException("Failed to convert JSONArray to Collection: " + collectionType.getName(), e); + } + } + + /** + * Creates and returns a new instance of a supported {@link Collection} implementation + * based on the specified collection type. + *

+ * This method currently supports the following collection types: + *

    + *
  • {@code List.class}
  • + *
  • {@code ArrayList.class}
  • + *
  • {@code Set.class}
  • + *
  • {@code HashSet.class}
  • + *
+ * If the provided type does not match any of the supported types, a {@link JSONException} + * is thrown. + * + * @param collectionType the {@link Class} object representing the desired collection type + * @return a new empty instance of the specified collection type + * @throws JSONException if the specified type is not a supported collection type + */ + private Collection getCollection(Class collectionType) throws JSONException { + if (collectionType == List.class || collectionType == ArrayList.class) { + return new ArrayList(); + } else if (collectionType == Set.class || collectionType == HashSet.class) { + return new HashSet(); + } else { + throw new JSONException("Unsupported Collection type: " + collectionType.getName()); + } } } diff --git a/src/main/java/org/json/JSONPointer.java b/src/main/java/org/json/JSONPointer.java index 859e1e644..34066c1aa 100644 --- a/src/main/java/org/json/JSONPointer.java +++ b/src/main/java/org/json/JSONPointer.java @@ -127,7 +127,7 @@ public JSONPointer(final String pointer) { if (pointer == null) { throw new NullPointerException("pointer cannot be null"); } - if (pointer.isEmpty() || pointer.equals("#")) { + if (pointer.isEmpty() || "#".equals(pointer)) { this.refTokens = Collections.emptyList(); return; } @@ -246,7 +246,7 @@ private static Object readByIndexToken(Object current, String indexToken) throws */ @Override public String toString() { - StringBuilder rval = new StringBuilder(""); + StringBuilder rval = new StringBuilder(); for (String token: this.refTokens) { rval.append('/').append(escape(token)); } diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index a90d51ae3..3726856d3 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -509,13 +509,26 @@ Object nextSimpleValue(char c) { string = sb.toString().trim(); if ("".equals(string)) { throw this.syntaxError("Missing value"); + } else if (jsonParserConfiguration != null && + jsonParserConfiguration.isStrictMode() && string.endsWith(".")) { + throw this.syntaxError(String.format("Strict mode error: Value '%s' ends with dot", string)); } - Object obj = JSONObject.stringToValue(string); - // Strict mode only allows strings with explicit double quotes + Object obj = JSONObject.stringToValue(string, jsonParserConfiguration); + // if obj is a boolean, look at string if (jsonParserConfiguration != null && - jsonParserConfiguration.isStrictMode() && - obj instanceof String) { - throw this.syntaxError(String.format("Strict mode error: Value '%s' is not surrounded by quotes", obj)); + jsonParserConfiguration.isStrictMode()) { + if (obj instanceof Boolean && !"true".equals(string) && !"false".equals(string)) { + // Strict mode only allows lowercase true or false + throw this.syntaxError(String.format("Strict mode error: Value '%s' is not lowercase boolean", obj)); + } + else if (obj == JSONObject.NULL && !"null".equals(string)) { + // Strint mode only allows lowercase null + throw this.syntaxError(String.format("Strict mode error: Value '%s' is not lowercase null", obj)); + } + else if (obj instanceof String) { + // Strict mode only allows strings with explicit double quotes + throw this.syntaxError(String.format("Strict mode error: Value '%s' is not surrounded by quotes", obj)); + } } return obj; } diff --git a/src/main/java/org/json/ParserConfiguration.java b/src/main/java/org/json/ParserConfiguration.java index 06cc44366..e80093cac 100644 --- a/src/main/java/org/json/ParserConfiguration.java +++ b/src/main/java/org/json/ParserConfiguration.java @@ -13,11 +13,21 @@ public class ParserConfiguration { */ public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1; + /** + * Used to indicate there's no defined limit to the maximum number length + */ + public static final int UNDEFINED_MAXIMUM_NUMBER_LENGTH = -1; + /** * The default maximum nesting depth when parsing a document. */ public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; + /** + * The default max number length + */ + public static final int DEFAULT_MAX_NUMBER_LENGTH = 1000; + /** * Specifies if values should be kept as strings (true), or if * they should try to be guessed into JSON values (numeric, boolean, string). @@ -29,23 +39,32 @@ public class ParserConfiguration { */ protected int maxNestingDepth; + /** + * The max number of chars for any number. Exceeding this limit will cause the value to be converted to a string + */ + protected int maxNumberLength; + /** * Constructs a new ParserConfiguration with default settings. */ public ParserConfiguration() { this.keepStrings = false; this.maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH; + this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH; } /** - * Constructs a new ParserConfiguration with the specified settings. + * Constructs a new ParserConfiguration with the specified settings. Use the with* methods instead of calling this ctor. * * @param keepStrings A boolean indicating whether to preserve strings during parsing. * @param maxNestingDepth An integer representing the maximum allowed nesting depth. + * @deprecated Use the with*() methods instead */ + @Deprecated protected ParserConfiguration(final boolean keepStrings, final int maxNestingDepth) { this.keepStrings = keepStrings; this.maxNestingDepth = maxNestingDepth; + this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH; } /** @@ -58,10 +77,11 @@ protected ParserConfiguration clone() { // 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 ParserConfiguration( - this.keepStrings, - this.maxNestingDepth - ); + ParserConfiguration parserConfiguration = new ParserConfiguration(); + parserConfiguration.keepStrings = this.keepStrings; + parserConfiguration.maxNestingDepth = this.maxNestingDepth; + parserConfiguration.maxNumberLength = this.maxNumberLength; + return parserConfiguration; } /** @@ -123,4 +143,37 @@ public T withMaxNestingDepth(int maxNestingDepth return newConfig; } + + + /** + * The maximum number length that the parser will allow + * + * @return the maximum number lengtj set for this configuration + */ + public int getMaxNumberLength() { + return maxNumberLength; + } + + /** + * Defines the maximum number length that the parser will allow + * Using any negative value as a parameter is equivalent to setting no limit to the length + * which means any size number is allowed + * + * @param maxNumberLength the maximum number length allowed + * @param the type of the configuration object + * @return The existing configuration will not be modified. A new configuration is returned. + */ + @SuppressWarnings("unchecked") + public T withMaxNumberLength(int maxNumberLength) { + T newConfig = (T) this.clone(); + + if (maxNumberLength > UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + newConfig.maxNumberLength = maxNumberLength; + } else { + newConfig.maxNumberLength = UNDEFINED_MAXIMUM_NUMBER_LENGTH; + } + + return newConfig; + } + } diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index e59ec7a4a..f5846cc1e 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.Iterator; +import java.util.NoSuchElementException; /** * This provides static methods to convert an XML text into a JSONObject, and to @@ -80,7 +81,7 @@ private static Iterable codePointIterator(final String string) { public Iterator iterator() { return new Iterator() { private int nextIndex = 0; - private int length = string.length(); + private final int length = string.length(); @Override public boolean hasNext() { @@ -89,6 +90,9 @@ public boolean hasNext() { @Override public Integer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } int result = string.codePointAt(this.nextIndex); this.nextIndex += Character.charCount(result); return result; @@ -154,7 +158,7 @@ public static String escape(String string) { * @param cp code point to test * @return true if the code point is not valid for an XML */ - private static boolean mustEscape(int cp) { + static boolean mustEscape(int cp) { /* Valid range from https://www.w3.org/TR/REC-xml/#charsets * * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] @@ -168,8 +172,14 @@ private static boolean mustEscape(int cp) { && cp != 0xA && cp != 0xD ) || !( - // valid the range of acceptable characters that aren't control - (cp >= 0x20 && cp <= 0xD7FF) + // Valid character range per W3C XML 1.0 spec: + // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + // Previously omitted #x9/#xA/#xD, causing unescape(" ") etc. + // to reject valid LF/TAB/CR as illegal (see #1059) + cp == 0x9 + || cp == 0xA + || cp == 0xD + || (cp >= 0x20 && cp <= 0xD7FF) || (cp >= 0xE000 && cp <= 0xFFFD) || (cp >= 0x10000 && cp <= 0x10FFFF) ) @@ -355,10 +365,20 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP && TYPE_ATTR.equals(string)) { xmlXsiTypeConverter = config.getXsiTypeMap().get(token); } else if (!nilAttributeFound) { - jsonObject.accumulate(string, - config.isKeepStrings() - ? ((String) token) - : stringToValue((String) token)); + Object obj = stringToValue((String) token); + if (obj instanceof Boolean) { + jsonObject.accumulate(string, + config.isKeepBooleanAsString() + ? ((String) token) + : obj); + } else if (obj instanceof Number) { + jsonObject.accumulate(string, + config.isKeepNumberAsString() + ? ((String) token) + : obj); + } else { + jsonObject.accumulate(string, stringToValue((String) token)); + } } token = null; } else { @@ -377,8 +397,13 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP context.append(tagName, JSONObject.NULL); } else if (jsonObject.length() > 0) { context.append(tagName, jsonObject); - } else { + } else if(context.isEmpty()) { //avoids resetting the array in case of an empty tag in the middle or end context.put(tagName, new JSONArray()); + if (jsonObject.isEmpty()){ + context.append(tagName, ""); + } + } else { + context.append(tagName, ""); } } else { if (nilAttributeFound) { @@ -407,8 +432,23 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP jsonObject.accumulate(config.getcDataTagName(), stringToValue(string, xmlXsiTypeConverter)); } else { - jsonObject.accumulate(config.getcDataTagName(), - config.isKeepStrings() ? string : stringToValue(string)); + Object obj = stringToValue((String) token); + if (obj instanceof Boolean) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepBooleanAsString() + ? ((String) token) + : obj); + } else if (obj instanceof Number) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepNumberAsString() + ? ((String) token) + : obj); + } else if (obj == JSONObject.NULL) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepStrings() ? ((String) token) : obj); + } else { + jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token)); + } } } @@ -422,7 +462,11 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP if (config.getForceList().contains(tagName)) { // Force the value to be an array if (jsonObject.length() == 0) { - context.put(tagName, new JSONArray()); + //avoids resetting the array in case of an empty element in the middle or end + if(context.isEmpty()) { + context.put(tagName, new JSONArray()); + } + context.append(tagName, ""); } else if (jsonObject.length() == 1 && jsonObject.opt(config.getcDataTagName()) != null) { context.append(tagName, jsonObject.opt(config.getcDataTagName())); @@ -579,16 +623,32 @@ public static Object stringToValue(String string, XMLXsiTypeConverter typeCon return stringToValue(string); } + /** + * This method is the same as {@link JSONObject#stringToValue(String)}. + * Warning! stringToValue(String) uses the default max number length. If you want to override it, + * use a suitable initialized XMLParserConfiguration and the method: stringToValue(String, XMLParserConfiguration). + * @param str String to convert + * @return JSON value of this string or the string + */ + // To maintain compatibility with the Android API, this method is a direct copy of + // the one in JSONObject. Changes made here should be reflected there. + // This method should not make calls out of the XML object. + public static Object stringToValue(String str) { + return stringToValue(str, new XMLParserConfiguration()); + } + /** * This method is the same as {@link JSONObject#stringToValue(String)}. * * @param string String to convert - * @return JSON value of this string or the string + * @param xmlParserConfiguration the XML parser config object + * @return JSON value of this string or the string. If the string represents a number that is too large, + * a string will be returned. */ // To maintain compatibility with the Android API, this method is a direct copy of // the one in JSONObject. Changes made here should be reflected there. // This method should not make calls out of the XML object. - public static Object stringToValue(String string) { + public static Object stringToValue(String string, XMLParserConfiguration xmlParserConfiguration) { if ("".equals(string)) { return string; } @@ -612,7 +672,13 @@ public static Object stringToValue(String string) { char initial = string.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { try { - return stringToNumber(string); + // user declines max number checking + if (xmlParserConfiguration.getMaxNumberLength() == ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + return stringToNumber(string); + } + if(string.length() <= xmlParserConfiguration.getMaxNumberLength()) { + return stringToNumber(string); + } } catch (Exception ignore) { } } @@ -688,6 +754,44 @@ public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); } + /** + * Convert a well-formed (but not necessarily valid) XML into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code
+     * <[ [ ]]>}
+ * are ignored. + * + * All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to + * numbers but will instead be the exact value as seen in the XML document depending + * on how flag is set. + * All booleans are converted as strings, for true, false will not be coerced to + * booleans but will instead be the exact value as seen in the XML document depending + * on how flag is set. + * + * @param reader The XML source reader. + * @param keepNumberAsString If true, then numeric values will not be coerced into + * numeric values and will instead be left as strings + * @param keepBooleanAsString If true, then boolean values will not be coerced into + * * numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(Reader reader, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException { + XMLParserConfiguration xmlParserConfiguration = new XMLParserConfiguration(); + if(keepNumberAsString) { + xmlParserConfiguration = xmlParserConfiguration.withKeepNumberAsString(keepNumberAsString); + } + if(keepBooleanAsString) { + xmlParserConfiguration = xmlParserConfiguration.withKeepBooleanAsString(keepBooleanAsString); + } + return toJSONObject(reader, xmlParserConfiguration); + } + /** * Convert a well-formed (but not necessarily valid) XML into a * JSONObject. Some information may be lost in this transformation because @@ -746,6 +850,38 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws return toJSONObject(new StringReader(string), keepStrings); } + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code
+     * <[ [ ]]>}
+ * are ignored. + * + * All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to + * numbers but will instead be the exact value as seen in the XML document depending + * on how flag is set. + * All booleans are converted as strings, for true, false will not be coerced to + * booleans but will instead be the exact value as seen in the XML document depending + * on how flag is set. + * + * @param string + * The source string. + * @param keepNumberAsString If true, then numeric values will not be coerced into + * numeric values and will instead be left as strings + * @param keepBooleanAsString If true, then boolean values will not be coerced into + * numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(String string, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException { + return toJSONObject(new StringReader(string), keepNumberAsString, keepBooleanAsString); + } + /** * Convert a well-formed (but not necessarily valid) XML string into a * JSONObject. Some information may be lost in this transformation because diff --git a/src/main/java/org/json/XMLParserConfiguration.java b/src/main/java/org/json/XMLParserConfiguration.java index bc4a80074..de84b90cb 100644 --- a/src/main/java/org/json/XMLParserConfiguration.java +++ b/src/main/java/org/json/XMLParserConfiguration.java @@ -22,6 +22,16 @@ public class XMLParserConfiguration extends ParserConfiguration { */ // public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; // We could override + /** + * Allow user to control how numbers are parsed + */ + private boolean keepNumberAsString; + + /** + * Allow user to control how booleans are parsed + */ + private boolean keepBooleanAsString; + /** Original Configuration of the XML Parser. */ public static final XMLParserConfiguration ORIGINAL = new XMLParserConfiguration(); @@ -142,7 +152,9 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN */ @Deprecated public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) { - super(keepStrings, DEFAULT_MAXIMUM_NESTING_DEPTH); + super(false, DEFAULT_MAXIMUM_NESTING_DEPTH); + this.keepNumberAsString = keepStrings; + this.keepBooleanAsString = keepStrings; this.cDataTagName = cDataTagName; this.convertNilAttributeToNull = convertNilAttributeToNull; } @@ -163,8 +175,10 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN */ private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull, final Map> xsiTypeMap, final Set forceList, - final int maxNestingDepth, final boolean closeEmptyTag) { - super(keepStrings, maxNestingDepth); + final int maxNestingDepth, final boolean closeEmptyTag, final boolean keepNumberAsString, final boolean keepBooleanAsString) { + super(false, maxNestingDepth); + this.keepNumberAsString = keepNumberAsString; + this.keepBooleanAsString = keepBooleanAsString; this.cDataTagName = cDataTagName; this.convertNilAttributeToNull = convertNilAttributeToNull; this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap); @@ -189,7 +203,9 @@ protected XMLParserConfiguration clone() { this.xsiTypeMap, this.forceList, this.maxNestingDepth, - this.closeEmptyTag + this.closeEmptyTag, + this.keepNumberAsString, + this.keepBooleanAsString ); config.shouldTrimWhiteSpace = this.shouldTrimWhiteSpace; return config; @@ -207,7 +223,43 @@ protected XMLParserConfiguration clone() { @SuppressWarnings("unchecked") @Override public XMLParserConfiguration withKeepStrings(final boolean newVal) { - return super.withKeepStrings(newVal); + XMLParserConfiguration newConfig = this.clone(); + newConfig.keepStrings = newVal; + newConfig.keepNumberAsString = newVal; + newConfig.keepBooleanAsString = newVal; + return newConfig; + } + + /** + * When parsing the XML into JSON, specifies if numbers should be kept as strings (1), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @param newVal + * new value to use for the keepNumberAsString configuration option. + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLParserConfiguration withKeepNumberAsString(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.keepNumberAsString = newVal; + newConfig.keepStrings = newConfig.keepBooleanAsString && newConfig.keepNumberAsString; + return newConfig; + } + + /** + * When parsing the XML into JSON, specifies if booleans should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @param newVal + * new value to use for the withKeepBooleanAsString configuration option. + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLParserConfiguration withKeepBooleanAsString(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.keepBooleanAsString = newVal; + newConfig.keepStrings = newConfig.keepBooleanAsString && newConfig.keepNumberAsString; + return newConfig; } /** @@ -221,6 +273,26 @@ public String getcDataTagName() { return this.cDataTagName; } + /** + * When parsing the XML into JSONML, specifies if numbers should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string). + * + * @return The keepStrings configuration value. + */ + public boolean isKeepNumberAsString() { + return this.keepNumberAsString; + } + + /** + * When parsing the XML into JSONML, specifies if booleans should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string). + * + * @return The keepStrings configuration value. + */ + public boolean isKeepBooleanAsString() { + return this.keepBooleanAsString; + } + /** * The name of the key in a JSON Object that indicates a CDATA section. Historically this has * been the value "content" but can be changed. Use null to indicate no CDATA diff --git a/src/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java index bc18b31c9..dc90f84d4 100644 --- a/src/main/java/org/json/XMLTokener.java +++ b/src/main/java/org/json/XMLTokener.java @@ -151,33 +151,111 @@ public Object nextEntity(@SuppressWarnings("unused") char ampersand) throws JSON /** * Unescape an XML entity encoding; * @param e entity (only the actual entity value, not the preceding & or ending ; - * @return + * @return the unescaped entity string + * @throws JSONException if the entity is malformed */ - static String unescapeEntity(String e) { + static String unescapeEntity(String e) throws JSONException { // validate if (e == null || e.isEmpty()) { return ""; } // if our entity is an encoded unicode point, parse it. if (e.charAt(0) == '#') { - int cp; - if (e.charAt(1) == 'x' || e.charAt(1) == 'X') { - // hex encoded unicode - cp = Integer.parseInt(e.substring(2), 16); - } else { - // decimal encoded unicode - cp = Integer.parseInt(e.substring(1)); + if (e.length() < 2) { + throw new JSONException("Invalid numeric character reference: &#;"); + } + int cp = (e.charAt(1) == 'x' || e.charAt(1) == 'X') + ? parseHexEntity(e) + : parseDecimalEntity(e); + if (XML.mustEscape(cp)) { + throw new JSONException("Invalid numeric character reference: &#" + e.substring(1) + ";"); } - return new String(new int[] {cp},0,1); - } + return new String(new int[] {cp}, 0, 1); + } Character knownEntity = entity.get(e); - if(knownEntity==null) { + if (knownEntity == null) { // we don't know the entity so keep it encoded return '&' + e + ';'; } return knownEntity.toString(); } + /** + * Parse a hexadecimal numeric character reference (e.g., "઼"). + * @param e entity string starting with '#' (e.g., "#x1F4A9") + * @return the Unicode code point + * @throws JSONException if the format is invalid + */ + private static int parseHexEntity(String e) throws JSONException { + // hex encoded unicode - need at least one hex digit after #x + if (e.length() < 3) { + throw new JSONException("Invalid hex character reference: missing hex digits in &#" + e.substring(1) + ";"); + } + String hex = e.substring(2); + if (!isValidHex(hex)) { + throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";"); + } + try { + return Integer.parseInt(hex, 16); + } catch (NumberFormatException nfe) { + throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";", nfe); + } + } + + /** + * Parse a decimal numeric character reference (e.g., "{"). + * @param e entity string starting with '#' (e.g., "#123") + * @return the Unicode code point + * @throws JSONException if the format is invalid + */ + private static int parseDecimalEntity(String e) throws JSONException { + String decimal = e.substring(1); + if (!isValidDecimal(decimal)) { + throw new JSONException("Invalid decimal character reference: &#" + decimal + ";"); + } + try { + return Integer.parseInt(decimal); + } catch (NumberFormatException nfe) { + throw new JSONException("Invalid decimal character reference: &#" + decimal + ";", nfe); + } + } + + /** + * Check if a string contains only valid hexadecimal digits. + * @param s the string to check + * @return true if s is non-empty and contains only hex digits (0-9, a-f, A-F) + */ + private static boolean isValidHex(String s) { + if (s == null || s.isEmpty()) { + return false; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; + } + + /** + * Check if a string contains only valid decimal digits. + * @param s the string to check + * @return true if s is non-empty and contains only digits (0-9) + */ + private static boolean isValidDecimal(String s) { + if (s == null || s.isEmpty()) { + return false; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + /** *
{@code 
diff --git a/src/test/java/org/json/junit/CDLTest.java b/src/test/java/org/json/junit/CDLTest.java
index 0e3668bf7..e5eb9eda8 100644
--- a/src/test/java/org/json/junit/CDLTest.java
+++ b/src/test/java/org/json/junit/CDLTest.java
@@ -168,6 +168,33 @@ public void unbalancedEscapedQuote(){
            }
     }
 
+    /**
+     * Csv parsing skip last row if last field of this row is empty #943
+     */
+    @Test
+    public void csvParsingCatchesLastRow(){
+        String data = "Field 1,Field 2,Field 3\n" +
+                "value11,value12,\n" +
+                "value21,value22,";
+
+        JSONArray jsonArray = CDL.toJSONArray(data);
+
+        JSONArray expectedJsonArray = new JSONArray();
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("Field 1", "value11");
+        jsonObject.put("Field 2", "value12");
+        jsonObject.put("Field 3", "");
+        expectedJsonArray.put(jsonObject);
+
+        jsonObject = new JSONObject();
+        jsonObject.put("Field 1", "value21");
+        jsonObject.put("Field 2", "value22");
+        jsonObject.put("Field 3", "");
+        expectedJsonArray.put(jsonObject);
+
+        Util.compareActualVsExpectedJsonArrays(jsonArray, expectedJsonArray);
+    }
+
     /**
      * Assert that there is no error for a single escaped quote within a properly embedded quote.
      */
diff --git a/src/test/java/org/json/junit/HTTPTokenerTest.java b/src/test/java/org/json/junit/HTTPTokenerTest.java
new file mode 100644
index 000000000..28dd40353
--- /dev/null
+++ b/src/test/java/org/json/junit/HTTPTokenerTest.java
@@ -0,0 +1,107 @@
+package org.json.junit;
+
+import org.json.HTTPTokener;
+import org.json.JSONException;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+/**
+ * Tests for JSON-Java HTTPTokener.java
+ */
+public class HTTPTokenerTest {
+
+    /**
+     * Test parsing a simple unquoted token.
+     */
+    @Test
+    public void parseSimpleToken() {
+        HTTPTokener tokener = new HTTPTokener("Content-Type");
+        String token = tokener.nextToken();
+        assertEquals("Content-Type", token);
+    }
+
+    /**
+     * Test parsing multiple tokens separated by whitespace.
+     */
+    @Test
+    public void parseMultipleTokens() {
+        HTTPTokener tokener = new HTTPTokener("Content-Type application/json");
+        String token1 = tokener.nextToken();
+        String token2 = tokener.nextToken();
+        assertEquals("Content-Type", token1);
+        assertEquals("application/json", token2);
+    }
+
+    /**
+     * Test parsing a double-quoted token.
+     */
+    @Test
+    public void parseDoubleQuotedToken() {
+        HTTPTokener tokener = new HTTPTokener("\"application/json\"");
+        String token = tokener.nextToken();
+        assertEquals("application/json", token);
+    }
+
+    /**
+     * Test parsing a single-quoted token.
+     */
+    @Test
+    public void parseSingleQuotedToken() {
+        HTTPTokener tokener = new HTTPTokener("'application/json'");
+        String token = tokener.nextToken();
+        assertEquals("application/json", token);
+    }
+
+    /**
+     * Test parsing a quoted token that includes spaces and semicolons.
+     */
+    @Test
+    public void parseQuotedTokenWithSpaces() {
+        HTTPTokener tokener = new HTTPTokener("\"text/html; charset=UTF-8\"");
+        String token = tokener.nextToken();
+        assertEquals("text/html; charset=UTF-8", token);
+    }
+
+    /**
+     * Test that unterminated quoted strings throw a JSONException.
+     */
+    @Test
+    public void throwExceptionOnUnterminatedString() {
+        HTTPTokener tokener = new HTTPTokener("\"incomplete");
+        JSONException exception = assertThrows(JSONException.class, tokener::nextToken);
+        assertTrue(exception.getMessage().contains("Unterminated string"));
+    }
+
+    /**
+     * Test behavior with empty input string.
+     */
+    @Test
+    public void parseEmptyInput() {
+        HTTPTokener tokener = new HTTPTokener("");
+        String token = tokener.nextToken();
+        assertEquals("", token);
+    }
+
+    /**
+     * Test behavior with input consisting only of whitespace.
+     */
+    @Test
+    public void parseWhitespaceOnly() {
+        HTTPTokener tokener = new HTTPTokener("   \t \n ");
+        String token = tokener.nextToken();
+        assertEquals("", token);
+    }
+
+    /**
+     * Test parsing tokens separated by multiple whitespace characters.
+     */
+    @Test
+    public void parseTokensWithMultipleWhitespace() {
+        HTTPTokener tokener = new HTTPTokener("GET     /index.html");
+        String method = tokener.nextToken();
+        String path = tokener.nextToken();
+        assertEquals("GET", method);
+        assertEquals("/index.html", path);
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java
index 429620396..c54b61795 100644
--- a/src/test/java/org/json/junit/JSONArrayTest.java
+++ b/src/test/java/org/json/junit/JSONArrayTest.java
@@ -1502,19 +1502,23 @@ public void testRecursiveDepthArrayForDefaultLevels() {
     }
 
     @Test
-    public void testRecursiveDepthArrayFor1000Levels() {
+    /**
+     * This test was originally for 1000 levels, which passes in test builds, but fails on my laptop.
+     * The current value of 900 seems to work.
+     */
+    public void testRecursiveDepthArrayFor900Levels() {
         try {
-            ArrayList array = buildNestedArray(1000);
-            JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(1000);
+            ArrayList array = buildNestedArray(900);
+            JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(900);
             new JSONArray(array, parserConfiguration);
         } catch (StackOverflowError e) {
             String javaVersion = System.getProperty("java.version");
             if (javaVersion.startsWith("11.")) {
                 System.out.println(
-                        "testRecursiveDepthArrayFor1000Levels() allowing intermittent stackoverflow, Java Version: "
+                        "testRecursiveDepthArrayFor900Levels() allowing intermittent stackoverflow, Java Version: "
                                 + javaVersion);
             } else {
-                String errorStr = "testRecursiveDepthArrayFor1000Levels() unexpected stackoverflow, Java Version: "
+                String errorStr = "testRecursiveDepthArrayFor900Levels() unexpected stackoverflow, Java Version: "
                         + javaVersion;
                 System.out.println(errorStr);
                 throw new RuntimeException(errorStr);
@@ -1544,4 +1548,22 @@ public static ArrayList buildNestedArray(int maxDepth) {
         nestedArray.add(buildNestedArray(maxDepth - 1));
         return nestedArray;
     }
+
+    @Test
+    public void TestLenientCommas() {
+        String str = "[1,,3]";
+
+        // Test should fail if default strictMode is true, pass with an extra null entry if false
+        JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+        if (jsonParserConfiguration.isStrictMode()) {
+            try {
+                new JSONArray(str);
+                fail("Expected to throw exception due to invalid string");
+            } catch (JSONException e) { /* no action is needed here */ }
+        } else {
+            JSONArray jsonArray = new JSONArray(str);
+            assertEquals("JSONArray in non-strictMode should contain a null entry",
+                    "[1,null,3]", jsonArray.toString());
+        }
+    }
 }
diff --git a/src/test/java/org/json/junit/JSONMLTest.java b/src/test/java/org/json/junit/JSONMLTest.java
index 5a360dd59..93a6821d8 100644
--- a/src/test/java/org/json/junit/JSONMLTest.java
+++ b/src/test/java/org/json/junit/JSONMLTest.java
@@ -986,4 +986,70 @@ public void testToJSONObjectMaxNestingDepthWithValidFittingXML() {
         }
     }
 
+    /**
+     * Tests that malformed XML causing type mismatch throws JSONException.
+     * Previously threw ClassCastException when parse() returned String instead of JSONArray.
+     * Related to issue #1034
+     */
+    @Test(expected = JSONException.class)
+    public void testMalformedXMLThrowsJSONExceptionNotClassCast() {
+        // This malformed XML causes parse() to return wrong type
+        byte[] data = {0x3c, 0x0a, 0x2f, (byte)0xff, (byte)0xff, (byte)0xff,
+                      (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff,
+                      (byte)0xff, 0x3e, 0x42};
+        String xmlStr = new String(data);
+        JSONML.toJSONArray(xmlStr);
+    }
+
+    /**
+     * Tests that type mismatch in toJSONObject throws JSONException.
+     * Validates safe type casting in toJSONObject methods.
+     */
+    @Test
+    public void testToJSONObjectTypeMismatch() {
+        // Create XML that would cause parse() to return wrong type
+        String xmlStr = "<\n/\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff>B";
+        try {
+            JSONML.toJSONObject(xmlStr);
+            fail("Expected JSONException for type mismatch");
+        } catch (ClassCastException e) {
+            fail("Should throw JSONException, not ClassCastException");
+        } catch (JSONException e) {
+            // Expected - verify it's about type mismatch
+            assertTrue("Exception message should mention type error",
+                e.getMessage().contains("Expected") || e.getMessage().contains("got"));
+        }
+    }
+
+    /**
+     * Tests that valid XML still works correctly after the fix.
+     * Ensures the type checking doesn't break normal operation.
+     */
+    @Test
+    public void testValidXMLStillWorks() {
+        String xmlStr = "value";
+        try {
+            JSONArray jsonArray = JSONML.toJSONArray(xmlStr);
+            assertNotNull("JSONArray should not be null", jsonArray);
+            assertEquals("root", jsonArray.getString(0));
+        } catch (Exception e) {
+            fail("Valid XML should not throw exception: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Tests that valid XML to JSONObject still works correctly.
+     */
+    @Test
+    public void testValidXMLToJSONObjectStillWorks() {
+        String xmlStr = "content";
+        try {
+            JSONObject jsonObject = JSONML.toJSONObject(xmlStr);
+            assertNotNull("JSONObject should not be null", jsonObject);
+            assertEquals("root", jsonObject.getString("tagName"));
+        } catch (Exception e) {
+            fail("Valid XML should not throw exception: " + e.getMessage());
+        }
+    }
+
 }
diff --git a/src/test/java/org/json/junit/JSONObjectLocaleTest.java b/src/test/java/org/json/junit/JSONObjectLocaleTest.java
index 1cdaf743d..e1a9dd64e 100755
--- a/src/test/java/org/json/junit/JSONObjectLocaleTest.java
+++ b/src/test/java/org/json/junit/JSONObjectLocaleTest.java
@@ -36,25 +36,31 @@ public void jsonObjectByLocaleBean() {
 
         MyLocaleBean myLocaleBean = new MyLocaleBean();
 
-        /**
-         * This is just the control case which happens when the locale.ROOT
-         * lowercasing behavior is the same as the current locale.
-         */
-        Locale.setDefault(new Locale("en"));
-        JSONObject jsonen = new JSONObject(myLocaleBean);
-        assertEquals("expected size 2, found: " +jsonen.length(), 2, jsonen.length());
-        assertEquals("expected jsonen[i] == beanI", "beanI", jsonen.getString("i"));
-        assertEquals("expected jsonen[id] == beanId", "beanId", jsonen.getString("id"));
-
-        /**
-         * Without the JSON-Java change, these keys would be stored internally as
-         * starting with the letter, 'ı' (dotless i), since the lowercasing of
-         * the getI and getId keys would be specific to the Turkish locale.
-         */
-        Locale.setDefault(new Locale("tr"));
-        JSONObject jsontr = new JSONObject(myLocaleBean);
-        assertEquals("expected size 2, found: " +jsontr.length(), 2, jsontr.length());
-        assertEquals("expected jsontr[i] == beanI", "beanI", jsontr.getString("i"));
-        assertEquals("expected jsontr[id] == beanId", "beanId", jsontr.getString("id"));
+        // save and restore the current default locale, to avoid any side effects on other executions in the same JVM
+        Locale defaultLocale = Locale.getDefault();
+        try {
+            /**
+             * This is just the control case which happens when the locale.ROOT
+             * lowercasing behavior is the same as the current locale.
+             */
+            Locale.setDefault(new Locale("en"));
+            JSONObject jsonen = new JSONObject(myLocaleBean);
+            assertEquals("expected size 2, found: " +jsonen.length(), 2, jsonen.length());
+            assertEquals("expected jsonen[i] == beanI", "beanI", jsonen.getString("i"));
+            assertEquals("expected jsonen[id] == beanId", "beanId", jsonen.getString("id"));
+
+            /**
+             * Without the JSON-Java change, these keys would be stored internally as
+             * starting with the letter, 'ı' (dotless i), since the lowercasing of
+             * the getI and getId keys would be specific to the Turkish locale.
+             */
+            Locale.setDefault(new Locale("tr"));
+            JSONObject jsontr = new JSONObject(myLocaleBean);
+            assertEquals("expected size 2, found: " +jsontr.length(), 2, jsontr.length());
+            assertEquals("expected jsontr[i] == beanI", "beanI", jsontr.getString("i"));
+            assertEquals("expected jsontr[id] == beanId", "beanId", jsontr.getString("id"));
+        } finally {
+            Locale.setDefault(defaultLocale);
+        }
     }
 }
diff --git a/src/test/java/org/json/junit/JSONObjectRecordTest.java b/src/test/java/org/json/junit/JSONObjectRecordTest.java
new file mode 100644
index 000000000..f1a673d28
--- /dev/null
+++ b/src/test/java/org/json/junit/JSONObjectRecordTest.java
@@ -0,0 +1,179 @@
+package org.json.junit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.StringReader;
+
+import org.json.JSONObject;
+import org.json.junit.data.GenericBeanInt;
+import org.json.junit.data.MyEnum;
+import org.json.junit.data.MyNumber;
+import org.json.junit.data.PersonRecord;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests for JSONObject support of Java record types.
+ * 
+ * NOTE: These tests are currently ignored because PersonRecord is not an actual Java record.
+ * The implementation now correctly detects actual Java records using reflection (Class.isRecord()).
+ * These tests will need to be enabled and run with Java 17+ where PersonRecord can be converted
+ * to an actual record type.
+ * 
+ * This ensures backward compatibility - regular classes with lowercase method names will not
+ * be treated as records unless they are actual Java record types.
+ */
+public class JSONObjectRecordTest {
+
+    /**
+     * Tests that JSONObject can be created from a record-style class.
+     * Record-style classes use accessor methods like name() instead of getName().
+     * 
+     * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+     */
+    @Test
+    @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+    public void jsonObjectByRecord() {
+        PersonRecord person = new PersonRecord("John Doe", 30, true);
+        JSONObject jsonObject = new JSONObject(person);
+        
+        assertEquals("Expected 3 keys in the JSONObject", 3, jsonObject.length());
+        assertEquals("John Doe", jsonObject.get("name"));
+        assertEquals(30, jsonObject.get("age"));
+        assertEquals(true, jsonObject.get("active"));
+    }
+    
+    /**
+     * Test that Object methods (toString, hashCode, equals, etc.) are not included
+     * 
+     * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+     */
+    @Test
+    @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+    public void recordStyleClassShouldNotIncludeObjectMethods() {
+        PersonRecord person = new PersonRecord("Jane Doe", 25, false);
+        JSONObject jsonObject = new JSONObject(person);
+        
+        // Should NOT include Object methods
+        assertFalse("Should not include toString", jsonObject.has("toString"));
+        assertFalse("Should not include hashCode", jsonObject.has("hashCode"));
+        assertFalse("Should not include equals", jsonObject.has("equals"));
+        assertFalse("Should not include clone", jsonObject.has("clone"));
+        assertFalse("Should not include wait", jsonObject.has("wait"));
+        assertFalse("Should not include notify", jsonObject.has("notify"));
+        assertFalse("Should not include notifyAll", jsonObject.has("notifyAll"));
+        
+        // Should only have the 3 record fields
+        assertEquals("Should only have 3 fields", 3, jsonObject.length());
+    }
+    
+    /**
+     * Test that enum methods are not included when processing an enum
+     */
+    @Test
+    public void enumsShouldNotIncludeEnumMethods() {
+        MyEnum myEnum = MyEnum.VAL1;
+        JSONObject jsonObject = new JSONObject(myEnum);
+        
+        // Should NOT include enum-specific methods like name(), ordinal(), values(), valueOf()
+        assertFalse("Should not include name method", jsonObject.has("name"));
+        assertFalse("Should not include ordinal method", jsonObject.has("ordinal"));
+        assertFalse("Should not include declaringClass", jsonObject.has("declaringClass"));
+        
+        // Enums should still work with traditional getters if they have any
+        // But should not pick up the built-in enum methods
+    }
+    
+    /**
+     * Test that Number subclass methods are not included
+     */
+    @Test
+    public void numberSubclassesShouldNotIncludeNumberMethods() {
+        MyNumber myNumber = new MyNumber();
+        JSONObject jsonObject = new JSONObject(myNumber);
+        
+        // Should NOT include Number methods like intValue(), longValue(), etc.
+        assertFalse("Should not include intValue", jsonObject.has("intValue"));
+        assertFalse("Should not include longValue", jsonObject.has("longValue"));
+        assertFalse("Should not include doubleValue", jsonObject.has("doubleValue"));
+        assertFalse("Should not include floatValue", jsonObject.has("floatValue"));
+        
+        // Should include the actual getter
+        assertTrue("Should include number", jsonObject.has("number"));
+        assertEquals("Should have 1 field", 1, jsonObject.length());
+    }
+    
+    /**
+     * Test that generic bean with get() and is() methods works correctly
+     */
+    @Test
+    public void genericBeanWithGetAndIsMethodsShouldNotBeIncluded() {
+        GenericBeanInt bean = new GenericBeanInt(42);
+        JSONObject jsonObject = new JSONObject(bean);
+        
+        // Should NOT include standalone get() or is() methods
+        assertFalse("Should not include standalone 'get' method", jsonObject.has("get"));
+        assertFalse("Should not include standalone 'is' method", jsonObject.has("is"));
+        
+        // Should include the actual getters
+        assertTrue("Should include genericValue field", jsonObject.has("genericValue"));
+        assertTrue("Should include a field", jsonObject.has("a"));
+    }
+    
+    /**
+     * Test that java.* classes don't have their methods picked up
+     */
+    @Test
+    public void javaLibraryClassesShouldNotIncludeTheirMethods() {
+        StringReader reader = new StringReader("test");
+        JSONObject jsonObject = new JSONObject(reader);
+        
+        // Should NOT include java.io.Reader methods like read(), reset(), etc.
+        assertFalse("Should not include read method", jsonObject.has("read"));
+        assertFalse("Should not include reset method", jsonObject.has("reset"));
+        assertFalse("Should not include ready method", jsonObject.has("ready"));
+        assertFalse("Should not include skip method", jsonObject.has("skip"));
+        
+        // Reader should produce empty JSONObject (no valid properties)
+        assertEquals("Reader should produce empty JSON", 0, jsonObject.length());
+    }
+    
+    /**
+     * Test mixed case - object with both traditional getters and record-style accessors
+     * 
+     * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+     */
+    @Test
+    @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+    public void mixedGettersAndRecordStyleAccessors() {
+        // PersonRecord has record-style accessors: name(), age(), active()
+        // These should all be included
+        PersonRecord person = new PersonRecord("Mixed Test", 40, true);
+        JSONObject jsonObject = new JSONObject(person);
+        
+        assertEquals("Should have all 3 record-style fields", 3, jsonObject.length());
+        assertTrue("Should include name", jsonObject.has("name"));
+        assertTrue("Should include age", jsonObject.has("age"));
+        assertTrue("Should include active", jsonObject.has("active"));
+    }
+    
+    /**
+     * Test that methods starting with uppercase are not included (not valid record accessors)
+     * 
+     * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+     */
+    @Test
+    @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+    public void methodsStartingWithUppercaseShouldNotBeIncluded() {
+        PersonRecord person = new PersonRecord("Test", 50, false);
+        JSONObject jsonObject = new JSONObject(person);
+        
+        // Record-style accessors must start with lowercase
+        // Methods like Name(), Age() (uppercase) should not be picked up
+        // Our PersonRecord only has lowercase accessors, which is correct
+        
+        assertEquals("Should only have lowercase accessors", 3, jsonObject.length());
+    }
+}
diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java
index 9d9ef8fa6..6b692789e 100644
--- a/src/test/java/org/json/junit/JSONObjectTest.java
+++ b/src/test/java/org/json/junit/JSONObjectTest.java
@@ -56,6 +56,17 @@
 import org.json.junit.data.Singleton;
 import org.json.junit.data.SingletonEnum;
 import org.json.junit.data.WeirdList;
+import org.json.junit.data.CustomClass;
+import org.json.junit.data.CustomClassA;
+import org.json.junit.data.CustomClassB;
+import org.json.junit.data.CustomClassC;
+import org.json.junit.data.CustomClassD;
+import org.json.junit.data.CustomClassE;
+import org.json.junit.data.CustomClassF;
+import org.json.junit.data.CustomClassG;
+import org.json.junit.data.CustomClassH;
+import org.json.junit.data.CustomClassI;
+import org.json.junit.data.CustomClassJ;
 import org.junit.After;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -64,3867 +75,4266 @@
 import com.jayway.jsonpath.JsonPath;
 
 /**
- * JSONObject, along with JSONArray, are the central classes of the reference app.
- * All of the other classes interact with them, and JSON functionality would
- * otherwise be impossible.
+ * JSONObject, along with JSONArray, are the central classes of the reference
+ * app. All of the other classes interact with them, and JSON functionality
+ * would otherwise be impossible.
  */
 public class JSONObjectTest {
 
-    /**
-     *  Regular Expression Pattern that matches JSON Numbers. This is primarily used for
-     *  output to guarantee that we are always writing valid JSON. 
-     */
-    static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?");
-
-    @After
-    public void tearDown() {
-        SingletonEnum.getInstance().setSomeInt(0);
-        SingletonEnum.getInstance().setSomeString(null);
-        Singleton.getInstance().setSomeInt(0);
-        Singleton.getInstance().setSomeString(null);
-    }
-
-    /**
-     * Tests that the similar method is working as expected.
-     */
-    @Test
-    public void verifySimilar() {
-        final String string1 = "HasSameRef";
-        final String string2 = "HasDifferentRef";
-        JSONObject obj1 = new JSONObject()
-                .put("key1", "abc")
-                .put("key2", 2)
-                .put("key3", string1);
-        
-        JSONObject obj2 = new JSONObject()
-                .put("key1", "abc")
-                .put("key2", 3)
-                .put("key3", string1);
-
-        JSONObject obj3 = new JSONObject()
-                .put("key1", "abc")
-                .put("key2", 2)
-                .put("key3", new String(string1));
-        
-        JSONObject obj4 = new JSONObject()
-                .put("key1", "abc")
-                .put("key2", 2.0)
-                .put("key3", new String(string1));
-
-        JSONObject obj5 = new JSONObject()
-                .put("key1", "abc")
-                .put("key2", 2.0)
-                .put("key3", new String(string2));
-        
-        assertFalse("obj1-obj2 Should eval to false", obj1.similar(obj2));
-        assertTrue("obj1-obj3 Should eval to true", obj1.similar(obj3));
-        assertTrue("obj1-obj4 Should eval to true", obj1.similar(obj4));
-        assertFalse("obj1-obj5 Should eval to false", obj1.similar(obj5));
-        // verify that a double and big decimal are "similar"
-        assertTrue("should eval to true",new JSONObject().put("a",1.1d).similar(new JSONObject("{\"a\":1.1}")));
-        // Confirm #618 is fixed (compare should not exit early if similar numbers are found)
-        // Note that this test may not work if the JSONObject map entry order changes
-        JSONObject first = new JSONObject("{\"a\": 1, \"b\": 2, \"c\": 3}");
-        JSONObject second = new JSONObject("{\"a\": 1, \"b\": 2.0, \"c\": 4}");
-        assertFalse("first-second should eval to false", first.similar(second));
-        List jsonObjects = new ArrayList(
-                Arrays.asList(obj1, obj2, obj3, obj4, obj5)
-        );
-        Util.checkJSONObjectsMaps(jsonObjects);
-    }
-
-    @Test
-    public void timeNumberParsing() {
-        // test data to use
-        final String[] testData = new String[] {
-                null,
-                "",
-                "100",
-                "-100",
-                "abc123",
-                "012345",
-                "100.5e199",
-                "-100.5e199",
-                "DEADBEEF",
-                "0xDEADBEEF",
-                "1234567890.1234567890",
-                "-1234567890.1234567890",
-                "adloghakuidghauiehgauioehgdkjfb nsruoh aeu noerty384 nkljfgh "
-                    + "395h tdfn kdz8yt3 4hkls gn.ey85 4hzfhnz.o8y5a84 onvklt "
-                    + "yh389thub nkz8y49lihv al4itlaithknty8hnbl"
-                // long (in length) number sequences with invalid data at the end of the
-                // string offer very poor performance for the REGEX.
-                ,"123467890123467890123467890123467890123467890123467890123467"
-                    + "8901234678901234678901234678901234678901234678901234678"
-                    + "9012346789012346789012346789012346789012346789012346789"
-                    + "0a"
-        };
-        final int testDataLength = testData.length;
-        /**
-         * Changed to 1000 for faster test runs
-         */
-        // final int iterations = 1000000;
-        final int iterations = 1000;
-
-        // 10 million iterations 1,000,000 * 10 (currently 100,000)
-        long startTime = System.nanoTime();
-        for(int i = 0; i < iterations; i++) {
-            for(int j = 0; j < testDataLength; j++) {
-                try {
-                BigDecimal v1 = new BigDecimal(testData[j]);
-                v1.signum();
-                } catch(Exception ignore) {
-                    //do nothing
-                }
-            }
-        }
-        final long elapsedNano1 = System.nanoTime() - startTime;
-        System.out.println("new BigDecimal(testData[]) : " + elapsedNano1 / 1000000 + " ms");
-
-        startTime = System.nanoTime();
-        for(int i = 0; i < iterations; i++) {
-            for(int j = 0; j < testDataLength; j++) {
-                try {
-                boolean v2 = NUMBER_PATTERN.matcher(testData[j]).matches();
-                assert v2 == !!v2;
-                } catch(Exception ignore) {
-                    //do nothing
-                }
-            }
-        }
-        final long elapsedNano2 = System.nanoTime() - startTime;
-        System.out.println("NUMBER_PATTERN.matcher(testData[]).matches() : " + elapsedNano2 / 1000000 + " ms");
-        // don't assert normally as the testing is machine dependent.
-        // assertTrue("Expected Pattern matching to be faster than BigDecimal constructor",elapsedNano2)(JsonPath.read(doc, "$"))).size() == 4);
-        assertTrue("expected \"falseKey\":false", Boolean.FALSE.equals(jsonObjectByName.query("/falseKey")));
-        assertTrue("expected \"nullKey\":null", JSONObject.NULL.equals(jsonObjectByName.query("/nullKey")));
-        assertTrue("expected \"stringKey\":\"hello world!\"", "hello world!".equals(jsonObjectByName.query("/stringKey")));
-        assertTrue("expected \"doubleKey\":-23.45e67", new BigDecimal("-23.45e67").equals(jsonObjectByName.query("/doubleKey")));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject, jsonObjectByName)));
-    }
-
-    /**
-     * JSONObjects can be built from a Map. 
-     * In this test the map is null.
-     * the JSONObject(JsonTokener) ctor is not tested directly since it already
-     * has full coverage from other tests.
-     */
-    @Test
-    public void jsonObjectByNullMap() {
-        Map map = null;
-        JSONObject jsonObject = new JSONObject(map);
-        assertTrue("jsonObject should be empty", jsonObject.isEmpty());
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * JSONObjects can be built from a Map. 
-     * In this test all of the map entries are valid JSON types.
-     */
-    @Test
-    public void jsonObjectByMap() {
-        Map map = new HashMap();
-        map.put("trueKey", Boolean.valueOf(true));
-        map.put("falseKey", Boolean.valueOf(false));
-        map.put("stringKey", "hello world!");
-        map.put("escapeStringKey", "h\be\tllo w\u1234orld!");
-        map.put("intKey", Long.valueOf(42));
-        map.put("doubleKey", Double.valueOf(-23.45e67));
-        JSONObject jsonObject = new JSONObject(map);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 6 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 6);
-        assertTrue("expected \"trueKey\":true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
-        assertTrue("expected \"falseKey\":false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
-        assertTrue("expected \"stringKey\":\"hello world!\"", "hello world!".equals(jsonObject.query("/stringKey")));
-        assertTrue("expected \"escapeStringKey\":\"h\be\tllo w\u1234orld!\"", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/escapeStringKey")));
-        assertTrue("expected \"doubleKey\":-23.45e67", Double.valueOf("-23.45e67").equals(jsonObject.query("/doubleKey")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Verifies that the constructor has backwards compatability with RAW types pre-java5.
-     */
-    @Test
-    public void verifyConstructor() {
-        
-        final JSONObject expected = new JSONObject("{\"myKey\":10}");
-        
-        @SuppressWarnings("rawtypes")
-        Map myRawC = Collections.singletonMap("myKey", Integer.valueOf(10));
-        JSONObject jaRaw = new JSONObject(myRawC);
-
-        Map myCStrObj = Collections.singletonMap("myKey",
-                (Object) Integer.valueOf(10));
-        JSONObject jaStrObj = new JSONObject(myCStrObj);
-
-        Map myCStrInt = Collections.singletonMap("myKey",
-                Integer.valueOf(10));
-        JSONObject jaStrInt = new JSONObject(myCStrInt);
-
-        Map myCObjObj = Collections.singletonMap((Object) "myKey",
-                (Object) Integer.valueOf(10));
-        JSONObject jaObjObj = new JSONObject(myCObjObj);
-
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaRaw));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaStrObj));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaStrInt));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaObjObj));
-        Util.checkJSONObjectsMaps(new ArrayList(
-                Arrays.asList(jaRaw, jaStrObj, jaStrInt, jaObjObj))
-        );
-    }
-    
-    /**
-     * Tests Number serialization.
-     */
-    @Test
-    public void verifyNumberOutput(){
-        /**
-         * MyNumberContainer is a POJO, so call JSONObject(bean), 
-         * which builds a map of getter names/values
-         * The only getter is getMyNumber (key=myNumber), 
-         * whose return value is MyNumber. MyNumber extends Number, 
-         * but is not recognized as such by wrap() per current
-         * implementation, so wrap() returns the default new JSONObject(bean).
-         * The only getter is getNumber (key=number), whose return value is
-         * BigDecimal(42). 
-         */
-        JSONObject jsonObject0 = new JSONObject(new MyNumberContainer());
-        String actual = jsonObject0.toString();
-        String expected = "{\"myNumber\":{\"number\":42}}";
-        assertEquals("Equal", expected , actual);
-        
-        /**
-         * JSONObject.put() handles objects differently than the 
-         * bean constructor. Where the bean ctor wraps objects before 
-         * placing them in the map, put() inserts the object without wrapping.
-         * In this case, a MyNumber instance is the value.
-         * The MyNumber.toString() method is responsible for
-         * returning a reasonable value: the string '42'.
-         */
-        JSONObject jsonObject1 = new JSONObject();
-        jsonObject1.put("myNumber", new MyNumber());
-        actual = jsonObject1.toString();
-        expected = "{\"myNumber\":42}";
-        assertEquals("Equal", expected , actual);
-
-        /**
-         * Calls the JSONObject(Map) ctor, which calls wrap() for values.
-         * AtomicInteger is a Number, but is not recognized by wrap(), per
-         * current implementation. However, the type is
-         * 'java.util.concurrent.atomic', so due to the 'java' prefix,
-         * wrap() inserts the value as a string. That is why 42 comes back
-         * wrapped in quotes.
-         */
-        JSONObject jsonObject2 = new JSONObject(Collections.singletonMap("myNumber", new AtomicInteger(42)));
-        actual = jsonObject2.toString();
-        expected = "{\"myNumber\":\"42\"}";
-        assertEquals("Equal", expected , actual);
-
-        /**
-         * JSONObject.put() inserts the AtomicInteger directly into the
-         * map not calling wrap(). In toString()->write()->writeValue(), 
-         * AtomicInteger is recognized as a Number, and converted via
-         * numberToString() into the unquoted string '42'.
-         */
-        JSONObject jsonObject3 = new JSONObject();
-        jsonObject3.put("myNumber", new AtomicInteger(42));
-        actual = jsonObject3.toString();
-        expected = "{\"myNumber\":42}";
-        assertEquals("Equal", expected , actual);
-
-        /**
-         * Calls the JSONObject(Map) ctor, which calls wrap() for values.
-         * Fraction is a Number, but is not recognized by wrap(), per
-         * current implementation. As a POJO, Fraction is handled as a
-         * bean and inserted into a contained JSONObject. It has 2 getters,
-         * for numerator and denominator. 
-         */
-        JSONObject jsonObject4 = new JSONObject(Collections.singletonMap("myNumber", new Fraction(4,2)));
-        assertEquals(1, jsonObject4.length());
-        assertEquals(2, ((JSONObject)(jsonObject4.get("myNumber"))).length());
-        assertEquals("Numerator", BigInteger.valueOf(4) , jsonObject4.query("/myNumber/numerator"));
-        assertEquals("Denominator", BigInteger.valueOf(2) , jsonObject4.query("/myNumber/denominator"));
-
-        /**
-         * JSONObject.put() inserts the Fraction directly into the
-         * map not calling wrap(). In toString()->write()->writeValue(), 
-         * Fraction is recognized as a Number, and converted via
-         * numberToString() into the unquoted string '4/2'. But the 
-         * BigDecimal sanity check fails, so writeValue() defaults
-         * to returning a safe JSON quoted string. Pretty slick!
-         */
-        JSONObject jsonObject5 = new JSONObject();
-        jsonObject5.put("myNumber", new Fraction(4,2));
-        actual = jsonObject5.toString();
-        expected = "{\"myNumber\":\"4/2\"}"; // valid JSON, bug fixed
-        assertEquals("Equal", expected , actual);
-
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject0, jsonObject1, jsonObject2, jsonObject3, jsonObject4, jsonObject5
-        )));
-    }
-
-    /**
-     * Verifies that the put Collection has backwards compatibility with RAW types pre-java5.
-     */
-    @Test
-    public void verifyPutCollection() {
-        
-        final JSONObject expected = new JSONObject("{\"myCollection\":[10]}");
-
-        @SuppressWarnings("rawtypes")
-        Collection myRawC = Collections.singleton(Integer.valueOf(10));
-        JSONObject jaRaw = new JSONObject();
-        jaRaw.put("myCollection", myRawC);
-
-        Collection myCObj = Collections.singleton((Object) Integer
-                .valueOf(10));
-        JSONObject jaObj = new JSONObject();
-        jaObj.put("myCollection", myCObj);
-
-        Collection myCInt = Collections.singleton(Integer
-                .valueOf(10));
-        JSONObject jaInt = new JSONObject();
-        jaInt.put("myCollection", myCInt);
-
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaRaw));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaObj));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaInt));
-
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jaRaw, jaObj, jaInt
-        )));
-    }
-
-    
-    /**
-     * Verifies that the put Map has backwards compatibility with RAW types pre-java5.
-     */
-    @Test
-    public void verifyPutMap() {
-        
-        final JSONObject expected = new JSONObject("{\"myMap\":{\"myKey\":10}}");
-
-        @SuppressWarnings("rawtypes")
-        Map myRawC = Collections.singletonMap("myKey", Integer.valueOf(10));
-        JSONObject jaRaw = new JSONObject();
-        jaRaw.put("myMap", myRawC);
-
-        Map myCStrObj = Collections.singletonMap("myKey",
-                (Object) Integer.valueOf(10));
-        JSONObject jaStrObj = new JSONObject();
-        jaStrObj.put("myMap", myCStrObj);
-
-        Map myCStrInt = Collections.singletonMap("myKey",
-                Integer.valueOf(10));
-        JSONObject jaStrInt = new JSONObject();
-        jaStrInt.put("myMap", myCStrInt);
-
-        Map myCObjObj = Collections.singletonMap((Object) "myKey",
-                (Object) Integer.valueOf(10));
-        JSONObject jaObjObj = new JSONObject();
-        jaObjObj.put("myMap", myCObjObj);
-
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaRaw));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaStrObj));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaStrInt));
-        assertTrue(
-                "The RAW Collection should give me the same as the Typed Collection",
-                expected.similar(jaObjObj));
-
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jaRaw, jaStrObj, jaStrInt, jaStrObj
-        )));
-    }
-
-
-    /**
-     * JSONObjects can be built from a Map. 
-     * In this test the map entries are not valid JSON types.
-     * The actual conversion is kind of interesting.
-     */
-    @Test
-    public void jsonObjectByMapWithUnsupportedValues() {
-        Map jsonMap = new HashMap();
-        // Just insert some random objects
-        jsonMap.put("key1", new CDL());
-        jsonMap.put("key2", new Exception());
-
-        JSONObject jsonObject = new JSONObject(jsonMap);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 2 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 2);
-        assertTrue("expected 0 key1 items", ((Map)(JsonPath.read(doc, "$.key1"))).size() == 0);
-        assertTrue("expected \"key2\":java.lang.Exception","java.lang.Exception".equals(jsonObject.query("/key2")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * JSONObjects can be built from a Map. 
-     * In this test one of the map values is null 
-     */
-    @Test
-    public void jsonObjectByMapWithNullValue() {
-        Map map = new HashMap();
-        map.put("trueKey", Boolean.valueOf(true));
-        map.put("falseKey", Boolean.valueOf(false));
-        map.put("stringKey", "hello world!");
-        map.put("nullKey", null);
-        map.put("escapeStringKey", "h\be\tllo w\u1234orld!");
-        map.put("intKey", Long.valueOf(42));
-        map.put("doubleKey", Double.valueOf(-23.45e67));
-        JSONObject jsonObject = new JSONObject(map);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 6 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 6);
-        assertTrue("expected \"trueKey\":true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
-        assertTrue("expected \"falseKey\":false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
-        assertTrue("expected \"stringKey\":\"hello world!\"", "hello world!".equals(jsonObject.query("/stringKey")));
-        assertTrue("expected \"escapeStringKey\":\"h\be\tllo w\u1234orld!\"", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/escapeStringKey")));
-        assertTrue("expected \"intKey\":42", Long.valueOf("42").equals(jsonObject.query("/intKey")));
-        assertTrue("expected \"doubleKey\":-23.45e67", Double.valueOf("-23.45e67").equals(jsonObject.query("/doubleKey")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-    
-    @Test
-    public void jsonObjectByMapWithNullValueAndParserConfiguration() {
-        Map map = new HashMap();
-        map.put("nullKey", null);
-        
-        // by default, null values are ignored
-        JSONObject obj1 = new JSONObject(map);
-        assertTrue("expected null value to be ignored by default", obj1.isEmpty());
-
-        // if configured, null values are written as such into the JSONObject.
-        JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
-        JSONObject obj2 = new JSONObject(map, parserConfiguration);
-        assertFalse("expected null value to accepted when configured", obj2.isEmpty());
-        assertTrue(obj2.has("nullKey"));
-        assertEquals(JSONObject.NULL, obj2.get("nullKey"));
-    }
-    
-    @Test
-    public void jsonObjectByMapWithNestedNullValueAndParserConfiguration() {
-        Map map = new HashMap();
-        Map nestedMap = new HashMap();
-        nestedMap.put("nullKey", null);
-        map.put("nestedMap", nestedMap);
-        List> nestedList = new ArrayList>();
-        nestedList.add(nestedMap);        
-        map.put("nestedList", nestedList);
-        
-        JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
-        JSONObject jsonObject = new JSONObject(map, parserConfiguration);
-
-        JSONObject nestedObject = jsonObject.getJSONObject("nestedMap");
-        assertTrue(nestedObject.has("nullKey"));
-        assertEquals(JSONObject.NULL, nestedObject.get("nullKey"));
-        
-        JSONArray nestedArray = jsonObject.getJSONArray("nestedList");
-        assertEquals(1, nestedArray.length());
-        assertTrue(nestedArray.getJSONObject(0).has("nullKey"));
-        assertEquals(JSONObject.NULL, nestedArray.getJSONObject(0).get("nullKey"));
-    }
-
-    /**
-     * JSONObject built from a bean. In this case all but one of the 
-     * bean getters return valid JSON types
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void jsonObjectByBean1() {
-        /**
-         * Default access classes have to be mocked since JSONObject, which is
-         * not in the same package, cannot call MyBean methods by reflection.
-         */
-        MyBean myBean = mock(MyBean.class);
-        when(myBean.getDoubleKey()).thenReturn(-23.45e7);
-        when(myBean.getIntKey()).thenReturn(42);
-        when(myBean.getStringKey()).thenReturn("hello world!");
-        when(myBean.getEscapeStringKey()).thenReturn("h\be\tllo w\u1234orld!");
-        when(myBean.isTrueKey()).thenReturn(true);
-        when(myBean.isFalseKey()).thenReturn(false);
-        when(myBean.getStringReaderKey()).thenReturn(
-            new StringReader("") {
-            });
-
-        JSONObject jsonObject = new JSONObject(myBean);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 8 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 8);
-        assertTrue("expected 0 items in stringReaderKey", ((Map) (JsonPath.read(doc, "$.stringReaderKey"))).size() == 0);
-        assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
-        assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
-        assertTrue("expected hello world!","hello world!".equals(jsonObject.query("/stringKey")));
-        assertTrue("expected h\be\tllo w\u1234orld!", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/escapeStringKey")));
-        assertTrue("expected 42", Integer.valueOf("42").equals(jsonObject.query("/intKey")));
-        assertTrue("expected -23.45e7", Double.valueOf("-23.45e7").equals(jsonObject.query("/doubleKey")));
-        // sorry, mockito artifact
-        assertTrue("expected 2 mockitoInterceptor items", ((Map)(JsonPath.read(doc, "$.mockitoInterceptor"))).size() == 2);
-        assertTrue("expected 0 mockitoInterceptor.serializationSupport items",
-                ((Map)(JsonPath.read(doc, "$.mockitoInterceptor.serializationSupport"))).size() == 0);
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * JSONObject built from a bean that has custom field names.
-     */
-    @Test
-    public void jsonObjectByBean2() {
-        JSONObject jsonObject = new JSONObject(new MyBeanCustomName());
-        assertNotNull(jsonObject);
-        assertEquals("Wrong number of keys found:",
-                5,
-                jsonObject.keySet().size());
-        assertFalse("Normal field name (someString) processing did not work",
-                jsonObject.has("someString"));
-        assertFalse("Normal field name (myDouble) processing did not work",
-                jsonObject.has("myDouble"));
-        assertFalse("Normal field name (someFloat) processing did not work",
-                jsonObject.has("someFloat"));
-        assertFalse("Ignored field not found!",
-                jsonObject.has("ignoredInt"));
-        // getSomeInt() has no user-defined annotation
-        assertTrue("Normal field name (someInt) should have been found",
-                jsonObject.has("someInt"));
-        // the user-defined annotation does not replace any value, so someLong should be found
-        assertTrue("Normal field name (someLong) should have been found",
-                jsonObject.has("someLong"));
-        // myStringField replaces someString property name via user-defined annotation
-        assertTrue("Overridden String field name (myStringField) should have been found",
-                jsonObject.has("myStringField"));
-        // weird name replaces myDouble property name via user-defined annotation
-        assertTrue("Overridden String field name (Some Weird NAme that Normally Wouldn't be possible!) should have been found",
-                jsonObject.has("Some Weird NAme that Normally Wouldn't be possible!"));
-        // InterfaceField replaces someFloat property name via user-defined annotation
-        assertTrue("Overridden String field name (InterfaceField) should have been found",
-                jsonObject.has("InterfaceField"));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-    
-    /**
-     * JSONObject built from a bean that has custom field names inherited from a parent class.
-     */
-    @Test
-    public void jsonObjectByBean3() {
-        JSONObject jsonObject = new JSONObject(new MyBeanCustomNameSubClass());
-        assertNotNull(jsonObject);
-        assertEquals("Wrong number of keys found:",
-                7,
-                jsonObject.keySet().size());
-        assertFalse("Normal int field name (someInt) found, but was overridden",
-                jsonObject.has("someInt"));
-        assertFalse("Normal field name (myDouble) processing did not work",
-                jsonObject.has("myDouble"));
-        // myDouble was replaced by weird name, and then replaced again by AMoreNormalName via user-defined annotation
-        assertFalse("Overridden String field name (Some Weird NAme that Normally Wouldn't be possible!) should not be FOUND!",
-                jsonObject.has("Some Weird NAme that Normally Wouldn't be possible!"));
-        assertFalse("Normal field name (someFloat) found, but was overridden",
-                jsonObject.has("someFloat"));
-        assertFalse("Ignored field found! but was overridden",
-                jsonObject.has("ignoredInt"));
-        // shouldNotBeJSON property name was first ignored, then replaced by ShouldBeIgnored via user-defined annotations
-        assertFalse("Ignored field at the same level as forced name should not have been found",
-                jsonObject.has("ShouldBeIgnored"));
-        // able property name was replaced by Getable via user-defined annotation
-        assertFalse("Normally ignored field (able) with explicit property name should not have been found",
-                jsonObject.has("able"));
-        // property name someInt was replaced by newIntFieldName via user-defined annotation
-        assertTrue("Overridden int field name (newIntFieldName) should have been found",
-                jsonObject.has("newIntFieldName"));
-        // property name someLong was not replaced via user-defined annotation
-        assertTrue("Normal field name (someLong) should have been found",
-                jsonObject.has("someLong"));
-        // property name someString was replaced by myStringField via user-defined annotation
-        assertTrue("Overridden String field name (myStringField) should have been found",
-                jsonObject.has("myStringField"));
-        // property name myDouble was replaced by a weird name, followed by AMoreNormalName via user-defined annotations
-        assertTrue("Overridden double field name (AMoreNormalName) should have been found",
-                jsonObject.has("AMoreNormalName"));
-        // property name someFloat was replaced by InterfaceField via user-defined annotation
-        assertTrue("Overridden String field name (InterfaceField) should have been found",
-                jsonObject.has("InterfaceField"));
-        // property name ignoredInt was replaced by none, followed by forcedInt via user-defined annotations
-        assertTrue("Forced field should have been found!",
-                jsonObject.has("forcedInt"));
-        // property name able was replaced by Getable via user-defined annotation
-        assertTrue("Overridden boolean field name (Getable) should have been found",
-                jsonObject.has("Getable"));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * A bean is also an object. But in order to test the JSONObject
-     * ctor that takes an object and a list of names, 
-     * this particular bean needs some public
-     * data members, which have been added to the class.
-     */
-    @Test
-    public void jsonObjectByObjectAndNames() {
-        String[] keys = {"publicString", "publicInt"};
-        // just need a class that has public data members
-        MyPublicClass myPublicClass = new MyPublicClass();
-        JSONObject jsonObject = new JSONObject(myPublicClass, keys);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 2 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 2);
-        assertTrue("expected \"publicString\":\"abc\"", "abc".equals(jsonObject.query("/publicString")));
-        assertTrue("expected \"publicInt\":42", Integer.valueOf(42).equals(jsonObject.query("/publicInt")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Exercise the JSONObject from resource bundle functionality.
-     * The test resource bundle is uncomplicated, but provides adequate test coverage.
-     */
-    @Test
-    public void jsonObjectByResourceBundle() {
-        JSONObject jsonObject = new
-                JSONObject("org.json.junit.data.StringsResourceBundle",
-                        Locale.getDefault());
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 2 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 2);
-        assertTrue("expected 2 greetings items", ((Map)(JsonPath.read(doc, "$.greetings"))).size() == 2);
-        assertTrue("expected \"hello\":\"Hello, \"", "Hello, ".equals(jsonObject.query("/greetings/hello")));
-        assertTrue("expected \"world\":\"World!\"", "World!".equals(jsonObject.query("/greetings/world")));
-        assertTrue("expected 2 farewells items", ((Map)(JsonPath.read(doc, "$.farewells"))).size() == 2);
-        assertTrue("expected \"later\":\"Later, \"", "Later, ".equals(jsonObject.query("/farewells/later")));
-        assertTrue("expected \"world\":\"World!\"", "Alligator!".equals(jsonObject.query("/farewells/gator")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-    
-    /**
-     * Exercise the JSONObject.accumulate() method
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void jsonObjectAccumulate() {
-
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.accumulate("myArray", true);
-        jsonObject.accumulate("myArray", false);
-        jsonObject.accumulate("myArray", "hello world!");
-        jsonObject.accumulate("myArray", "h\be\tllo w\u1234orld!");
-        jsonObject.accumulate("myArray", 42);
-        jsonObject.accumulate("myArray", -23.45e7);
-        // include an unsupported object for coverage
-        try {
-            jsonObject.accumulate("myArray", Double.NaN);
-            fail("Expected exception");
-        } catch (JSONException ignored) {}
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 1 top level item", ((Map)(JsonPath.read(doc, "$"))).size() == 1);
-        assertTrue("expected 6 myArray items", ((List)(JsonPath.read(doc, "$.myArray"))).size() == 6);
-        assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/myArray/0")));
-        assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/myArray/1")));
-        assertTrue("expected hello world!", "hello world!".equals(jsonObject.query("/myArray/2")));
-        assertTrue("expected h\be\tllo w\u1234orld!", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/myArray/3")));
-        assertTrue("expected 42", Integer.valueOf(42).equals(jsonObject.query("/myArray/4")));
-        assertTrue("expected -23.45e7", Double.valueOf(-23.45e7).equals(jsonObject.query("/myArray/5")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Exercise the JSONObject append() functionality
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void jsonObjectAppend() {
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.append("myArray", true);
-        jsonObject.append("myArray", false);
-        jsonObject.append("myArray", "hello world!");
-        jsonObject.append("myArray", "h\be\tllo w\u1234orld!");
-        jsonObject.append("myArray", 42);
-        jsonObject.append("myArray", -23.45e7);
-        // include an unsupported object for coverage
-        try {
-            jsonObject.append("myArray", Double.NaN);
-            fail("Expected exception");
-        } catch (JSONException ignored) {}
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 1 top level item", ((Map)(JsonPath.read(doc, "$"))).size() == 1);
-        assertTrue("expected 6 myArray items", ((List)(JsonPath.read(doc, "$.myArray"))).size() == 6);
-        assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/myArray/0")));
-        assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/myArray/1")));
-        assertTrue("expected hello world!", "hello world!".equals(jsonObject.query("/myArray/2")));
-        assertTrue("expected h\be\tllo w\u1234orld!", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/myArray/3")));
-        assertTrue("expected 42", Integer.valueOf(42).equals(jsonObject.query("/myArray/4")));
-        assertTrue("expected -23.45e7", Double.valueOf(-23.45e7).equals(jsonObject.query("/myArray/5")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Exercise the JSONObject doubleToString() method
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void jsonObjectDoubleToString() {
-        String [] expectedStrs = {"1", "1", "-23.4", "-2.345E68", "null", "null" };
-        Double [] doubles = { 1.0, 00001.00000, -23.4, -23.45e67, 
-                Double.NaN, Double.NEGATIVE_INFINITY };
-        for (int i = 0; i < expectedStrs.length; ++i) {
-            String actualStr = JSONObject.doubleToString(doubles[i]);
-            assertTrue("value expected ["+expectedStrs[i]+
-                    "] found ["+actualStr+ "]",
-                    expectedStrs[i].equals(actualStr));
-        }
-    }
-
-    /**
-     * Exercise some JSONObject get[type] and opt[type] methods
-     */
-    @Test
-    public void jsonObjectValues() {
-        String str = 
-            "{"+
-                "\"trueKey\":true,"+
-                "\"falseKey\":false,"+
-                "\"trueStrKey\":\"true\","+
-                "\"falseStrKey\":\"false\","+
-                "\"stringKey\":\"hello world!\","+
-                "\"intKey\":42,"+
-                "\"intStrKey\":\"43\","+
-                "\"longKey\":1234567890123456789,"+
-                "\"longStrKey\":\"987654321098765432\","+
-                "\"doubleKey\":-23.45e7,"+
-                "\"doubleStrKey\":\"00001.000\","+
-                "\"BigDecimalStrKey\":\"19007199254740993.35481234487103587486413587843213584\","+
-                "\"negZeroKey\":-0.0,"+
-                "\"negZeroStrKey\":\"-0.0\","+
-                "\"arrayKey\":[0,1,2],"+
-                "\"objectKey\":{\"myKey\":\"myVal\"}"+
-            "}";
-        JSONObject jsonObject = new JSONObject(str);
-        assertTrue("trueKey should be true", jsonObject.getBoolean("trueKey"));
-        assertTrue("opt trueKey should be true", jsonObject.optBoolean("trueKey"));
-        assertTrue("opt trueKey should be true", jsonObject.optBooleanObject("trueKey"));
-        assertTrue("falseKey should be false", !jsonObject.getBoolean("falseKey"));
-        assertTrue("trueStrKey should be true", jsonObject.getBoolean("trueStrKey"));
-        assertTrue("trueStrKey should be true", jsonObject.optBoolean("trueStrKey"));
-        assertTrue("trueStrKey should be true", jsonObject.optBooleanObject("trueStrKey"));
-        assertTrue("falseStrKey should be false", !jsonObject.getBoolean("falseStrKey"));
-        assertTrue("stringKey should be string",
-            jsonObject.getString("stringKey").equals("hello world!"));
-        assertTrue("doubleKey should be double", 
-                jsonObject.getDouble("doubleKey") == -23.45e7);
-        assertTrue("doubleStrKey should be double", 
-                jsonObject.getDouble("doubleStrKey") == 1);
-        assertTrue("doubleKey can be float", 
-                jsonObject.getFloat("doubleKey") == -23.45e7f);
-        assertTrue("doubleStrKey can be float", 
-                jsonObject.getFloat("doubleStrKey") == 1f);
-        assertTrue("opt doubleKey should be double", 
-                jsonObject.optDouble("doubleKey") == -23.45e7);
-        assertTrue("opt doubleKey with Default should be double", 
-                jsonObject.optDouble("doubleStrKey", Double.NaN) == 1);
-        assertTrue("opt doubleKey should be Double",
-                Double.valueOf(-23.45e7).equals(jsonObject.optDoubleObject("doubleKey")));
-        assertTrue("opt doubleKey with Default should be Double",
-                Double.valueOf(1).equals(jsonObject.optDoubleObject("doubleStrKey", Double.NaN)));
-        assertTrue("opt negZeroKey should be a Double", 
-                jsonObject.opt("negZeroKey") instanceof Double);
-        assertTrue("get negZeroKey should be a Double", 
-                jsonObject.get("negZeroKey") instanceof Double);
-        assertTrue("optNumber negZeroKey should return Double",
-                jsonObject.optNumber("negZeroKey") instanceof Double);
-        assertTrue("optNumber negZeroStrKey should return Double",
-                jsonObject.optNumber("negZeroStrKey") instanceof Double);
-        assertTrue("opt negZeroKey should be double", 
-                Double.compare(jsonObject.optDouble("negZeroKey"), -0.0d) == 0);
-        assertTrue("opt negZeroStrKey with Default should be double", 
-                Double.compare(jsonObject.optDouble("negZeroStrKey"), -0.0d) == 0);
-        assertTrue("opt negZeroKey should be Double",
-                Double.valueOf(-0.0d).equals(jsonObject.optDoubleObject("negZeroKey")));
-        assertTrue("opt negZeroStrKey with Default should be Double",
-                Double.valueOf(-0.0d).equals(jsonObject.optDoubleObject("negZeroStrKey")));
-        assertTrue("optNumber negZeroKey should be -0.0", 
-                Double.compare(jsonObject.optNumber("negZeroKey").doubleValue(), -0.0d) == 0);
-        assertTrue("optNumber negZeroStrKey should be -0.0", 
-                Double.compare(jsonObject.optNumber("negZeroStrKey").doubleValue(), -0.0d) == 0);
-        assertTrue("optFloat doubleKey should be float", 
-                jsonObject.optFloat("doubleKey") == -23.45e7f);
-        assertTrue("optFloat doubleKey with Default should be float", 
-                jsonObject.optFloat("doubleStrKey", Float.NaN) == 1f);
-        assertTrue("optFloat doubleKey should be Float",
-                Float.valueOf(-23.45e7f).equals(jsonObject.optFloatObject("doubleKey")));
-        assertTrue("optFloat doubleKey with Default should be Float",
-                Float.valueOf(1f).equals(jsonObject.optFloatObject("doubleStrKey", Float.NaN)));
-        assertTrue("intKey should be int", 
-                jsonObject.optInt("intKey") == 42);
-        assertTrue("opt intKey should be int", 
-                jsonObject.optInt("intKey", 0) == 42);
-        assertTrue("intKey should be Integer",
-                Integer.valueOf(42).equals(jsonObject.optIntegerObject("intKey")));
-        assertTrue("opt intKey should be Integer",
-                Integer.valueOf(42).equals(jsonObject.optIntegerObject("intKey", 0)));
-        assertTrue("opt intKey with default should be int", 
-                jsonObject.getInt("intKey") == 42);
-        assertTrue("intStrKey should be int", 
-                jsonObject.getInt("intStrKey") == 43);
-        assertTrue("longKey should be long", 
-                jsonObject.getLong("longKey") == 1234567890123456789L);
-        assertTrue("opt longKey should be long", 
-                jsonObject.optLong("longKey") == 1234567890123456789L);
-        assertTrue("opt longKey with default should be long", 
-                jsonObject.optLong("longKey", 0) == 1234567890123456789L);
-        assertTrue("opt longKey should be Long",
-                Long.valueOf(1234567890123456789L).equals(jsonObject.optLongObject("longKey")));
-        assertTrue("opt longKey with default should be Long",
-                Long.valueOf(1234567890123456789L).equals(jsonObject.optLongObject("longKey", 0L)));
-        assertTrue("longStrKey should be long", 
-                jsonObject.getLong("longStrKey") == 987654321098765432L);
-        assertTrue("optNumber int should return Integer",
-                jsonObject.optNumber("intKey") instanceof Integer);
-        assertTrue("optNumber long should return Long",
-                jsonObject.optNumber("longKey") instanceof Long);
-        assertTrue("optNumber double should return BigDecimal",
-                jsonObject.optNumber("doubleKey") instanceof BigDecimal);
-        assertTrue("optNumber Str int should return Integer",
-                jsonObject.optNumber("intStrKey") instanceof Integer);
-        assertTrue("optNumber Str long should return Long",
-                jsonObject.optNumber("longStrKey") instanceof Long);
-        assertTrue("optNumber Str double should return BigDecimal",
-                jsonObject.optNumber("doubleStrKey") instanceof BigDecimal);
-        assertTrue("optNumber BigDecimalStrKey should return BigDecimal",
-                jsonObject.optNumber("BigDecimalStrKey") instanceof BigDecimal);
-        assertTrue("xKey should not exist",
-                jsonObject.isNull("xKey"));
-        assertTrue("stringKey should exist",
-                jsonObject.has("stringKey"));
-        assertTrue("opt stringKey should string",
-                jsonObject.optString("stringKey").equals("hello world!"));
-        assertTrue("opt stringKey with default should string",
-                jsonObject.optString("stringKey", "not found").equals("hello world!"));
-        JSONArray jsonArray = jsonObject.getJSONArray("arrayKey");
-        assertTrue("arrayKey should be JSONArray", 
-                jsonArray.getInt(0) == 0 &&
-                jsonArray.getInt(1) == 1 &&
-                jsonArray.getInt(2) == 2);
-        jsonArray = jsonObject.optJSONArray("arrayKey");
-        assertTrue("opt arrayKey should be JSONArray", 
-                jsonArray.getInt(0) == 0 &&
-                jsonArray.getInt(1) == 1 &&
-                jsonArray.getInt(2) == 2);
-        JSONObject jsonObjectInner = jsonObject.getJSONObject("objectKey");
-        assertTrue("objectKey should be JSONObject", 
-                jsonObjectInner.get("myKey").equals("myVal"));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Check whether JSONObject handles large or high precision numbers correctly
-     */
-    @Test
-    public void stringToValueNumbersTest() {
-        assertTrue("-0 Should be a Double!",JSONObject.stringToValue("-0")  instanceof Double);
-        assertTrue("-0.0 Should be a Double!",JSONObject.stringToValue("-0.0") instanceof Double);
-        assertTrue("'-' Should be a String!",JSONObject.stringToValue("-") instanceof String);
-        assertTrue( "0.2 should be a BigDecimal!",
-                JSONObject.stringToValue( "0.2" ) instanceof BigDecimal );
-        assertTrue( "Doubles should be BigDecimal, even when incorrectly converting floats!",
-                JSONObject.stringToValue( Double.valueOf( "0.2f" ).toString() ) instanceof BigDecimal );
-        /**
-         * This test documents a need for BigDecimal conversion.
-         */
-        Object obj = JSONObject.stringToValue( "299792.457999999984" );
-        assertTrue( "does not evaluate to 299792.457999999984 BigDecimal!",
-                 obj.equals(new BigDecimal("299792.457999999984")) );
-        assertTrue( "1 should be an Integer!",
-                JSONObject.stringToValue( "1" ) instanceof Integer );
-        assertTrue( "Integer.MAX_VALUE should still be an Integer!",
-                JSONObject.stringToValue( Integer.valueOf( Integer.MAX_VALUE ).toString() ) instanceof Integer );
-        assertTrue( "Large integers should be a Long!",
-                JSONObject.stringToValue( Long.valueOf(((long)Integer.MAX_VALUE) + 1 ) .toString() ) instanceof Long );
-        assertTrue( "Long.MAX_VALUE should still be an Integer!",
-                JSONObject.stringToValue( Long.valueOf( Long.MAX_VALUE ).toString() ) instanceof Long );
-
-        String str = new BigInteger( Long.valueOf( Long.MAX_VALUE ).toString() ).add( BigInteger.ONE ).toString();
-        assertTrue( "Really large integers currently evaluate to BigInteger",
-                JSONObject.stringToValue(str).equals(new BigInteger("9223372036854775808")));
-    }
-
-    /**
-     * This test documents numeric values which could be numerically
-     * handled as BigDecimal or BigInteger. It helps determine what outputs
-     * will change if those types are supported.
-     */
-    @Test
-    public void jsonValidNumberValuesNeitherLongNorIEEE754Compatible() {
-        // Valid JSON Numbers, probably should return BigDecimal or BigInteger objects
-        String str = 
-            "{"+
-                "\"numberWithDecimals\":299792.457999999984,"+
-                "\"largeNumber\":12345678901234567890,"+
-                "\"preciseNumber\":0.2000000000000000111,"+
-                "\"largeExponent\":-23.45e2327"+
-            "}";
-        JSONObject jsonObject = new JSONObject(str);
-        // Comes back as a double, but loses precision
-        assertTrue( "numberWithDecimals currently evaluates to double 299792.458",
-                jsonObject.get( "numberWithDecimals" ).equals( new BigDecimal( "299792.457999999984" ) ) );
-        Object obj = jsonObject.get( "largeNumber" );
-        assertTrue("largeNumber currently evaluates to BigInteger",
-                new BigInteger("12345678901234567890").equals(obj));
-        // comes back as a double but loses precision
-        assertEquals( "preciseNumber currently evaluates to double 0.2",
-                0.2, jsonObject.getDouble( "preciseNumber" ), 0.0);
-        obj = jsonObject.get( "largeExponent" );
-        assertTrue("largeExponent should evaluate as a BigDecimal",
-                new BigDecimal("-23.45e2327").equals(obj));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * This test documents how JSON-Java handles invalid numeric input.
-     */
-    @Test
-    public void jsonInvalidNumberValues() {
-        // Number-notations supported by Java and invalid as JSON
-        String str =
-                "{" +
-                        "\"hexNumber\":-0x123," +
-                        "\"tooManyZeros\":00," +
-                        "\"negativeInfinite\":-Infinity," +
-                        "\"negativeNaN\":-NaN," +
-                        "\"negativeFraction\":-.01," +
-                        "\"tooManyZerosFraction\":00.001," +
-                        "\"negativeHexFloat\":-0x1.fffp1," +
-                        "\"hexFloat\":0x1.0P-1074," +
-                        "\"floatIdentifier\":0.1f," +
-                        "\"doubleIdentifier\":0.1d" +
-                        "}";
-
-        // Test should fail if default strictMode is true, pass if false
-        JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
-        if (jsonParserConfiguration.isStrictMode()) {
-            try {
-                JSONObject jsonObject = new JSONObject(str);
-                assertEquals("Expected to throw exception due to invalid string", true, false);
-            } catch (JSONException e) { }
-        } else {
-            JSONObject jsonObject = new JSONObject(str);
-            Object obj;
-            obj = jsonObject.get("hexNumber");
-            assertFalse("hexNumber must not be a number (should throw exception!?)",
-                    obj instanceof Number);
-            assertTrue("hexNumber currently evaluates to string",
-                    obj.equals("-0x123"));
-            assertTrue("tooManyZeros currently evaluates to string",
-                    jsonObject.get("tooManyZeros").equals("00"));
-            obj = jsonObject.get("negativeInfinite");
-            assertTrue("negativeInfinite currently evaluates to string",
-                    obj.equals("-Infinity"));
-            obj = jsonObject.get("negativeNaN");
-            assertTrue("negativeNaN currently evaluates to string",
-                    obj.equals("-NaN"));
-            assertTrue("negativeFraction currently evaluates to double -0.01",
-                    jsonObject.get("negativeFraction").equals(BigDecimal.valueOf(-0.01)));
-            assertTrue("tooManyZerosFraction currently evaluates to double 0.001",
-                    jsonObject.optLong("tooManyZerosFraction") == 0);
-            assertTrue("negativeHexFloat currently evaluates to double -3.99951171875",
-                    jsonObject.get("negativeHexFloat").equals(Double.valueOf(-3.99951171875)));
-            assertTrue("hexFloat currently evaluates to double 4.9E-324",
-                    jsonObject.get("hexFloat").equals(Double.valueOf(4.9E-324)));
-            assertTrue("floatIdentifier currently evaluates to double 0.1",
-                    jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1)));
-            assertTrue("doubleIdentifier currently evaluates to double 0.1",
-                    jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1)));
-            Util.checkJSONObjectMaps(jsonObject);
-        }
-    }
-
-    /**
-     * Tests how JSONObject get[type] handles incorrect types
-     */
-    @Test
-    public void jsonObjectNonAndWrongValues() {
-        String str = 
-            "{"+
-                "\"trueKey\":true,"+
-                "\"falseKey\":false,"+
-                "\"trueStrKey\":\"true\","+
-                "\"falseStrKey\":\"false\","+
-                "\"stringKey\":\"hello world!\","+
-                "\"intKey\":42,"+
-                "\"intStrKey\":\"43\","+
-                "\"longKey\":1234567890123456789,"+
-                "\"longStrKey\":\"987654321098765432\","+
-                "\"doubleKey\":-23.45e7,"+
-                "\"doubleStrKey\":\"00001.000\","+
-                "\"arrayKey\":[0,1,2],"+
-                "\"objectKey\":{\"myKey\":\"myVal\"}"+
-            "}";
-        JSONObject jsonObject = new JSONObject(str);
-        try {
-            jsonObject.getBoolean("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("expecting an exception message", 
-                    "JSONObject[\"nonKey\"] not found.", e.getMessage());
-        }
-        try {
-            jsonObject.getBoolean("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"stringKey\"] is not a Boolean (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getString("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getString("trueKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"trueKey\"] is not a string (class java.lang.Boolean : true).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getDouble("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) {
-            assertEquals("Expecting an exception message",
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getDouble("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message",
-                    "JSONObject[\"stringKey\"] is not a double (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getFloat("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) {
-            assertEquals("Expecting an exception message",
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getFloat("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message",
-                    "JSONObject[\"stringKey\"] is not a float (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getInt("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message",
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getInt("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"stringKey\"] is not a int (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getLong("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getLong("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"stringKey\"] is not a long (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getJSONArray("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getJSONArray("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"stringKey\"] is not a JSONArray (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getJSONObject("nonKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"nonKey\"] not found.",
-                    e.getMessage());
-        }
-        try {
-            jsonObject.getJSONObject("stringKey");
-            fail("Expected an exception");
-        } catch (JSONException e) { 
-            assertEquals("Expecting an exception message", 
-                    "JSONObject[\"stringKey\"] is not a JSONObject (class java.lang.String : hello world!).",
-                    e.getMessage());
-        }
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * This test documents an unexpected numeric behavior.
-     * A double that ends with .0 is parsed, serialized, then
-     * parsed again. On the second parse, it has become an int.
-     */
-    @Test
-    public void unexpectedDoubleToIntConversion() {
-        String key30 = "key30";
-        String key31 = "key31";
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.put(key30, Double.valueOf(3.0));
-        jsonObject.put(key31, Double.valueOf(3.1));
-
-        assertTrue("3.0 should remain a double",
-                jsonObject.getDouble(key30) == 3); 
-        assertTrue("3.1 should remain a double",
-                jsonObject.getDouble(key31) == 3.1); 
- 
-        // turns 3.0 into 3.
-        String serializedString = jsonObject.toString();
-        JSONObject deserialized = new JSONObject(serializedString);
-        assertTrue("3.0 is now an int", deserialized.get(key30) instanceof Integer);
-        assertTrue("3.0 can still be interpreted as a double",
-                deserialized.getDouble(key30) == 3.0);
-        assertTrue("3.1 remains a double", deserialized.getDouble(key31) == 3.1);
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Document behaviors of big numbers. Includes both JSONObject
-     * and JSONArray tests
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void bigNumberOperations() {
-        /**
-         * JSONObject tries to parse BigInteger as a bean, but it only has
-         * one getter, getLowestBitSet(). The value is lost and an unhelpful
-         * value is stored. This should be fixed.
-         */
-        BigInteger bigInteger = new BigInteger("123456789012345678901234567890");
-        JSONObject jsonObject0 = new JSONObject(bigInteger);
-        Object obj = jsonObject0.get("lowestSetBit");
-        assertTrue("JSONObject only has 1 value", jsonObject0.length() == 1);
-        assertTrue("JSONObject parses BigInteger as the Integer lowestBitSet",
-                obj instanceof Integer);
-        assertTrue("this bigInteger lowestBitSet happens to be 1",
-                obj.equals(1));
-
-        /**
-         * JSONObject tries to parse BigDecimal as a bean, but it has
-         * no getters, The value is lost and no value is stored.
-         * This should be fixed.
-         */
-        BigDecimal bigDecimal = new BigDecimal(
-                "123456789012345678901234567890.12345678901234567890123456789");
-        JSONObject jsonObject1 = new JSONObject(bigDecimal);
-        assertTrue("large bigDecimal is not stored", jsonObject1.isEmpty());
-
-        /**
-         * JSONObject put(String, Object) method stores and serializes
-         * bigInt and bigDec correctly. Nothing needs to change. 
-         */
-        JSONObject jsonObject2 = new JSONObject();
-        jsonObject2.put("bigInt", bigInteger);
-        assertTrue("jsonObject.put() handles bigInt correctly",
-                jsonObject2.get("bigInt").equals(bigInteger));
-        assertTrue("jsonObject.getBigInteger() handles bigInt correctly",
-                jsonObject2.getBigInteger("bigInt").equals(bigInteger));
-        assertTrue("jsonObject.optBigInteger() handles bigInt correctly",
-                jsonObject2.optBigInteger("bigInt", BigInteger.ONE).equals(bigInteger));
-        assertTrue("jsonObject serializes bigInt correctly",
-                jsonObject2.toString().equals("{\"bigInt\":123456789012345678901234567890}"));
-        assertTrue("BigInteger as BigDecimal",
-                jsonObject2.getBigDecimal("bigInt").equals(new BigDecimal(bigInteger)));
-
-        
-        JSONObject jsonObject3 = new JSONObject();
-        jsonObject3.put("bigDec", bigDecimal);
-        assertTrue("jsonObject.put() handles bigDec correctly",
-                jsonObject3.get("bigDec").equals(bigDecimal));
-        assertTrue("jsonObject.getBigDecimal() handles bigDec correctly",
-                jsonObject3.getBigDecimal("bigDec").equals(bigDecimal));
-        assertTrue("jsonObject.optBigDecimal() handles bigDec correctly",
-                jsonObject3.optBigDecimal("bigDec", BigDecimal.ONE).equals(bigDecimal));
-        assertTrue("jsonObject serializes bigDec correctly",
-                jsonObject3.toString().equals(
-                "{\"bigDec\":123456789012345678901234567890.12345678901234567890123456789}"));
-
-        assertTrue("BigDecimal as BigInteger",
-                jsonObject3.getBigInteger("bigDec").equals(bigDecimal.toBigInteger()));
-        /**
-         * exercise some exceptions
-         */
-        try {
-            // bigInt key does not exist
-            jsonObject3.getBigDecimal("bigInt");
-            fail("expected an exeption");
-        } catch (JSONException ignored) {}
-        obj = jsonObject3.optBigDecimal("bigInt", BigDecimal.ONE);
-        assertTrue("expected BigDecimal", obj.equals(BigDecimal.ONE));
-        jsonObject3.put("stringKey",  "abc");
-        try {
-            jsonObject3.getBigDecimal("stringKey");
-            fail("expected an exeption");
-        } catch (JSONException ignored) {}
-        obj = jsonObject3.optBigInteger("bigDec", BigInteger.ONE);
-        assertTrue("expected BigInteger", obj instanceof BigInteger);
-        assertEquals(bigDecimal.toBigInteger(), obj);
-
-        /**
-         * JSONObject.numberToString() works correctly, nothing to change.
-         */
-        String str = JSONObject.numberToString(bigInteger);
-        assertTrue("numberToString() handles bigInteger correctly",
-                str.equals("123456789012345678901234567890"));
-        str = JSONObject.numberToString(bigDecimal);
-        assertTrue("numberToString() handles bigDecimal correctly",
-                str.equals("123456789012345678901234567890.12345678901234567890123456789"));
-
-        /**
-         * JSONObject.stringToValue() turns bigInt into an accurate string,
-         * and rounds bigDec. This incorrect, but users may have come to 
-         * expect this behavior. Change would be marginally better, but 
-         * might inconvenience users.
-         */
-        obj = JSONObject.stringToValue(bigInteger.toString());
-        assertTrue("stringToValue() turns bigInteger string into Number",
-                obj instanceof Number);
-        obj = JSONObject.stringToValue(bigDecimal.toString());
-        assertTrue("stringToValue() changes bigDecimal Number",
-                obj instanceof Number);
-
-        /**
-         * wrap() vs put() big number behavior is now the same.
-         */
-        // bigInt map ctor 
-        Map map = new HashMap();
-        map.put("bigInt", bigInteger);
-        JSONObject jsonObject4 = new JSONObject(map);
-        String actualFromMapStr = jsonObject4.toString();
-        assertTrue("bigInt in map (or array or bean) is a string",
-                actualFromMapStr.equals(
-                "{\"bigInt\":123456789012345678901234567890}"));
-        // bigInt put
-        JSONObject jsonObject5 = new JSONObject();
-        jsonObject5.put("bigInt", bigInteger);
-        String actualFromPutStr = jsonObject5.toString();
-        assertTrue("bigInt from put is a number",
-                actualFromPutStr.equals(
-                "{\"bigInt\":123456789012345678901234567890}"));
-        // bigDec map ctor
-        map = new HashMap();
-        map.put("bigDec", bigDecimal);
-        JSONObject jsonObject6 = new JSONObject(map);
-        actualFromMapStr = jsonObject6.toString();
-        assertTrue("bigDec in map (or array or bean) is a bigDec",
-                actualFromMapStr.equals(
-                "{\"bigDec\":123456789012345678901234567890.12345678901234567890123456789}"));
-        // bigDec put
-        JSONObject jsonObject7 = new JSONObject();
-        jsonObject7.put("bigDec", bigDecimal);
-        actualFromPutStr = jsonObject7.toString();
-        assertTrue("bigDec from put is a number",
-                actualFromPutStr.equals(
-                "{\"bigDec\":123456789012345678901234567890.12345678901234567890123456789}"));
-        // bigInt,bigDec put 
-        JSONArray jsonArray0 = new JSONArray();
-        jsonArray0.put(bigInteger);
-        jsonArray0.put(bigDecimal);
-        actualFromPutStr = jsonArray0.toString();
-        assertTrue("bigInt, bigDec from put is a number",
-                actualFromPutStr.equals(
-                "[123456789012345678901234567890,123456789012345678901234567890.12345678901234567890123456789]"));
-        assertTrue("getBigInt is bigInt", jsonArray0.getBigInteger(0).equals(bigInteger));
-        assertTrue("getBigDec is bigDec", jsonArray0.getBigDecimal(1).equals(bigDecimal));
-        assertTrue("optBigInt is bigInt", jsonArray0.optBigInteger(0, BigInteger.ONE).equals(bigInteger));
-        assertTrue("optBigDec is bigDec", jsonArray0.optBigDecimal(1, BigDecimal.ONE).equals(bigDecimal));
-        jsonArray0.put(Boolean.TRUE);
-        try {
-            jsonArray0.getBigInteger(2);
-            fail("should not be able to get big int");
-        } catch (Exception ignored) {}
-        try {
-            jsonArray0.getBigDecimal(2);
-            fail("should not be able to get big dec");
-        } catch (Exception ignored) {}
-        assertTrue("optBigInt is default", jsonArray0.optBigInteger(2, BigInteger.ONE).equals(BigInteger.ONE));
-        assertTrue("optBigDec is default", jsonArray0.optBigDecimal(2, BigDecimal.ONE).equals(BigDecimal.ONE));
-
-        // bigInt,bigDec list ctor
-        List list = new ArrayList();
-        list.add(bigInteger);
-        list.add(bigDecimal);
-        JSONArray jsonArray1 = new JSONArray(list);
-        String actualFromListStr = jsonArray1.toString();
-        assertTrue("bigInt, bigDec in list is a bigInt, bigDec",
-                actualFromListStr.equals(
-                "[123456789012345678901234567890,123456789012345678901234567890.12345678901234567890123456789]"));
-        // bigInt bean ctor
-        MyBigNumberBean myBigNumberBean = mock(MyBigNumberBean.class);
-        when(myBigNumberBean.getBigInteger()).thenReturn(new BigInteger("123456789012345678901234567890"));
-        JSONObject jsonObject8 = new JSONObject(myBigNumberBean);
-        String actualFromBeanStr = jsonObject8.toString();
-        // can't do a full string compare because mockery adds an extra key/value
-        assertTrue("bigInt from bean ctor is a bigInt",
-                actualFromBeanStr.contains("123456789012345678901234567890"));
-        // bigDec bean ctor
-        myBigNumberBean = mock(MyBigNumberBean.class);
-        when(myBigNumberBean.getBigDecimal()).thenReturn(new BigDecimal("123456789012345678901234567890.12345678901234567890123456789"));
-        jsonObject8 = new JSONObject(myBigNumberBean);
-        actualFromBeanStr = jsonObject8.toString();
-        // can't do a full string compare because mockery adds an extra key/value
-        assertTrue("bigDec from bean ctor is a bigDec",
-                actualFromBeanStr.contains("123456789012345678901234567890.12345678901234567890123456789"));
-        // bigInt,bigDec wrap()
-        obj = JSONObject.wrap(bigInteger);
-        assertTrue("wrap() returns big num",obj.equals(bigInteger));
-        obj = JSONObject.wrap(bigDecimal);
-        assertTrue("wrap() returns string",obj.equals(bigDecimal));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject0, jsonObject1, jsonObject2, jsonObject3, jsonObject4,
-                jsonObject5, jsonObject6, jsonObject7, jsonObject8
-        )));
-        Util.checkJSONArrayMaps(jsonArray0, jsonObject0.getMapType());
-        Util.checkJSONArrayMaps(jsonArray1, jsonObject0.getMapType());
-    }
-
-    /**
-     * The purpose for the static method getNames() methods are not clear.
-     * This method is not called from within JSON-Java. Most likely
-     * uses are to prep names arrays for:  
-     * JSONObject(JSONObject jo, String[] names)
-     * JSONObject(Object object, String names[]),
-     */
-    @Test
-    public void jsonObjectNames() {
-
-        // getNames() from null JSONObject
-        assertTrue("null names from null Object", 
-                null == JSONObject.getNames((Object)null));
-
-        // getNames() from object with no fields
-        assertTrue("null names from Object with no fields", 
-                null == JSONObject.getNames(new MyJsonString()));
-
-        // getNames from new JSONOjbect
-        JSONObject jsonObject0 = new JSONObject();
-        String [] names = JSONObject.getNames(jsonObject0);
-        assertTrue("names should be null", names == null);
-
-        
-        // getNames() from empty JSONObject
-        String emptyStr = "{}";
-        JSONObject jsonObject1 = new JSONObject(emptyStr);
-        assertTrue("empty JSONObject should have null names",
-                null == JSONObject.getNames(jsonObject1));
-
-        // getNames() from JSONObject
-        String str = 
-            "{"+
-                "\"trueKey\":true,"+
-                "\"falseKey\":false,"+
-                "\"stringKey\":\"hello world!\""+
-            "}";
-        JSONObject jsonObject2 = new JSONObject(str);
-        names = JSONObject.getNames(jsonObject2);
-        JSONArray jsonArray0 = new JSONArray(names);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider()
-                .parse(jsonArray0.toString());
-        List docList = JsonPath.read(doc, "$");
-        assertTrue("expected 3 items", docList.size() == 3);
-        assertTrue(
-                "expected to find trueKey",
-                ((List) JsonPath.read(doc, "$[?(@=='trueKey')]")).size() == 1);
-        assertTrue(
-                "expected to find falseKey",
-                ((List) JsonPath.read(doc, "$[?(@=='falseKey')]")).size() == 1);
-        assertTrue(
-                "expected to find stringKey",
-                ((List) JsonPath.read(doc, "$[?(@=='stringKey')]")).size() == 1);
-
-        /**
-         * getNames() from an enum with properties has an interesting result.
-         * It returns the enum values, not the selected enum properties
-         */
-        MyEnumField myEnumField = MyEnumField.VAL1;
-        names = JSONObject.getNames(myEnumField);
-
-        // validate JSON
-        JSONArray jsonArray1 = new JSONArray(names);
-        doc = Configuration.defaultConfiguration().jsonProvider()
-                .parse(jsonArray1.toString());
-        docList = JsonPath.read(doc, "$");
-        assertTrue("expected 3 items", docList.size() == 3);
-        assertTrue(
-                "expected to find VAL1",
-                ((List) JsonPath.read(doc, "$[?(@=='VAL1')]")).size() == 1);
-        assertTrue(
-                "expected to find VAL2",
-                ((List) JsonPath.read(doc, "$[?(@=='VAL2')]")).size() == 1);
-        assertTrue(
-                "expected to find VAL3",
-                ((List) JsonPath.read(doc, "$[?(@=='VAL3')]")).size() == 1);
-
-        /**
-         * A bean is also an object. But in order to test the static
-         * method getNames(), this particular bean needs some public
-         * data members.
-         */
-        MyPublicClass myPublicClass = new MyPublicClass();
-        names = JSONObject.getNames(myPublicClass);
-
-        // validate JSON
-        JSONArray jsonArray2 = new JSONArray(names);
-        doc = Configuration.defaultConfiguration().jsonProvider()
-                .parse(jsonArray2.toString());
-        docList = JsonPath.read(doc, "$");
-        assertTrue("expected 2 items", docList.size() == 2);
-        assertTrue(
-                "expected to find publicString",
-                ((List) JsonPath.read(doc, "$[?(@=='publicString')]")).size() == 1);
-        assertTrue(
-                "expected to find publicInt",
-                ((List) JsonPath.read(doc, "$[?(@=='publicInt')]")).size() == 1);
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject0, jsonObject1, jsonObject2
-        )));
-        Util.checkJSONArrayMaps(jsonArray0, jsonObject0.getMapType());
-        Util.checkJSONArrayMaps(jsonArray1, jsonObject0.getMapType());
-        Util.checkJSONArrayMaps(jsonArray2, jsonObject0.getMapType());
-    }
-
-    /**
-     * Populate a JSONArray from an empty JSONObject names() method.
-     * It should be empty.
-     */
-    @Test
-    public void emptyJsonObjectNamesToJsonAray() {
-        JSONObject jsonObject = new JSONObject();
-        JSONArray jsonArray = jsonObject.names();
-        assertTrue("jsonArray should be null", jsonArray == null);
-        Util.checkJSONObjectMaps(jsonObject);
-        Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
-    }
-
-    /**
-     * Populate a JSONArray from a JSONObject names() method.
-     * Confirm that it contains the expected names.
-     */
-    @Test
-    public void jsonObjectNamesToJsonAray() {
-        String str = 
-            "{"+
-                "\"trueKey\":true,"+
-                "\"falseKey\":false,"+
-                "\"stringKey\":\"hello world!\""+
-            "}";
-
-        JSONObject jsonObject = new JSONObject(str);
-        JSONArray jsonArray = jsonObject.names();
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray.toString());
-        assertTrue("expected 3 top level items", ((List)(JsonPath.read(doc, "$"))).size() == 3);
-        assertTrue("expected to find trueKey", ((List) JsonPath.read(doc, "$[?(@=='trueKey')]")).size() == 1);
-        assertTrue("expected to find falseKey", ((List) JsonPath.read(doc, "$[?(@=='falseKey')]")).size() == 1);
-        assertTrue("expected to find stringKey", ((List) JsonPath.read(doc, "$[?(@=='stringKey')]")).size() == 1);
-        Util.checkJSONObjectMaps(jsonObject);
-        Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
-    }
-
-    /**
-     * Exercise the JSONObject increment() method.
-     */
-    @SuppressWarnings("cast")
-    @Test
-    public void jsonObjectIncrement() {
-        String str = 
-            "{"+
-                "\"keyLong\":9999999991,"+
-                "\"keyDouble\":1.1"+
-             "}";
-        JSONObject jsonObject = new JSONObject(str);
-        jsonObject.increment("keyInt");
-        jsonObject.increment("keyInt");
-        jsonObject.increment("keyLong");
-        jsonObject.increment("keyDouble");
-        jsonObject.increment("keyInt");
-        jsonObject.increment("keyLong");
-        jsonObject.increment("keyDouble");
-        /**
-         * JSONObject constructor won't handle these types correctly, but
-         * adding them via put works.
-         */
-        jsonObject.put("keyFloat", 1.1f);
-        jsonObject.put("keyBigInt", new BigInteger("123456789123456789123456789123456780"));
-        jsonObject.put("keyBigDec", new BigDecimal("123456789123456789123456789123456780.1"));
-        jsonObject.increment("keyFloat");
-        jsonObject.increment("keyFloat");
-        jsonObject.increment("keyBigInt");
-        jsonObject.increment("keyBigDec");
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 6 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 6);
-        assertTrue("expected 3", Integer.valueOf(3).equals(jsonObject.query("/keyInt")));
-        assertTrue("expected 9999999993", Long.valueOf(9999999993L).equals(jsonObject.query("/keyLong")));
-        assertTrue("expected 3.1", BigDecimal.valueOf(3.1).equals(jsonObject.query("/keyDouble")));
-        assertTrue("expected 123456789123456789123456789123456781", new BigInteger("123456789123456789123456789123456781").equals(jsonObject.query("/keyBigInt")));
-        assertTrue("expected 123456789123456789123456789123456781.1", new BigDecimal("123456789123456789123456789123456781.1").equals(jsonObject.query("/keyBigDec")));
-
-        /**
-         * Should work the same way on any platform! @see https://docs.oracle
-         * .com/javase/specs/jls/se7/html/jls-4.html#jls-4.2.3 This is the
-         * effect of a float to double conversion and is inherent to the
-         * shortcomings of the IEEE 754 format, when converting 32-bit into
-         * double-precision 64-bit. Java type-casts float to double. A 32 bit
-         * float is type-casted to 64 bit double by simply appending zero-bits
-         * to the mantissa (and extended the signed exponent by 3 bits.) and
-         * there is no way to obtain more information than it is stored in the
-         * 32-bits float.
-         * 
-         * Like 1/3 cannot be represented as base10 number because it is
-         * periodically, 1/5 (for example) cannot be represented as base2 number
-         * since it is periodically in base2 (take a look at
-         * http://www.h-schmidt.net/FloatConverter/) The same happens to 3.1,
-         * that decimal number (base10 representation) is periodic in base2
-         * representation, therefore appending zero-bits is inaccurate. Only
-         * repeating the periodically occurring bits (0110) would be a proper
-         * conversion. However one cannot detect from a 32 bit IEE754
-         * representation which bits would "repeat infinitely", since the
-         * missing bits would not fit into the 32 bit float, i.e. the
-         * information needed simply is not there!
-         */
-        assertEquals(Float.valueOf(3.1f), jsonObject.query("/keyFloat"));
-
-        /**
-         * float f = 3.1f; double df = (double) f; double d = 3.1d;
-         * System.out.println
-         * (Integer.toBinaryString(Float.floatToRawIntBits(f)));
-         * System.out.println
-         * (Long.toBinaryString(Double.doubleToRawLongBits(df)));
-         * System.out.println
-         * (Long.toBinaryString(Double.doubleToRawLongBits(d)));
-         * 
-         * - Float:
-         * seeeeeeeemmmmmmmmmmmmmmmmmmmmmmm
-         * 1000000010001100110011001100110
-         * - Double
-         * seeeeeeeeeeemmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
-         * 10000000   10001100110011001100110
-         * 100000000001000110011001100110011000000000000000000000000000000
-         * 100000000001000110011001100110011001100110011001100110011001101
-         */
-
-        /**
-        * Examples of well documented but probably unexpected behavior in 
-        * java / with 32-bit float to 64-bit float conversion.
-        */
-        assertFalse("Document unexpected behaviour with explicit type-casting float as double!", (double)0.2f == 0.2d );
-        assertFalse("Document unexpected behaviour with implicit type-cast!", 0.2f == 0.2d );
-        Double d1 = Double.valueOf( 1.1f );
-        Double d2 = Double.valueOf( "1.1f" );
-        assertFalse( "Document implicit type cast from float to double before calling Double(double d) constructor", d1.equals( d2 ) );
-
-        assertTrue( "Correctly converting float to double via base10 (string) representation!", Double.valueOf( 3.1d ).equals(  Double.valueOf( Float.valueOf( 3.1f ).toString() ) ) );
-
-        // Pinpointing the not so obvious "buggy" conversion from float to double in JSONObject
-        JSONObject jo = new JSONObject();
-        jo.put( "bug", 3.1f ); // will call put( String key, double value ) with implicit and "buggy" type-cast from float to double
-        assertFalse( "The java-compiler did add some zero bits for you to the mantissa (unexpected, but well documented)", jo.get( "bug" ).equals(  Double.valueOf( 3.1d ) ) );
-
-        JSONObject inc = new JSONObject();
-        inc.put( "bug", Float.valueOf( 3.1f ) ); // This will put in instance of Float into JSONObject, i.e. call put( String key, Object value )
-        assertTrue( "Everything is ok here!", inc.get( "bug" ) instanceof Float );
-        inc.increment( "bug" ); // after adding 1, increment will call put( String key, double value ) with implicit and "buggy" type-cast from float to double!
-        // this.put(key, (Float) value + 1);
-        // 1.        The (Object)value will be typecasted to (Float)value since it is an instanceof Float actually nothing is done. 
-        // 2.        Float instance will be autoboxed into float because the + operator will work on primitives not Objects!
-        // 3.        A float+float operation will be performed and results into a float primitive.
-        // 4.        There is no method that matches the signature put( String key, float value), java-compiler will choose the method
-        //                put( String key, double value) and does an implicit type-cast(!) by appending zero-bits to the mantissa
-        assertTrue( "JSONObject increment converts Float to Double", jo.get( "bug" ) instanceof Float );
-        // correct implementation (with change of behavior) would be:
-        // this.put(key, new Float((Float) value + 1)); 
-        // Probably it would be better to deprecate the method and remove some day, while convenient processing the "payload" is not
-        // really in the scope of a JSON-library (IMHO.)
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject, inc
-        )));
-    }
-
-    /**
-     * Exercise JSONObject numberToString() method
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void jsonObjectNumberToString() {
-        String str;
-        Double dVal;
-        Integer iVal = 1;
-        str = JSONObject.numberToString(iVal);
-        assertTrue("expected "+iVal+" actual "+str, iVal.toString().equals(str));
-        dVal = 12.34;
-        str = JSONObject.numberToString(dVal);
-        assertTrue("expected "+dVal+" actual "+str, dVal.toString().equals(str));
-        dVal = 12.34e27;
-        str = JSONObject.numberToString(dVal);
-        assertTrue("expected "+dVal+" actual "+str, dVal.toString().equals(str));
-        // trailing .0 is truncated, so it doesn't quite match toString()
-        dVal = 5000000.0000000;
-        str = JSONObject.numberToString(dVal);
-        assertTrue("expected 5000000 actual "+str, str.equals("5000000"));
-    }
-
-    /**
-     * Exercise JSONObject put() and similar() methods
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void jsonObjectPut() {
-        String expectedStr = 
-            "{"+
-                "\"trueKey\":true,"+
-                "\"falseKey\":false,"+
-                "\"arrayKey\":[0,1,2],"+
-                "\"objectKey\":{"+
-                    "\"myKey1\":\"myVal1\","+
-                    "\"myKey2\":\"myVal2\","+
-                    "\"myKey3\":\"myVal3\","+
-                    "\"myKey4\":\"myVal4\""+
-                "}"+
-            "}";
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.put("trueKey", true);
-        jsonObject.put("falseKey", false);
-        Integer [] intArray = { 0, 1, 2 };
-        jsonObject.put("arrayKey", Arrays.asList(intArray));
-        Map myMap = new HashMap();
-        myMap.put("myKey1", "myVal1");
-        myMap.put("myKey2", "myVal2");
-        myMap.put("myKey3", "myVal3");
-        myMap.put("myKey4", "myVal4");
-        jsonObject.put("objectKey", myMap);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 4 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 4);
-        assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
-        assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
-        assertTrue("expected 3 arrayKey items", ((List)(JsonPath.read(doc, "$.arrayKey"))).size() == 3);
-        assertTrue("expected 0", Integer.valueOf(0).equals(jsonObject.query("/arrayKey/0")));
-        assertTrue("expected 1", Integer.valueOf(1).equals(jsonObject.query("/arrayKey/1")));
-        assertTrue("expected 2", Integer.valueOf(2).equals(jsonObject.query("/arrayKey/2")));
-        assertTrue("expected 4 objectKey items", ((Map)(JsonPath.read(doc, "$.objectKey"))).size() == 4);
-        assertTrue("expected myVal1", "myVal1".equals(jsonObject.query("/objectKey/myKey1")));
-        assertTrue("expected myVal2", "myVal2".equals(jsonObject.query("/objectKey/myKey2")));
-        assertTrue("expected myVal3", "myVal3".equals(jsonObject.query("/objectKey/myKey3")));
-        assertTrue("expected myVal4", "myVal4".equals(jsonObject.query("/objectKey/myKey4")));
-
-        jsonObject.remove("trueKey");
-        JSONObject expectedJsonObject = new JSONObject(expectedStr);
-        assertTrue("unequal jsonObjects should not be similar",
-                !jsonObject.similar(expectedJsonObject));
-        assertTrue("jsonObject should not be similar to jsonArray",
-                !jsonObject.similar(new JSONArray()));
-
-        String aCompareValueStr = "{\"a\":\"aval\",\"b\":true}";
-        String bCompareValueStr = "{\"a\":\"notAval\",\"b\":true}";
-        JSONObject aCompareValueJsonObject = new JSONObject(aCompareValueStr);
-        JSONObject bCompareValueJsonObject = new JSONObject(bCompareValueStr);
-        assertTrue("different values should not be similar",
-                !aCompareValueJsonObject.similar(bCompareValueJsonObject));
-
-        String aCompareObjectStr = "{\"a\":\"aval\",\"b\":{}}";
-        String bCompareObjectStr = "{\"a\":\"aval\",\"b\":true}";
-        JSONObject aCompareObjectJsonObject = new JSONObject(aCompareObjectStr);
-        JSONObject bCompareObjectJsonObject = new JSONObject(bCompareObjectStr);
-        assertTrue("different nested JSONObjects should not be similar",
-                !aCompareObjectJsonObject.similar(bCompareObjectJsonObject));
-
-        String aCompareArrayStr = "{\"a\":\"aval\",\"b\":[]}";
-        String bCompareArrayStr = "{\"a\":\"aval\",\"b\":true}";
-        JSONObject aCompareArrayJsonObject = new JSONObject(aCompareArrayStr);
-        JSONObject bCompareArrayJsonObject = new JSONObject(bCompareArrayStr);
-        assertTrue("different nested JSONArrays should not be similar",
-                !aCompareArrayJsonObject.similar(bCompareArrayJsonObject));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject, expectedJsonObject, aCompareValueJsonObject,
-                aCompareArrayJsonObject, aCompareObjectJsonObject, aCompareArrayJsonObject,
-                bCompareValueJsonObject, bCompareArrayJsonObject, bCompareObjectJsonObject,
-                bCompareArrayJsonObject
-        )));
-    }
-
-    /**
-     * Exercise JSONObject toString() method
-     */
-    @Test
-    public void jsonObjectToString() {
-        String str = 
-            "{"+
-                "\"trueKey\":true,"+
-                "\"falseKey\":false,"+
-                "\"arrayKey\":[0,1,2],"+
-                "\"objectKey\":{"+
-                    "\"myKey1\":\"myVal1\","+
-                    "\"myKey2\":\"myVal2\","+
-                    "\"myKey3\":\"myVal3\","+
-                    "\"myKey4\":\"myVal4\""+
-                "}"+
-            "}";
-        JSONObject jsonObject = new JSONObject(str);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 4 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 4);
-        assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
-        assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
-        assertTrue("expected 3 arrayKey items", ((List)(JsonPath.read(doc, "$.arrayKey"))).size() == 3);
-        assertTrue("expected 0", Integer.valueOf(0).equals(jsonObject.query("/arrayKey/0")));
-        assertTrue("expected 1", Integer.valueOf(1).equals(jsonObject.query("/arrayKey/1")));
-        assertTrue("expected 2", Integer.valueOf(2).equals(jsonObject.query("/arrayKey/2")));
-        assertTrue("expected 4 objectKey items", ((Map)(JsonPath.read(doc, "$.objectKey"))).size() == 4);
-        assertTrue("expected myVal1", "myVal1".equals(jsonObject.query("/objectKey/myKey1")));
-        assertTrue("expected myVal2", "myVal2".equals(jsonObject.query("/objectKey/myKey2")));
-        assertTrue("expected myVal3", "myVal3".equals(jsonObject.query("/objectKey/myKey3")));
-        assertTrue("expected myVal4", "myVal4".equals(jsonObject.query("/objectKey/myKey4")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Exercise JSONObject toString() method with various indent levels.
-     */
-    @Test
-    public void jsonObjectToStringIndent() {
-        String jsonObject0Str =
-                "{"+
-                        "\"key1\":" +
-                                "[1,2," +
-                                        "{\"key3\":true}" +
-                                "],"+
-                        "\"key2\":" +
-                                "{\"key1\":\"val1\",\"key2\":" +
-                                        "{\"key2\":\"val2\"}" +
-                                "},"+
-                        "\"key3\":" +
-                                "[" +
-                                        "[1,2.1]" +
-                                "," +
-                                        "[null]" +
-                                "]"+
-                        "}";
-
-        String jsonObject1Str =
-                "{\n" +
-                " \"key1\": [\n" +
-                "  1,\n" +
-                "  2,\n" +
-                "  {\"key3\": true}\n" +
-                " ],\n" +
-                " \"key2\": {\n" +
-                "  \"key1\": \"val1\",\n" +
-                "  \"key2\": {\"key2\": \"val2\"}\n" +
-                " },\n" +
-                " \"key3\": [\n" +
-                "  [\n" +
-                "   1,\n" +
-                "   2.1\n" +
-                "  ],\n" +
-                "  [null]\n" +
-                " ]\n" +
-                "}";
-        String jsonObject4Str =
-                "{\n" +
-                "    \"key1\": [\n" +
-                "        1,\n" +
-                "        2,\n" +
-                "        {\"key3\": true}\n" +
-                "    ],\n" +
-                "    \"key2\": {\n" +
-                "        \"key1\": \"val1\",\n" +
-                "        \"key2\": {\"key2\": \"val2\"}\n" +
-                "    },\n" +
-                "    \"key3\": [\n" +
-                "        [\n" +
-                "            1,\n" +
-                "            2.1\n" +
-                "        ],\n" +
-                "        [null]\n" +
-                "    ]\n" +
-                "}";
-        JSONObject jsonObject = new JSONObject(jsonObject0Str);
-        // contents are tested in other methods, in this case just validate the spacing by
-        // checking length
-        assertEquals("toString() length",jsonObject0Str.length(), jsonObject.toString().length());
-        assertEquals("toString(0) length",jsonObject0Str.length(), jsonObject.toString(0).length());
-        assertEquals("toString(1) length",jsonObject1Str.length(), jsonObject.toString(1).length());
-        assertEquals("toString(4) length",jsonObject4Str.length(), jsonObject.toString(4).length());
-        
-        JSONObject jo = new JSONObject().put("TABLE", new JSONObject().put("yhoo", new JSONObject()));
-        assertEquals("toString(2)","{\"TABLE\": {\"yhoo\": {}}}", jo.toString(2));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject, jo
-        )));
-    }
-
-    /**
-     * Explores how JSONObject handles maps. Insert a string/string map
-     * as a value in a JSONObject. It will remain a map. Convert the 
-     * JSONObject to string, then create a new JSONObject from the string. 
-     * In the new JSONObject, the value will be stored as a nested JSONObject.
-     * Confirm that map and nested JSONObject have the same contents.
-     */
-    @Test
-    public void jsonObjectToStringSuppressWarningOnCastToMap() {
-        JSONObject jsonObject = new JSONObject();
-        Map map = new HashMap<>();
-        map.put("abc", "def");
-        jsonObject.put("key", map);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 1 top level item", ((Map)(JsonPath.read(doc, "$"))).size() == 1);
-        assertTrue("expected 1 key item", ((Map)(JsonPath.read(doc, "$.key"))).size() == 1);
-        assertTrue("expected def", "def".equals(jsonObject.query("/key/abc")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Explores how JSONObject handles collections. Insert a string collection
-     * as a value in a JSONObject. It will remain a collection. Convert the 
-     * JSONObject to string, then create a new JSONObject from the string. 
-     * In the new JSONObject, the value will be stored as a nested JSONArray.
-     * Confirm that collection and nested JSONArray have the same contents.
-     */
-    @Test
-    public void jsonObjectToStringSuppressWarningOnCastToCollection() {
-        JSONObject jsonObject = new JSONObject();
-        Collection collection = new ArrayList();
-        collection.add("abc");
-        // ArrayList will be added as an object
-        jsonObject.put("key", collection);
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
-        assertTrue("expected 1 top level item", ((Map)(JsonPath.read(doc, "$"))).size() == 1);
-        assertTrue("expected 1 key item", ((List)(JsonPath.read(doc, "$.key"))).size() == 1);
-        assertTrue("expected abc", "abc".equals(jsonObject.query("/key/0")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Exercises the JSONObject.valueToString() method for various types
-     */
-    @Test
-    public void valueToString() {
-        
-        assertTrue("null valueToString() incorrect",
-                "null".equals(JSONObject.valueToString(null)));
-        MyJsonString jsonString = new MyJsonString();
-        assertTrue("jsonstring valueToString() incorrect",
-                "my string".equals(JSONObject.valueToString(jsonString)));
-        assertTrue("boolean valueToString() incorrect",
-                "true".equals(JSONObject.valueToString(Boolean.TRUE)));
-        assertTrue("non-numeric double",
-                "null".equals(JSONObject.doubleToString(Double.POSITIVE_INFINITY)));
-        String jsonObjectStr = 
-            "{"+
-                "\"key1\":\"val1\","+
-                "\"key2\":\"val2\","+
-                "\"key3\":\"val3\""+
-             "}";
-        JSONObject jsonObject = new JSONObject(jsonObjectStr);
-        assertTrue("jsonObject valueToString() incorrect",
-            new JSONObject(JSONObject.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()));
-        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))));
-        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))); 
-        Integer[] array = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) };
-        assertTrue("array valueToString() incorrect",
-                jsonArray.toString().equals(JSONObject.valueToString(array)));
-        Util.checkJSONObjectMaps(jsonObject);
-        Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
-    }
-
-    /**
-     * Confirm that https://github.com/douglascrockford/JSON-java/issues/167 is fixed.
-     * The following code was throwing a ClassCastException in the 
-     * JSONObject(Map) constructor
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    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);
-        // confirm result, just in case
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(str);
-        assertTrue("expected 1 top level item", ((Map)(JsonPath.read(doc, "$"))).size() == 1);
-        assertTrue("expected myValue", "myValue".equals(JsonPath.read(doc, "$.1")));
-    }
-
-    /**
-     * Exercise the JSONObject wrap() method. Sometimes wrap() will change
-     * the object being wrapped, other times not. The purpose of wrap() is
-     * to ensure the value is packaged in a way that is compatible with how
-     * a JSONObject value or JSONArray value is supposed to be stored.
-     */
-    @Test
-    public void wrapObject() {
-        // wrap(null) returns NULL
-        assertTrue("null wrap() incorrect",
-                JSONObject.NULL == JSONObject.wrap(null));
-
-        // wrap(Integer) returns Integer
-        Integer in = Integer.valueOf(1);
-        assertTrue("Integer wrap() incorrect",
-                in == JSONObject.wrap(in));
-
-        /**
-         * This test is to document the preferred behavior if BigDecimal is
-         * supported. Previously bd returned as a string, since it
-         * is recognized as being a Java package class. Now with explicit
-         * support for big numbers, it remains a BigDecimal 
-         */
-        Object bdWrap = JSONObject.wrap(BigDecimal.ONE);
-        assertTrue("BigDecimal.ONE evaluates to ONE",
-                bdWrap.equals(BigDecimal.ONE));
-
-        // wrap JSONObject returns JSONObject
-        String jsonObjectStr = 
-                "{"+
-                    "\"key1\":\"val1\","+
-                    "\"key2\":\"val2\","+
-                    "\"key3\":\"val3\""+
-                 "}";
-        JSONObject jsonObject = new JSONObject(jsonObjectStr);
-        assertTrue("JSONObject wrap() incorrect",
-                jsonObject == JSONObject.wrap(jsonObject));
-
-        // wrap collection returns JSONArray
-        Collection collection = new ArrayList();
-        collection.add(Integer.valueOf(1));
-        collection.add(Integer.valueOf(2));
-        collection.add(Integer.valueOf(3));
-        JSONArray jsonArray = (JSONArray) (JSONObject.wrap(collection));
-
-        // validate JSON
-        Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray.toString());
-        assertTrue("expected 3 top level items", ((List)(JsonPath.read(doc, "$"))).size() == 3);
-        assertTrue("expected 1", Integer.valueOf(1).equals(jsonArray.query("/0")));
-        assertTrue("expected 2", Integer.valueOf(2).equals(jsonArray.query("/1")));
-        assertTrue("expected 3", Integer.valueOf(3).equals(jsonArray.query("/2")));
-
-        // wrap Array returns JSONArray
-        Integer[] array = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) };
-        JSONArray integerArrayJsonArray = (JSONArray)(JSONObject.wrap(array));
-
-        // validate JSON
-        doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray.toString());
-        assertTrue("expected 3 top level items", ((List)(JsonPath.read(doc, "$"))).size() == 3);
-        assertTrue("expected 1", Integer.valueOf(1).equals(jsonArray.query("/0")));
-        assertTrue("expected 2", Integer.valueOf(2).equals(jsonArray.query("/1")));
-        assertTrue("expected 3", Integer.valueOf(3).equals(jsonArray.query("/2")));
-
-        // validate JSON
-        doc = Configuration.defaultConfiguration().jsonProvider().parse(integerArrayJsonArray.toString());
-        assertTrue("expected 3 top level items", ((List)(JsonPath.read(doc, "$"))).size() == 3);
-        assertTrue("expected 1", Integer.valueOf(1).equals(jsonArray.query("/0")));
-        assertTrue("expected 2", Integer.valueOf(2).equals(jsonArray.query("/1")));
-        assertTrue("expected 3", Integer.valueOf(3).equals(jsonArray.query("/2")));
-
-        // wrap map returns JSONObject
-        Map map = new HashMap();
-        map.put("key1", "val1");
-        map.put("key2", "val2");
-        map.put("key3", "val3");
-        JSONObject mapJsonObject = (JSONObject) (JSONObject.wrap(map));
-
-        // validate JSON
-        doc = Configuration.defaultConfiguration().jsonProvider().parse(mapJsonObject.toString());
-        assertTrue("expected 3 top level items", ((Map)(JsonPath.read(doc, "$"))).size() == 3);
-        assertTrue("expected val1", "val1".equals(mapJsonObject.query("/key1")));
-        assertTrue("expected val2", "val2".equals(mapJsonObject.query("/key2")));
-        assertTrue("expected val3", "val3".equals(mapJsonObject.query("/key3")));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObject, mapJsonObject
-        )));
-        Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
-        Util.checkJSONArrayMaps(integerArrayJsonArray, jsonObject.getMapType());
-    }
-
-    
-    /**
-     * RFC 7159 defines control characters to be U+0000 through U+001F. This test verifies that the parser is checking for these in expected ways.
-     */
-    @Test
-    public void jsonObjectParseControlCharacters(){
-        for(int i = 0;i<=0x001f;i++){
-            final String charString = String.valueOf((char)i);
-            final String source = "{\"key\":\""+charString+"\"}";
-            try {
-                JSONObject jo = new JSONObject(source);
-                assertTrue("Expected "+charString+"("+i+") in the JSON Object but did not find it.",charString.equals(jo.getString("key")));
-                Util.checkJSONObjectMaps(jo);
-            } catch (JSONException ex) {
-                assertTrue("Only \\0 (U+0000), \\n (U+000A), and \\r (U+000D) should cause an error. Instead "+charString+"("+i+") caused an error",
-                        i=='\0' || i=='\n' || i=='\r'
-                );
-            }
-        }
-    }
-
-    @Test
-    public void jsonObjectParseControlCharacterEOFAssertExceptionMessage(){
-        char c = '\0';
-        final String source = "{\"key\":\"" + c + "\"}";
-        try {
-            JSONObject jo = new JSONObject(source);
-            fail("JSONException should be thrown");
-        } catch (JSONException ex) {
-            assertEquals("Unterminated string. " + "Character with int code 0" +
-                    " is not allowed within a quoted string. at 8 [character 9 line 1]", ex.getMessage());
-        }
-    }
-
-    @Test
-    public void jsonObjectParseControlCharacterNewLineAssertExceptionMessage(){
-        char[] chars = {'\n', '\r'};
-        for( char c : chars) {
-            final String source = "{\"key\":\"" + c + "\"}";
-            try {
-                JSONObject jo = new JSONObject(source);
-                fail("JSONException should be thrown");
-            } catch (JSONException ex) {
-                assertEquals("Unterminated string. " + "Character with int code " + (int) c +
-                        " is not allowed within a quoted string. at 9 [character 0 line 2]", ex.getMessage());
-            }
-        }
-    }
-
-    @Test
-    public void jsonObjectParseUTF8EncodingAssertExceptionMessage(){
-        String c = "\\u123x";
-        final String source = "{\"key\":\"" + c + "\"}";
-        try {
-            JSONObject jo = new JSONObject(source);
-            fail("JSONException should be thrown");
-        } catch (JSONException ex) {
-            assertEquals("Illegal escape. \\u must be followed by a 4 digit hexadecimal number. " +
-                    "\\123x is not valid. at 14 [character 15 line 1]", ex.getMessage());
-        }
-    }
-
-    @Test
-    public void jsonObjectParseIllegalEscapeAssertExceptionMessage(){
-        String c = "\\x";
-        final String source = "{\"key\":\"" + c + "\"}";
-        try {
-            JSONObject jo = new JSONObject(source);
-            fail("JSONException should be thrown");
-        } catch (JSONException ex) {
-            assertEquals("Illegal escape. Escape sequence  " + c + " is not valid." +
-                    " at 10 [character 11 line 1]", ex.getMessage());
-        }
-    }
-
-    /**
-     * Explore how JSONObject handles parsing errors.
-     */
-    @SuppressWarnings({"boxing", "unused"})
-    @Test
-    public void jsonObjectParsingErrors() {
-        JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
-        if (jsonParserConfiguration.isStrictMode()) {
-            System.out.println("Skipping JSONObjectTest jsonObjectParsingErrors() when strictMode default is true");
-        } else {
-            try {
-                // does not start with '{'
-                String str = "abc";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "A JSONObject text must begin with '{' at 1 [character 2 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // does not end with '}'
-                String str = "{";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "A JSONObject text must end with '}' at 1 [character 2 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // key with no ':'
-                String str = "{\"myKey\" = true}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Expected a ':' after a key at 10 [character 11 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // entries with no ',' separator
-                String str = "{\"myKey\":true \"myOtherKey\":false}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Expected a ',' or '}' at 15 [character 16 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // key is a nested map
-                String str = "{{\"foo\": \"bar\"}: \"baz\"}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Missing value at 1 [character 2 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // key is a nested array containing a map
-                String str = "{\"a\": 1, [{\"foo\": \"bar\"}]: \"baz\"}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Missing value at 9 [character 10 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // key contains }
-                String str = "{foo}: 2}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Expected a ':' after a key at 5 [character 6 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // key contains ]
-                String str = "{foo]: 2}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Expected a ':' after a key at 5 [character 6 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // \0 after ,
-                String str = "{\"myKey\":true, \0\"myOtherKey\":false}";
-                assertNull("Expected an exception", new JSONObject(str));
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "A JSONObject text must end with '}' at 15 [character 16 line 1]",
-                        e.getMessage());
-            }
-            try {
-                // append to wrong key
-                String str = "{\"myKey\":true, \"myOtherKey\":false}";
-                JSONObject jsonObject = new JSONObject(str);
-                jsonObject.append("myKey", "hello");
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "JSONObject[\"myKey\"] is not a JSONArray (null).",
-                        e.getMessage());
-            }
-            try {
-                // increment wrong key
-                String str = "{\"myKey\":true, \"myOtherKey\":false}";
-                JSONObject jsonObject = new JSONObject(str);
-                jsonObject.increment("myKey");
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Unable to increment [\"myKey\"].",
-                        e.getMessage());
-            }
-            try {
-                // invalid key
-                String str = "{\"myKey\":true, \"myOtherKey\":false}";
-                JSONObject jsonObject = new JSONObject(str);
-                jsonObject.get(null);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Null key.",
-                        e.getMessage());
-            }
-            try {
-                // invalid numberToString()
-                JSONObject.numberToString((Number) null);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an exception message",
-                        "Null pointer",
-                        e.getMessage());
-            }
-
-            try {
-                // multiple putOnce key
-                JSONObject jsonObject = new JSONObject("{}");
-                jsonObject.putOnce("hello", "world");
-                jsonObject.putOnce("hello", "world!");
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertTrue("", true);
-            }
-            try {
-                // test validity of invalid double
-                JSONObject.testValidity(Double.NaN);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertTrue("", true);
-            }
-            try {
-                // test validity of invalid float
-                JSONObject.testValidity(Float.NEGATIVE_INFINITY);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertTrue("", true);
-            }
-            try {
-                // test exception message when including a duplicate key (level 0)
-                String str = "{\n"
-                        + "    \"attr01\":\"value-01\",\n"
-                        + "    \"attr02\":\"value-02\",\n"
-                        + "    \"attr03\":\"value-03\",\n"
-                        + "    \"attr03\":\"value-04\"\n"
-                        + "}";
-                new JSONObject(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr03\" at 90 [character 13 line 5]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key (level 0) holding an object
-                String str = "{\n"
-                        + "    \"attr01\":\"value-01\",\n"
-                        + "    \"attr02\":\"value-02\",\n"
-                        + "    \"attr03\":\"value-03\",\n"
-                        + "    \"attr03\": {"
-                        + "        \"attr04-01\":\"value-04-01\",n"
-                        + "        \"attr04-02\":\"value-04-02\",n"
-                        + "        \"attr04-03\":\"value-04-03\"n"
-                        + "    }\n"
-                        + "}";
-                new JSONObject(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr03\" at 90 [character 13 line 5]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key (level 0) holding an array
-                String str = "{\n"
-                        + "    \"attr01\":\"value-01\",\n"
-                        + "    \"attr02\":\"value-02\",\n"
-                        + "    \"attr03\":\"value-03\",\n"
-                        + "    \"attr03\": [\n"
-                        + "        {"
-                        + "            \"attr04-01\":\"value-04-01\",n"
-                        + "            \"attr04-02\":\"value-04-02\",n"
-                        + "            \"attr04-03\":\"value-04-03\"n"
-                        + "        }\n"
-                        + "    ]\n"
-                        + "}";
-                new JSONObject(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr03\" at 90 [character 13 line 5]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key (level 1)
-                String str = "{\n"
-                        + "    \"attr01\":\"value-01\",\n"
-                        + "    \"attr02\":\"value-02\",\n"
-                        + "    \"attr03\":\"value-03\",\n"
-                        + "    \"attr04\": {\n"
-                        + "        \"attr04-01\":\"value04-01\",\n"
-                        + "        \"attr04-02\":\"value04-02\",\n"
-                        + "        \"attr04-03\":\"value04-03\",\n"
-                        + "        \"attr04-03\":\"value04-04\"\n"
-                        + "    }\n"
-                        + "}";
-                new JSONObject(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key (level 1) holding an object
-                String str = "{\n"
-                        + "    \"attr01\":\"value-01\",\n"
-                        + "    \"attr02\":\"value-02\",\n"
-                        + "    \"attr03\":\"value-03\",\n"
-                        + "    \"attr04\": {\n"
-                        + "        \"attr04-01\":\"value04-01\",\n"
-                        + "        \"attr04-02\":\"value04-02\",\n"
-                        + "        \"attr04-03\":\"value04-03\",\n"
-                        + "        \"attr04-03\": {\n"
-                        + "            \"attr04-04-01\":\"value04-04-01\",\n"
-                        + "            \"attr04-04-02\":\"value04-04-02\",\n"
-                        + "            \"attr04-04-03\":\"value04-04-03\",\n"
-                        + "        }\n"
-                        + "    }\n"
-                        + "}";
-                new JSONObject(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key (level 1) holding an array
-                String str = "{\n"
-                        + "    \"attr01\":\"value-01\",\n"
-                        + "    \"attr02\":\"value-02\",\n"
-                        + "    \"attr03\":\"value-03\",\n"
-                        + "    \"attr04\": {\n"
-                        + "        \"attr04-01\":\"value04-01\",\n"
-                        + "        \"attr04-02\":\"value04-02\",\n"
-                        + "        \"attr04-03\":\"value04-03\",\n"
-                        + "        \"attr04-03\": [\n"
-                        + "            {\n"
-                        + "                \"attr04-04-01\":\"value04-04-01\",\n"
-                        + "                \"attr04-04-02\":\"value04-04-02\",\n"
-                        + "                \"attr04-04-03\":\"value04-04-03\",\n"
-                        + "            }\n"
-                        + "        ]\n"
-                        + "    }\n"
-                        + "}";
-                new JSONObject(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key in object (level 0) within an array
-                String str = "[\n"
-                        + "    {\n"
-                        + "        \"attr01\":\"value-01\",\n"
-                        + "        \"attr02\":\"value-02\"\n"
-                        + "    },\n"
-                        + "    {\n"
-                        + "        \"attr01\":\"value-01\",\n"
-                        + "        \"attr01\":\"value-02\"\n"
-                        + "    }\n"
-                        + "]";
-                new JSONArray(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr01\" at 124 [character 17 line 8]",
-                        e.getMessage());
-            }
-            try {
-                // test exception message when including a duplicate key in object (level 1) within an array
-                String str = "[\n"
-                        + "    {\n"
-                        + "        \"attr01\":\"value-01\",\n"
-                        + "        \"attr02\": {\n"
-                        + "            \"attr02-01\":\"value-02-01\",\n"
-                        + "            \"attr02-02\":\"value-02-02\"\n"
-                        + "        }\n"
-                        + "    },\n"
-                        + "    {\n"
-                        + "        \"attr01\":\"value-01\",\n"
-                        + "        \"attr02\": {\n"
-                        + "            \"attr02-01\":\"value-02-01\",\n"
-                        + "            \"attr02-01\":\"value-02-02\"\n"
-                        + "        }\n"
-                        + "    }\n"
-                        + "]";
-                new JSONArray(str);
-                fail("Expected an exception");
-            } catch (JSONException e) {
-                assertEquals("Expecting an expection message",
-                        "Duplicate key \"attr02-01\" at 269 [character 24 line 13]",
-                        e.getMessage());
-            }
-        }
-    }
-
-    /**
-     * Confirm behavior when putOnce() is called with null parameters
-     */
-    @Test
-    public void jsonObjectPutOnceNull() {
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.putOnce(null, null);
-        assertTrue("jsonObject should be empty", jsonObject.isEmpty());
-        jsonObject.putOnce("", null);
-        assertTrue("jsonObject should be empty", jsonObject.isEmpty());
-        jsonObject.putOnce(null, "");
-        assertTrue("jsonObject should be empty", jsonObject.isEmpty());
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-     * Exercise JSONObject opt(key, default) method.
-     */
-    @Test
-    public void jsonObjectOptDefault() {
-
-        String str = "{\"myKey\": \"myval\", \"hiKey\": null}";
-        JSONObject jsonObject = new JSONObject(str);
-
-        assertTrue("optBigDecimal() should return default BigDecimal",
-                BigDecimal.TEN.compareTo(jsonObject.optBigDecimal("myKey", BigDecimal.TEN))==0);
-        assertTrue("optBigInteger() should return default BigInteger",
-                BigInteger.TEN.compareTo(jsonObject.optBigInteger("myKey",BigInteger.TEN ))==0);
-        assertTrue("optBoolean() should return default boolean",
-                 jsonObject.optBoolean("myKey", true));
-        assertTrue("optBooleanObject() should return default Boolean",
-                 jsonObject.optBooleanObject("myKey", true));
-        assertTrue("optInt() should return default int",
-                42 == jsonObject.optInt("myKey", 42));
-        assertTrue("optIntegerObject() should return default Integer",
-                Integer.valueOf(42).equals(jsonObject.optIntegerObject("myKey", 42)));
-        assertTrue("optEnum() should return default Enum",
-                MyEnum.VAL1.equals(jsonObject.optEnum(MyEnum.class, "myKey", MyEnum.VAL1)));
-        assertTrue("optJSONArray() should return null ",
-                null==jsonObject.optJSONArray("myKey"));
-        assertTrue("optJSONArray() should return default JSONArray",
-                "value".equals(jsonObject.optJSONArray("myKey", new JSONArray("[\"value\"]")).getString(0)));
-        assertTrue("optJSONObject() should return default JSONObject ",
-                jsonObject.optJSONObject("myKey", new JSONObject("{\"testKey\":\"testValue\"}")).getString("testKey").equals("testValue"));
-        assertTrue("optLong() should return default long",
-                42l == jsonObject.optLong("myKey", 42l));
-        assertTrue("optLongObject() should return default Long",
-                Long.valueOf(42l).equals(jsonObject.optLongObject("myKey", 42l)));
-        assertTrue("optDouble() should return default double",
-                42.3d == jsonObject.optDouble("myKey", 42.3d));
-        assertTrue("optDoubleObject() should return default Double",
-                Double.valueOf(42.3d).equals(jsonObject.optDoubleObject("myKey", 42.3d)));
-        assertTrue("optFloat() should return default float",
-                42.3f == jsonObject.optFloat("myKey", 42.3f));
-        assertTrue("optFloatObject() should return default Float",
-                Float.valueOf(42.3f).equals(jsonObject.optFloatObject("myKey", 42.3f)));
-        assertTrue("optNumber() should return default Number",
-                42l == jsonObject.optNumber("myKey", Long.valueOf(42)).longValue());
-        assertTrue("optString() should return default string",
-                "hi".equals(jsonObject.optString("hiKey", "hi")));
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-    
-    /**
-     * Exercise JSONObject opt(key, default) method when the key doesn't exist.
-     */
-    @Test
-    public void jsonObjectOptNoKey() {
-
-         JSONObject jsonObject = new JSONObject();
-         
-         assertNull(jsonObject.opt(null));
-
-         assertTrue("optBigDecimal() should return default BigDecimal",
-                 BigDecimal.TEN.compareTo(jsonObject.optBigDecimal("myKey", BigDecimal.TEN))==0);
-         assertTrue("optBigInteger() should return default BigInteger",
-                 BigInteger.TEN.compareTo(jsonObject.optBigInteger("myKey",BigInteger.TEN ))==0);
-         assertTrue("optBoolean() should return default boolean",
-                  jsonObject.optBoolean("myKey", true));
-         assertTrue("optBooleanObject() should return default Boolean",
-                  jsonObject.optBooleanObject("myKey", true));
-         assertTrue("optInt() should return default int",
-                 42 == jsonObject.optInt("myKey", 42));
-         assertTrue("optIntegerObject() should return default Integer",
-                 Integer.valueOf(42).equals(jsonObject.optIntegerObject("myKey", 42)));
-         assertTrue("optEnum() should return default Enum",
-                 MyEnum.VAL1.equals(jsonObject.optEnum(MyEnum.class, "myKey", MyEnum.VAL1)));
-         assertTrue("optJSONArray() should return default JSONArray",
-                 "value".equals(jsonObject.optJSONArray("myKey", new JSONArray("[\"value\"]")).getString(0)));
-         assertTrue("optJSONArray() should return null ",
-                 null==jsonObject.optJSONArray("myKey"));
-         assertTrue("optJSONObject() should return default JSONObject ",
-                jsonObject.optJSONObject("myKey", new JSONObject("{\"testKey\":\"testValue\"}")).getString("testKey").equals("testValue"));
-         assertTrue("optLong() should return default long",
-                 42l == jsonObject.optLong("myKey", 42l));
-         assertTrue("optLongObject() should return default Long",
-                 Long.valueOf(42l).equals(jsonObject.optLongObject("myKey", 42l)));
-         assertTrue("optDouble() should return default double",
-                 42.3d == jsonObject.optDouble("myKey", 42.3d));
-         assertTrue("optDoubleObject() should return default Double",
-                 Double.valueOf(42.3d).equals(jsonObject.optDoubleObject("myKey", 42.3d)));
-         assertTrue("optFloat() should return default float",
-                 42.3f == jsonObject.optFloat("myKey", 42.3f));
-         assertTrue("optFloatObject() should return default Float",
-                 Float.valueOf(42.3f).equals(jsonObject.optFloatObject("myKey", 42.3f)));
-         assertTrue("optNumber() should return default Number",
-                 42l == jsonObject.optNumber("myKey", Long.valueOf(42)).longValue());
-         assertTrue("optString() should return default string",
-                 "hi".equals(jsonObject.optString("hiKey", "hi")));
-         Util.checkJSONObjectMaps(jsonObject);
-    }
-    
-    /**
-     * Verifies that the opt methods properly convert string values.
-     */
-    @Test
-    public void jsonObjectOptStringConversion() {
-        JSONObject jo = new JSONObject("{\"int\":\"123\",\"true\":\"true\",\"false\":\"false\"}");
-        assertTrue("unexpected optBoolean value",jo.optBoolean("true",false)==true);
-        assertTrue("unexpected optBooleanObject value",Boolean.valueOf(true).equals(jo.optBooleanObject("true",false)));
-        assertTrue("unexpected optBoolean value",jo.optBoolean("false",true)==false);
-        assertTrue("unexpected optBooleanObject value",Boolean.valueOf(false).equals(jo.optBooleanObject("false",true)));
-        assertTrue("unexpected optInt value",jo.optInt("int",0)==123);
-        assertTrue("unexpected optIntegerObject value",Integer.valueOf(123).equals(jo.optIntegerObject("int",0)));
-        assertTrue("unexpected optLong value",jo.optLong("int",0)==123l);
-        assertTrue("unexpected optLongObject value",Long.valueOf(123l).equals(jo.optLongObject("int",0L)));
-        assertTrue("unexpected optDouble value",jo.optDouble("int",0.0d)==123.0d);
-        assertTrue("unexpected optDoubleObject value",Double.valueOf(123.0d).equals(jo.optDoubleObject("int",0.0d)));
-        assertTrue("unexpected optFloat value",jo.optFloat("int",0.0f)==123.0f);
-        assertTrue("unexpected optFloatObject value",Float.valueOf(123.0f).equals(jo.optFloatObject("int",0.0f)));
-        assertTrue("unexpected optBigInteger value",jo.optBigInteger("int",BigInteger.ZERO).compareTo(new BigInteger("123"))==0);
-        assertTrue("unexpected optBigDecimal value",jo.optBigDecimal("int",BigDecimal.ZERO).compareTo(new BigDecimal("123"))==0);
-        assertTrue("unexpected optBigDecimal value",jo.optBigDecimal("int",BigDecimal.ZERO).compareTo(new BigDecimal("123"))==0);
-        assertTrue("unexpected optNumber value",jo.optNumber("int",BigInteger.ZERO).longValue()==123l);
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    /**
-     * Verifies that the opt methods properly convert string values to numbers and coerce them consistently.
-     */
-    @Test
-    public void jsonObjectOptCoercion() {
-        JSONObject jo = new JSONObject("{\"largeNumberStr\":\"19007199254740993.35481234487103587486413587843213584\"}");
-        // currently the parser doesn't recognize BigDecimal, to we have to put it manually
-        jo.put("largeNumber", new BigDecimal("19007199254740993.35481234487103587486413587843213584"));
-        
-        // Test type coercion from larger to smaller
-        assertEquals(new BigDecimal("19007199254740993.35481234487103587486413587843213584"), jo.optBigDecimal("largeNumber",null));
-        assertEquals(new BigInteger("19007199254740993"), jo.optBigInteger("largeNumber",null));
-        assertEquals(1.9007199254740992E16, jo.optDouble("largeNumber"),0.0);
-        assertEquals(1.9007199254740992E16, jo.optDoubleObject("largeNumber"),0.0);
-        assertEquals(1.90071995E16f, jo.optFloat("largeNumber"),0.0f);
-        assertEquals(1.90071995E16f, jo.optFloatObject("largeNumber"),0.0f);
-        assertEquals(19007199254740993l, jo.optLong("largeNumber"));
-        assertEquals(Long.valueOf(19007199254740993l), jo.optLongObject("largeNumber"));
-        assertEquals(1874919425, jo.optInt("largeNumber"));
-        assertEquals(Integer.valueOf(1874919425), jo.optIntegerObject("largeNumber"));
-
-        // conversion from a string
-        assertEquals(new BigDecimal("19007199254740993.35481234487103587486413587843213584"), jo.optBigDecimal("largeNumberStr",null));
-        assertEquals(new BigInteger("19007199254740993"), jo.optBigInteger("largeNumberStr",null));
-        assertEquals(1.9007199254740992E16, jo.optDouble("largeNumberStr"),0.0);
-        assertEquals(1.9007199254740992E16, jo.optDoubleObject("largeNumberStr"),0.0);
-        assertEquals(1.90071995E16f, jo.optFloat("largeNumberStr"),0.0f);
-        assertEquals(1.90071995E16f, jo.optFloatObject("largeNumberStr"),0.0f);
-        assertEquals(19007199254740993l, jo.optLong("largeNumberStr"));
-        assertEquals(Long.valueOf(19007199254740993l), jo.optLongObject("largeNumberStr"));
-        assertEquals(1874919425, jo.optInt("largeNumberStr"));
-        assertEquals(Integer.valueOf(1874919425), jo.optIntegerObject("largeNumberStr"));
-
-        // the integer portion of the actual value is larger than a double can hold.
-        assertNotEquals((long)Double.parseDouble("19007199254740993.35481234487103587486413587843213584"), jo.optLong("largeNumber"));
-        assertNotEquals(Long.valueOf((long)Double.parseDouble("19007199254740993.35481234487103587486413587843213584")), jo.optLongObject("largeNumber"));
-        assertNotEquals((int)Double.parseDouble("19007199254740993.35481234487103587486413587843213584"), jo.optInt("largeNumber"));
-        assertNotEquals(Integer.valueOf((int)Double.parseDouble("19007199254740993.35481234487103587486413587843213584")), jo.optIntegerObject("largeNumber"));
-        assertNotEquals((long)Double.parseDouble("19007199254740993.35481234487103587486413587843213584"), jo.optLong("largeNumberStr"));
-        assertNotEquals(Long.valueOf((long)Double.parseDouble("19007199254740993.35481234487103587486413587843213584")), jo.optLongObject("largeNumberStr"));
-        assertNotEquals((int)Double.parseDouble("19007199254740993.35481234487103587486413587843213584"), jo.optInt("largeNumberStr"));
-        assertNotEquals(Integer.valueOf((int)Double.parseDouble("19007199254740993.35481234487103587486413587843213584")), jo.optIntegerObject("largeNumberStr"));
-        assertEquals(19007199254740992l, (long)Double.parseDouble("19007199254740993.35481234487103587486413587843213584"));
-        assertEquals(2147483647, (int)Double.parseDouble("19007199254740993.35481234487103587486413587843213584"));
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    /**
-     * Verifies that the optBigDecimal method properly converts values to BigDecimal and coerce them consistently.
-     */
-    @Test
-    public void jsonObjectOptBigDecimal() {
-        JSONObject jo = new JSONObject().put("int", 123).put("long", 654L)
-                .put("float", 1.234f).put("double", 2.345d)
-                .put("bigInteger", new BigInteger("1234"))
-                .put("bigDecimal", new BigDecimal("1234.56789"))
-                .put("nullVal", JSONObject.NULL);
-        
-        assertEquals(new BigDecimal("123"),jo.optBigDecimal("int", null));
-        assertEquals(new BigDecimal("654"),jo.optBigDecimal("long", null));
-        assertEquals(new BigDecimal(1.234f),jo.optBigDecimal("float", null));
-        assertEquals(new BigDecimal(2.345d),jo.optBigDecimal("double", null));
-        assertEquals(new BigDecimal("1234"),jo.optBigDecimal("bigInteger", null));
-        assertEquals(new BigDecimal("1234.56789"),jo.optBigDecimal("bigDecimal", null));
-        assertNull(jo.optBigDecimal("nullVal", null));
-        assertEquals(jo.optBigDecimal("float", null),jo.getBigDecimal("float"));
-        assertEquals(jo.optBigDecimal("double", null),jo.getBigDecimal("double"));
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    /**
-     * Verifies that the optBigDecimal method properly converts values to BigDecimal and coerce them consistently.
-     */
-    @Test
-    public void jsonObjectOptBigInteger() {
-        JSONObject jo = new JSONObject().put("int", 123).put("long", 654L)
-                .put("float", 1.234f).put("double", 2.345d)
-                .put("bigInteger", new BigInteger("1234"))
-                .put("bigDecimal", new BigDecimal("1234.56789"))
-                .put("nullVal", JSONObject.NULL);
-        
-        assertEquals(new BigInteger("123"),jo.optBigInteger("int", null));
-        assertEquals(new BigInteger("654"),jo.optBigInteger("long", null));
-        assertEquals(new BigInteger("1"),jo.optBigInteger("float", null));
-        assertEquals(new BigInteger("2"),jo.optBigInteger("double", null));
-        assertEquals(new BigInteger("1234"),jo.optBigInteger("bigInteger", null));
-        assertEquals(new BigInteger("1234"),jo.optBigInteger("bigDecimal", null));
-        assertNull(jo.optBigDecimal("nullVal", null));
-        Util.checkJSONObjectMaps(jo);
-    }
-
-    /**
-     * Confirm behavior when JSONObject put(key, null object) is called
-     */
-    @Test
-    public void jsonObjectputNull() {
-
-        // put null should remove the item.
-        String str = "{\"myKey\": \"myval\"}";
-        JSONObject jsonObjectRemove = new JSONObject(str);
-        jsonObjectRemove.remove("myKey");
-        assertTrue("jsonObject should be empty", jsonObjectRemove.isEmpty());
-
-        JSONObject jsonObjectPutNull = new JSONObject(str);
-        jsonObjectPutNull.put("myKey", (Object) null);
-        assertTrue("jsonObject should be empty", jsonObjectPutNull.isEmpty());
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObjectRemove, jsonObjectPutNull
-        )));
-    }
-
-    /**
-     * Exercise JSONObject quote() method
-     * This purpose of quote() is to ensure that for strings with embedded
-     * quotes, the quotes are properly escaped.
-     */
-    @Test
-    public void jsonObjectQuote() {
-        String str;
-        str = "";
-        String quotedStr;
-        quotedStr = JSONObject.quote(str);
-        assertTrue("quote() expected escaped quotes, found "+quotedStr,
-                "\"\"".equals(quotedStr));
-        str = "\"\"";
-        quotedStr = JSONObject.quote(str);
-        assertTrue("quote() expected escaped quotes, found "+quotedStr,
-                "\"\\\"\\\"\"".equals(quotedStr));
-        str = " jsonObjects = new ArrayList(Arrays.asList(obj1, obj2, obj3, obj4, obj5));
+		Util.checkJSONObjectsMaps(jsonObjects);
+	}
+
+	@Test
+	public void timeNumberParsing() {
+		// test data to use
+		final String[] testData = new String[] { null, "", "100", "-100", "abc123", "012345", "100.5e199", "-100.5e199",
+				"DEADBEEF", "0xDEADBEEF", "1234567890.1234567890", "-1234567890.1234567890",
+				"adloghakuidghauiehgauioehgdkjfb nsruoh aeu noerty384 nkljfgh "
+						+ "395h tdfn kdz8yt3 4hkls gn.ey85 4hzfhnz.o8y5a84 onvklt "
+						+ "yh389thub nkz8y49lihv al4itlaithknty8hnbl"
+				// long (in length) number sequences with invalid data at the end of the
+				// string offer very poor performance for the REGEX.
+				,
+				"123467890123467890123467890123467890123467890123467890123467"
+						+ "8901234678901234678901234678901234678901234678901234678"
+						+ "9012346789012346789012346789012346789012346789012346789" + "0a" };
+		final int testDataLength = testData.length;
+		/**
+		 * Changed to 1000 for faster test runs
+		 */
+		// final int iterations = 1000000;
+		final int iterations = 1000;
+
+		// 10 million iterations 1,000,000 * 10 (currently 100,000)
+		long startTime = System.nanoTime();
+		for (int i = 0; i < iterations; i++) {
+			for (int j = 0; j < testDataLength; j++) {
+				try {
+					BigDecimal v1 = new BigDecimal(testData[j]);
+					v1.signum();
+				} catch (Exception ignore) {
+					// do nothing
+				}
+			}
+		}
+		final long elapsedNano1 = System.nanoTime() - startTime;
+		System.out.println("new BigDecimal(testData[]) : " + elapsedNano1 / 1000000 + " ms");
+
+		startTime = System.nanoTime();
+		for (int i = 0; i < iterations; i++) {
+			for (int j = 0; j < testDataLength; j++) {
+				try {
+					boolean v2 = NUMBER_PATTERN.matcher(testData[j]).matches();
+					assert v2 == !!v2;
+				} catch (Exception ignore) {
+					// do nothing
+				}
+			}
+		}
+		final long elapsedNano2 = System.nanoTime() - startTime;
+		System.out.println("NUMBER_PATTERN.matcher(testData[]).matches() : " + elapsedNano2 / 1000000 + " ms");
+		// don't assert normally as the testing is machine dependent.
+		// assertTrue("Expected Pattern matching to be faster than BigDecimal
+		// constructor",elapsedNano2) (JsonPath.read(doc, "$"))).size() == 4);
+		assertTrue("expected \"falseKey\":false", Boolean.FALSE.equals(jsonObjectByName.query("/falseKey")));
+		assertTrue("expected \"nullKey\":null", JSONObject.NULL.equals(jsonObjectByName.query("/nullKey")));
+		assertTrue("expected \"stringKey\":\"hello world!\"",
+				"hello world!".equals(jsonObjectByName.query("/stringKey")));
+		assertTrue("expected \"doubleKey\":-23.45e67",
+				new BigDecimal("-23.45e67").equals(jsonObjectByName.query("/doubleKey")));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject, jsonObjectByName)));
+	}
+
+	/**
+	 * JSONObjects can be built from a Map. In this test the map is
+	 * null. the JSONObject(JsonTokener) ctor is not tested directly since it
+	 * already has full coverage from other tests.
+	 */
+	@Test
+	public void jsonObjectByNullMap() {
+		Map map = null;
+		JSONObject jsonObject = new JSONObject(map);
+		assertTrue("jsonObject should be empty", jsonObject.isEmpty());
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * JSONObjects can be built from a Map. In this test all of the
+	 * map entries are valid JSON types.
+	 */
+	@Test
+	public void jsonObjectByMap() {
+		Map map = new HashMap();
+		map.put("trueKey", Boolean.valueOf(true));
+		map.put("falseKey", Boolean.valueOf(false));
+		map.put("stringKey", "hello world!");
+		map.put("escapeStringKey", "h\be\tllo w\u1234orld!");
+		map.put("intKey", Long.valueOf(42));
+		map.put("doubleKey", Double.valueOf(-23.45e67));
+		JSONObject jsonObject = new JSONObject(map);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 6 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 6);
+		assertTrue("expected \"trueKey\":true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
+		assertTrue("expected \"falseKey\":false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
+		assertTrue("expected \"stringKey\":\"hello world!\"", "hello world!".equals(jsonObject.query("/stringKey")));
+		assertTrue("expected \"escapeStringKey\":\"h\be\tllo w\u1234orld!\"",
+				"h\be\tllo w\u1234orld!".equals(jsonObject.query("/escapeStringKey")));
+		assertTrue("expected \"doubleKey\":-23.45e67",
+				Double.valueOf("-23.45e67").equals(jsonObject.query("/doubleKey")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Verifies that the constructor has backwards compatability with RAW types
+	 * pre-java5.
+	 */
+	@Test
+	public void verifyConstructor() {
+
+		final JSONObject expected = new JSONObject("{\"myKey\":10}");
+
+		@SuppressWarnings("rawtypes")
+		Map myRawC = Collections.singletonMap("myKey", Integer.valueOf(10));
+		JSONObject jaRaw = new JSONObject(myRawC);
+
+		Map myCStrObj = Collections.singletonMap("myKey", (Object) Integer.valueOf(10));
+		JSONObject jaStrObj = new JSONObject(myCStrObj);
+
+		Map myCStrInt = Collections.singletonMap("myKey", Integer.valueOf(10));
+		JSONObject jaStrInt = new JSONObject(myCStrInt);
+
+		Map myCObjObj = Collections.singletonMap((Object) "myKey", (Object) Integer.valueOf(10));
+		JSONObject jaObjObj = new JSONObject(myCObjObj);
+
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaRaw));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaStrObj));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaStrInt));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaObjObj));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jaRaw, jaStrObj, jaStrInt, jaObjObj)));
+	}
+
+	/**
+	 * Tests Number serialization.
+	 */
+	@Test
+	public void verifyNumberOutput() {
+		/**
+		 * MyNumberContainer is a POJO, so call JSONObject(bean), which builds a map of
+		 * getter names/values The only getter is getMyNumber (key=myNumber), whose
+		 * return value is MyNumber. MyNumber extends Number, but is not recognized as
+		 * such by wrap() per current implementation, so wrap() returns the default new
+		 * JSONObject(bean). The only getter is getNumber (key=number), whose return
+		 * value is BigDecimal(42).
+		 */
+		JSONObject jsonObject0 = new JSONObject(new MyNumberContainer());
+		String actual = jsonObject0.toString();
+		String expected = "{\"myNumber\":{\"number\":42}}";
+		assertEquals("Equal", expected, actual);
+
+		/**
+		 * JSONObject.put() handles objects differently than the bean constructor. Where
+		 * the bean ctor wraps objects before placing them in the map, put() inserts the
+		 * object without wrapping. In this case, a MyNumber instance is the value. The
+		 * MyNumber.toString() method is responsible for returning a reasonable value:
+		 * the string '42'.
+		 */
+		JSONObject jsonObject1 = new JSONObject();
+		jsonObject1.put("myNumber", new MyNumber());
+		actual = jsonObject1.toString();
+		expected = "{\"myNumber\":42}";
+		assertEquals("Equal", expected, actual);
+
+		/**
+		 * Calls the JSONObject(Map) ctor, which calls wrap() for values. AtomicInteger
+		 * is a Number, but is not recognized by wrap(), per current implementation.
+		 * However, the type is 'java.util.concurrent.atomic', so due to the 'java'
+		 * prefix, wrap() inserts the value as a string. That is why 42 comes back
+		 * wrapped in quotes.
+		 */
+		JSONObject jsonObject2 = new JSONObject(Collections.singletonMap("myNumber", new AtomicInteger(42)));
+		actual = jsonObject2.toString();
+		expected = "{\"myNumber\":\"42\"}";
+		assertEquals("Equal", expected, actual);
+
+		/**
+		 * JSONObject.put() inserts the AtomicInteger directly into the map not calling
+		 * wrap(). In toString()->write()->writeValue(), AtomicInteger is recognized as
+		 * a Number, and converted via numberToString() into the unquoted string '42'.
+		 */
+		JSONObject jsonObject3 = new JSONObject();
+		jsonObject3.put("myNumber", new AtomicInteger(42));
+		actual = jsonObject3.toString();
+		expected = "{\"myNumber\":42}";
+		assertEquals("Equal", expected, actual);
+
+		/**
+		 * Calls the JSONObject(Map) ctor, which calls wrap() for values. Fraction is a
+		 * Number, but is not recognized by wrap(), per current implementation. As a
+		 * POJO, Fraction is handled as a bean and inserted into a contained JSONObject.
+		 * It has 2 getters, for numerator and denominator.
+		 */
+		JSONObject jsonObject4 = new JSONObject(Collections.singletonMap("myNumber", new Fraction(4, 2)));
+		assertEquals(1, jsonObject4.length());
+		assertEquals(2, ((JSONObject) (jsonObject4.get("myNumber"))).length());
+		assertEquals("Numerator", BigInteger.valueOf(4), jsonObject4.query("/myNumber/numerator"));
+		assertEquals("Denominator", BigInteger.valueOf(2), jsonObject4.query("/myNumber/denominator"));
+
+		/**
+		 * JSONObject.put() inserts the Fraction directly into the map not calling
+		 * wrap(). In toString()->write()->writeValue(), Fraction is recognized as a
+		 * Number, and converted via numberToString() into the unquoted string '4/2'.
+		 * But the BigDecimal sanity check fails, so writeValue() defaults to returning
+		 * a safe JSON quoted string. Pretty slick!
+		 */
+		JSONObject jsonObject5 = new JSONObject();
+		jsonObject5.put("myNumber", new Fraction(4, 2));
+		actual = jsonObject5.toString();
+		expected = "{\"myNumber\":\"4/2\"}"; // valid JSON, bug fixed
+		assertEquals("Equal", expected, actual);
+
+		Util.checkJSONObjectsMaps(new ArrayList(
+				Arrays.asList(jsonObject0, jsonObject1, jsonObject2, jsonObject3, jsonObject4, jsonObject5)));
+	}
+
+	/**
+	 * Verifies that the put Collection has backwards compatibility with RAW types
+	 * pre-java5.
+	 */
+	@Test
+	public void verifyPutCollection() {
+
+		final JSONObject expected = new JSONObject("{\"myCollection\":[10]}");
+
+		@SuppressWarnings("rawtypes")
+		Collection myRawC = Collections.singleton(Integer.valueOf(10));
+		JSONObject jaRaw = new JSONObject();
+		jaRaw.put("myCollection", myRawC);
+
+		Collection myCObj = Collections.singleton((Object) Integer.valueOf(10));
+		JSONObject jaObj = new JSONObject();
+		jaObj.put("myCollection", myCObj);
+
+		Collection myCInt = Collections.singleton(Integer.valueOf(10));
+		JSONObject jaInt = new JSONObject();
+		jaInt.put("myCollection", myCInt);
+
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaRaw));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaObj));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaInt));
+
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jaRaw, jaObj, jaInt)));
+	}
+
+	/**
+	 * Verifies that the put Map has backwards compatibility with RAW types
+	 * pre-java5.
+	 */
+	@Test
+	public void verifyPutMap() {
+
+		final JSONObject expected = new JSONObject("{\"myMap\":{\"myKey\":10}}");
+
+		@SuppressWarnings("rawtypes")
+		Map myRawC = Collections.singletonMap("myKey", Integer.valueOf(10));
+		JSONObject jaRaw = new JSONObject();
+		jaRaw.put("myMap", myRawC);
+
+		Map myCStrObj = Collections.singletonMap("myKey", (Object) Integer.valueOf(10));
+		JSONObject jaStrObj = new JSONObject();
+		jaStrObj.put("myMap", myCStrObj);
+
+		Map myCStrInt = Collections.singletonMap("myKey", Integer.valueOf(10));
+		JSONObject jaStrInt = new JSONObject();
+		jaStrInt.put("myMap", myCStrInt);
+
+		Map myCObjObj = Collections.singletonMap((Object) "myKey", (Object) Integer.valueOf(10));
+		JSONObject jaObjObj = new JSONObject();
+		jaObjObj.put("myMap", myCObjObj);
+
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaRaw));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaStrObj));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaStrInt));
+		assertTrue("The RAW Collection should give me the same as the Typed Collection", expected.similar(jaObjObj));
+
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jaRaw, jaStrObj, jaStrInt, jaStrObj)));
+	}
+
+	/**
+	 * JSONObjects can be built from a Map. In this test the map
+	 * entries are not valid JSON types. The actual conversion is kind of
+	 * interesting.
+	 */
+	@Test
+	public void jsonObjectByMapWithUnsupportedValues() {
+		Map jsonMap = new HashMap();
+		// Just insert some random objects
+		jsonMap.put("key1", new CDL());
+		jsonMap.put("key2", new Exception());
+
+		JSONObject jsonObject = new JSONObject(jsonMap);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 2 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 2);
+		assertTrue("expected 0 key1 items", ((Map) (JsonPath.read(doc, "$.key1"))).size() == 0);
+		assertTrue("expected \"key2\":java.lang.Exception", "java.lang.Exception".equals(jsonObject.query("/key2")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * JSONObjects can be built from a Map. In this test one of the
+	 * map values is null
+	 */
+	@Test
+	public void jsonObjectByMapWithNullValue() {
+		Map map = new HashMap();
+		map.put("trueKey", Boolean.valueOf(true));
+		map.put("falseKey", Boolean.valueOf(false));
+		map.put("stringKey", "hello world!");
+		map.put("nullKey", null);
+		map.put("escapeStringKey", "h\be\tllo w\u1234orld!");
+		map.put("intKey", Long.valueOf(42));
+		map.put("doubleKey", Double.valueOf(-23.45e67));
+		JSONObject jsonObject = new JSONObject(map);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 6 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 6);
+		assertTrue("expected \"trueKey\":true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
+		assertTrue("expected \"falseKey\":false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
+		assertTrue("expected \"stringKey\":\"hello world!\"", "hello world!".equals(jsonObject.query("/stringKey")));
+		assertTrue("expected \"escapeStringKey\":\"h\be\tllo w\u1234orld!\"",
+				"h\be\tllo w\u1234orld!".equals(jsonObject.query("/escapeStringKey")));
+		assertTrue("expected \"intKey\":42", Long.valueOf("42").equals(jsonObject.query("/intKey")));
+		assertTrue("expected \"doubleKey\":-23.45e67",
+				Double.valueOf("-23.45e67").equals(jsonObject.query("/doubleKey")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	@Test
+	public void jsonObjectByMapWithNullValueAndParserConfiguration() {
+		Map map = new HashMap();
+		map.put("nullKey", null);
+
+		// by default, null values are ignored
+		JSONObject obj1 = new JSONObject(map);
+		assertTrue("expected null value to be ignored by default", obj1.isEmpty());
+
+		// if configured, null values are written as such into the JSONObject.
+		JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
+		JSONObject obj2 = new JSONObject(map, parserConfiguration);
+		assertFalse("expected null value to accepted when configured", obj2.isEmpty());
+		assertTrue(obj2.has("nullKey"));
+		assertEquals(JSONObject.NULL, obj2.get("nullKey"));
+	}
+
+	@Test
+	public void jsonObjectByMapWithNestedNullValueAndParserConfiguration() {
+		Map map = new HashMap();
+		Map nestedMap = new HashMap();
+		nestedMap.put("nullKey", null);
+		map.put("nestedMap", nestedMap);
+		List> nestedList = new ArrayList>();
+		nestedList.add(nestedMap);
+		map.put("nestedList", nestedList);
+
+		JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
+		JSONObject jsonObject = new JSONObject(map, parserConfiguration);
+
+		JSONObject nestedObject = jsonObject.getJSONObject("nestedMap");
+		assertTrue(nestedObject.has("nullKey"));
+		assertEquals(JSONObject.NULL, nestedObject.get("nullKey"));
+
+		JSONArray nestedArray = jsonObject.getJSONArray("nestedList");
+		assertEquals(1, nestedArray.length());
+		assertTrue(nestedArray.getJSONObject(0).has("nullKey"));
+		assertEquals(JSONObject.NULL, nestedArray.getJSONObject(0).get("nullKey"));
+	}
+
+	/**
+	 * JSONObject built from a bean. In this case all but one of the bean getters
+	 * return valid JSON types
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void jsonObjectByBean1() {
+		/**
+		 * Default access classes have to be mocked since JSONObject, which is not in
+		 * the same package, cannot call MyBean methods by reflection.
+		 */
+		MyBean myBean = mock(MyBean.class);
+		when(myBean.getDoubleKey()).thenReturn(-23.45e7);
+		when(myBean.getIntKey()).thenReturn(42);
+		when(myBean.getStringKey()).thenReturn("hello world!");
+		when(myBean.getEscapeStringKey()).thenReturn("h\be\tllo w\u1234orld!");
+		when(myBean.isTrueKey()).thenReturn(true);
+		when(myBean.isFalseKey()).thenReturn(false);
+		when(myBean.getStringReaderKey()).thenReturn(new StringReader("") {
+		});
+
+		JSONObject jsonObject = new JSONObject(myBean);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 8 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 8);
+		assertTrue("expected 0 items in stringReaderKey",
+				((Map) (JsonPath.read(doc, "$.stringReaderKey"))).size() == 0);
+		assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
+		assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
+		assertTrue("expected hello world!", "hello world!".equals(jsonObject.query("/stringKey")));
+		assertTrue("expected h\be\tllo w\u1234orld!",
+				"h\be\tllo w\u1234orld!".equals(jsonObject.query("/escapeStringKey")));
+		assertTrue("expected 42", Integer.valueOf("42").equals(jsonObject.query("/intKey")));
+		assertTrue("expected -23.45e7", Double.valueOf("-23.45e7").equals(jsonObject.query("/doubleKey")));
+		// sorry, mockito artifact
+		assertTrue("expected 2 mockitoInterceptor items",
+				((Map) (JsonPath.read(doc, "$.mockitoInterceptor"))).size() == 2);
+		assertTrue("expected 0 mockitoInterceptor.serializationSupport items",
+				((Map) (JsonPath.read(doc, "$.mockitoInterceptor.serializationSupport"))).size() == 0);
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * JSONObject built from a bean that has custom field names.
+	 */
+	@Test
+	public void jsonObjectByBean2() {
+		JSONObject jsonObject = new JSONObject(new MyBeanCustomName());
+		assertNotNull(jsonObject);
+		assertEquals("Wrong number of keys found:", 5, jsonObject.keySet().size());
+		assertFalse("Normal field name (someString) processing did not work", jsonObject.has("someString"));
+		assertFalse("Normal field name (myDouble) processing did not work", jsonObject.has("myDouble"));
+		assertFalse("Normal field name (someFloat) processing did not work", jsonObject.has("someFloat"));
+		assertFalse("Ignored field not found!", jsonObject.has("ignoredInt"));
+		// getSomeInt() has no user-defined annotation
+		assertTrue("Normal field name (someInt) should have been found", jsonObject.has("someInt"));
+		// the user-defined annotation does not replace any value, so someLong should be
+		// found
+		assertTrue("Normal field name (someLong) should have been found", jsonObject.has("someLong"));
+		// myStringField replaces someString property name via user-defined annotation
+		assertTrue("Overridden String field name (myStringField) should have been found",
+				jsonObject.has("myStringField"));
+		// weird name replaces myDouble property name via user-defined annotation
+		assertTrue(
+				"Overridden String field name (Some Weird NAme that Normally Wouldn't be possible!) should have been found",
+				jsonObject.has("Some Weird NAme that Normally Wouldn't be possible!"));
+		// InterfaceField replaces someFloat property name via user-defined annotation
+		assertTrue("Overridden String field name (InterfaceField) should have been found",
+				jsonObject.has("InterfaceField"));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * JSONObject built from a bean that has custom field names inherited from a
+	 * parent class.
+	 */
+	@Test
+	public void jsonObjectByBean3() {
+		JSONObject jsonObject = new JSONObject(new MyBeanCustomNameSubClass());
+		assertNotNull(jsonObject);
+		assertEquals("Wrong number of keys found:", 7, jsonObject.keySet().size());
+		assertFalse("Normal int field name (someInt) found, but was overridden", jsonObject.has("someInt"));
+		assertFalse("Normal field name (myDouble) processing did not work", jsonObject.has("myDouble"));
+		// myDouble was replaced by weird name, and then replaced again by
+		// AMoreNormalName via user-defined annotation
+		assertFalse(
+				"Overridden String field name (Some Weird NAme that Normally Wouldn't be possible!) should not be FOUND!",
+				jsonObject.has("Some Weird NAme that Normally Wouldn't be possible!"));
+		assertFalse("Normal field name (someFloat) found, but was overridden", jsonObject.has("someFloat"));
+		assertFalse("Ignored field found! but was overridden", jsonObject.has("ignoredInt"));
+		// shouldNotBeJSON property name was first ignored, then replaced by
+		// ShouldBeIgnored via user-defined annotations
+		assertFalse("Ignored field at the same level as forced name should not have been found",
+				jsonObject.has("ShouldBeIgnored"));
+		// able property name was replaced by Getable via user-defined annotation
+		assertFalse("Normally ignored field (able) with explicit property name should not have been found",
+				jsonObject.has("able"));
+		// property name someInt was replaced by newIntFieldName via user-defined
+		// annotation
+		assertTrue("Overridden int field name (newIntFieldName) should have been found",
+				jsonObject.has("newIntFieldName"));
+		// property name someLong was not replaced via user-defined annotation
+		assertTrue("Normal field name (someLong) should have been found", jsonObject.has("someLong"));
+		// property name someString was replaced by myStringField via user-defined
+		// annotation
+		assertTrue("Overridden String field name (myStringField) should have been found",
+				jsonObject.has("myStringField"));
+		// property name myDouble was replaced by a weird name, followed by
+		// AMoreNormalName via user-defined annotations
+		assertTrue("Overridden double field name (AMoreNormalName) should have been found",
+				jsonObject.has("AMoreNormalName"));
+		// property name someFloat was replaced by InterfaceField via user-defined
+		// annotation
+		assertTrue("Overridden String field name (InterfaceField) should have been found",
+				jsonObject.has("InterfaceField"));
+		// property name ignoredInt was replaced by none, followed by forcedInt via
+		// user-defined annotations
+		assertTrue("Forced field should have been found!", jsonObject.has("forcedInt"));
+		// property name able was replaced by Getable via user-defined annotation
+		assertTrue("Overridden boolean field name (Getable) should have been found", jsonObject.has("Getable"));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * A bean is also an object. But in order to test the JSONObject ctor that takes
+	 * an object and a list of names, this particular bean needs some public data
+	 * members, which have been added to the class.
+	 */
+	@Test
+	public void jsonObjectByObjectAndNames() {
+		String[] keys = { "publicString", "publicInt" };
+		// just need a class that has public data members
+		MyPublicClass myPublicClass = new MyPublicClass();
+		JSONObject jsonObject = new JSONObject(myPublicClass, keys);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 2 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 2);
+		assertTrue("expected \"publicString\":\"abc\"", "abc".equals(jsonObject.query("/publicString")));
+		assertTrue("expected \"publicInt\":42", Integer.valueOf(42).equals(jsonObject.query("/publicInt")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise the JSONObject from resource bundle functionality. The test resource
+	 * bundle is uncomplicated, but provides adequate test coverage.
+	 */
+	@Test
+	public void jsonObjectByResourceBundle() {
+		JSONObject jsonObject = new JSONObject("org.json.junit.data.StringsResourceBundle", Locale.getDefault());
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 2 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 2);
+		assertTrue("expected 2 greetings items", ((Map) (JsonPath.read(doc, "$.greetings"))).size() == 2);
+		assertTrue("expected \"hello\":\"Hello, \"", "Hello, ".equals(jsonObject.query("/greetings/hello")));
+		assertTrue("expected \"world\":\"World!\"", "World!".equals(jsonObject.query("/greetings/world")));
+		assertTrue("expected 2 farewells items", ((Map) (JsonPath.read(doc, "$.farewells"))).size() == 2);
+		assertTrue("expected \"later\":\"Later, \"", "Later, ".equals(jsonObject.query("/farewells/later")));
+		assertTrue("expected \"world\":\"World!\"", "Alligator!".equals(jsonObject.query("/farewells/gator")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise the JSONObject.accumulate() method
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void jsonObjectAccumulate() {
+
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.accumulate("myArray", true);
+		jsonObject.accumulate("myArray", false);
+		jsonObject.accumulate("myArray", "hello world!");
+		jsonObject.accumulate("myArray", "h\be\tllo w\u1234orld!");
+		jsonObject.accumulate("myArray", 42);
+		jsonObject.accumulate("myArray", -23.45e7);
+		// include an unsupported object for coverage
+		try {
+			jsonObject.accumulate("myArray", Double.NaN);
+			fail("Expected exception");
+		} catch (JSONException ignored) {
+		}
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 1 top level item", ((Map) (JsonPath.read(doc, "$"))).size() == 1);
+		assertTrue("expected 6 myArray items", ((List) (JsonPath.read(doc, "$.myArray"))).size() == 6);
+		assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/myArray/0")));
+		assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/myArray/1")));
+		assertTrue("expected hello world!", "hello world!".equals(jsonObject.query("/myArray/2")));
+		assertTrue("expected h\be\tllo w\u1234orld!", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/myArray/3")));
+		assertTrue("expected 42", Integer.valueOf(42).equals(jsonObject.query("/myArray/4")));
+		assertTrue("expected -23.45e7", Double.valueOf(-23.45e7).equals(jsonObject.query("/myArray/5")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise the JSONObject append() functionality
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void jsonObjectAppend() {
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.append("myArray", true);
+		jsonObject.append("myArray", false);
+		jsonObject.append("myArray", "hello world!");
+		jsonObject.append("myArray", "h\be\tllo w\u1234orld!");
+		jsonObject.append("myArray", 42);
+		jsonObject.append("myArray", -23.45e7);
+		// include an unsupported object for coverage
+		try {
+			jsonObject.append("myArray", Double.NaN);
+			fail("Expected exception");
+		} catch (JSONException ignored) {
+		}
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 1 top level item", ((Map) (JsonPath.read(doc, "$"))).size() == 1);
+		assertTrue("expected 6 myArray items", ((List) (JsonPath.read(doc, "$.myArray"))).size() == 6);
+		assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/myArray/0")));
+		assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/myArray/1")));
+		assertTrue("expected hello world!", "hello world!".equals(jsonObject.query("/myArray/2")));
+		assertTrue("expected h\be\tllo w\u1234orld!", "h\be\tllo w\u1234orld!".equals(jsonObject.query("/myArray/3")));
+		assertTrue("expected 42", Integer.valueOf(42).equals(jsonObject.query("/myArray/4")));
+		assertTrue("expected -23.45e7", Double.valueOf(-23.45e7).equals(jsonObject.query("/myArray/5")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise the JSONObject doubleToString() method
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void jsonObjectDoubleToString() {
+		String[] expectedStrs = { "1", "1", "-23.4", "-2.345E68", "null", "null" };
+		Double[] doubles = { 1.0, 00001.00000, -23.4, -23.45e67, Double.NaN, Double.NEGATIVE_INFINITY };
+		for (int i = 0; i < expectedStrs.length; ++i) {
+			String actualStr = JSONObject.doubleToString(doubles[i]);
+			assertTrue("value expected [" + expectedStrs[i] + "] found [" + actualStr + "]",
+					expectedStrs[i].equals(actualStr));
+		}
+	}
+
+	/**
+	 * Exercise some JSONObject get[type] and opt[type] methods
+	 */
+	@Test
+	public void jsonObjectValues() {
+		String str = "{" + "\"trueKey\":true," + "\"falseKey\":false," + "\"trueStrKey\":\"true\","
+				+ "\"falseStrKey\":\"false\"," + "\"stringKey\":\"hello world!\"," + "\"intKey\":42,"
+				+ "\"intStrKey\":\"43\"," + "\"longKey\":1234567890123456789,"
+				+ "\"longStrKey\":\"987654321098765432\"," + "\"doubleKey\":-23.45e7,"
+				+ "\"doubleStrKey\":\"00001.000\","
+				+ "\"BigDecimalStrKey\":\"19007199254740993.35481234487103587486413587843213584\","
+				+ "\"negZeroKey\":-0.0," + "\"negZeroStrKey\":\"-0.0\"," + "\"arrayKey\":[0,1,2],"
+				+ "\"objectKey\":{\"myKey\":\"myVal\"}" + "}";
+		JSONObject jsonObject = new JSONObject(str);
+		assertTrue("trueKey should be true", jsonObject.getBoolean("trueKey"));
+		assertTrue("opt trueKey should be true", jsonObject.optBoolean("trueKey"));
+		assertTrue("opt trueKey should be true", jsonObject.optBooleanObject("trueKey"));
+		assertTrue("falseKey should be false", !jsonObject.getBoolean("falseKey"));
+		assertTrue("trueStrKey should be true", jsonObject.getBoolean("trueStrKey"));
+		assertTrue("trueStrKey should be true", jsonObject.optBoolean("trueStrKey"));
+		assertTrue("trueStrKey should be true", jsonObject.optBooleanObject("trueStrKey"));
+		assertTrue("falseStrKey should be false", !jsonObject.getBoolean("falseStrKey"));
+		assertTrue("stringKey should be string", jsonObject.getString("stringKey").equals("hello world!"));
+		assertTrue("doubleKey should be double", jsonObject.getDouble("doubleKey") == -23.45e7);
+		assertTrue("doubleStrKey should be double", jsonObject.getDouble("doubleStrKey") == 1);
+		assertTrue("doubleKey can be float", jsonObject.getFloat("doubleKey") == -23.45e7f);
+		assertTrue("doubleStrKey can be float", jsonObject.getFloat("doubleStrKey") == 1f);
+		assertTrue("opt doubleKey should be double", jsonObject.optDouble("doubleKey") == -23.45e7);
+		assertTrue("opt doubleKey with Default should be double",
+				jsonObject.optDouble("doubleStrKey", Double.NaN) == 1);
+		assertTrue("opt doubleKey should be Double",
+				Double.valueOf(-23.45e7).equals(jsonObject.optDoubleObject("doubleKey")));
+		assertTrue("opt doubleKey with Default should be Double",
+				Double.valueOf(1).equals(jsonObject.optDoubleObject("doubleStrKey", Double.NaN)));
+		assertTrue("opt negZeroKey should be a Double", jsonObject.opt("negZeroKey") instanceof Double);
+		assertTrue("get negZeroKey should be a Double", jsonObject.get("negZeroKey") instanceof Double);
+		assertTrue("optNumber negZeroKey should return Double", jsonObject.optNumber("negZeroKey") instanceof Double);
+		assertTrue("optNumber negZeroStrKey should return Double",
+				jsonObject.optNumber("negZeroStrKey") instanceof Double);
+		assertTrue("opt negZeroKey should be double", Double.compare(jsonObject.optDouble("negZeroKey"), -0.0d) == 0);
+		assertTrue("opt negZeroStrKey with Default should be double",
+				Double.compare(jsonObject.optDouble("negZeroStrKey"), -0.0d) == 0);
+		assertTrue("opt negZeroKey should be Double",
+				Double.valueOf(-0.0d).equals(jsonObject.optDoubleObject("negZeroKey")));
+		assertTrue("opt negZeroStrKey with Default should be Double",
+				Double.valueOf(-0.0d).equals(jsonObject.optDoubleObject("negZeroStrKey")));
+		assertTrue("optNumber negZeroKey should be -0.0",
+				Double.compare(jsonObject.optNumber("negZeroKey").doubleValue(), -0.0d) == 0);
+		assertTrue("optNumber negZeroStrKey should be -0.0",
+				Double.compare(jsonObject.optNumber("negZeroStrKey").doubleValue(), -0.0d) == 0);
+		assertTrue("optFloat doubleKey should be float", jsonObject.optFloat("doubleKey") == -23.45e7f);
+		assertTrue("optFloat doubleKey with Default should be float",
+				jsonObject.optFloat("doubleStrKey", Float.NaN) == 1f);
+		assertTrue("optFloat doubleKey should be Float",
+				Float.valueOf(-23.45e7f).equals(jsonObject.optFloatObject("doubleKey")));
+		assertTrue("optFloat doubleKey with Default should be Float",
+				Float.valueOf(1f).equals(jsonObject.optFloatObject("doubleStrKey", Float.NaN)));
+		assertTrue("intKey should be int", jsonObject.optInt("intKey") == 42);
+		assertTrue("opt intKey should be int", jsonObject.optInt("intKey", 0) == 42);
+		assertTrue("intKey should be Integer", Integer.valueOf(42).equals(jsonObject.optIntegerObject("intKey")));
+		assertTrue("opt intKey should be Integer",
+				Integer.valueOf(42).equals(jsonObject.optIntegerObject("intKey", 0)));
+		assertTrue("opt intKey with default should be int", jsonObject.getInt("intKey") == 42);
+		assertTrue("intStrKey should be int", jsonObject.getInt("intStrKey") == 43);
+		assertTrue("longKey should be long", jsonObject.getLong("longKey") == 1234567890123456789L);
+		assertTrue("opt longKey should be long", jsonObject.optLong("longKey") == 1234567890123456789L);
+		assertTrue("opt longKey with default should be long", jsonObject.optLong("longKey", 0) == 1234567890123456789L);
+		assertTrue("opt longKey should be Long",
+				Long.valueOf(1234567890123456789L).equals(jsonObject.optLongObject("longKey")));
+		assertTrue("opt longKey with default should be Long",
+				Long.valueOf(1234567890123456789L).equals(jsonObject.optLongObject("longKey", 0L)));
+		assertTrue("longStrKey should be long", jsonObject.getLong("longStrKey") == 987654321098765432L);
+		assertTrue("optNumber int should return Integer", jsonObject.optNumber("intKey") instanceof Integer);
+		assertTrue("optNumber long should return Long", jsonObject.optNumber("longKey") instanceof Long);
+		assertTrue("optNumber double should return BigDecimal",
+				jsonObject.optNumber("doubleKey") instanceof BigDecimal);
+		assertTrue("optNumber Str int should return Integer", jsonObject.optNumber("intStrKey") instanceof Integer);
+		assertTrue("optNumber Str long should return Long", jsonObject.optNumber("longStrKey") instanceof Long);
+		assertTrue("optNumber Str double should return BigDecimal",
+				jsonObject.optNumber("doubleStrKey") instanceof BigDecimal);
+		assertTrue("optNumber BigDecimalStrKey should return BigDecimal",
+				jsonObject.optNumber("BigDecimalStrKey") instanceof BigDecimal);
+		assertTrue("xKey should not exist", jsonObject.isNull("xKey"));
+		assertTrue("stringKey should exist", jsonObject.has("stringKey"));
+		assertTrue("opt stringKey should string", jsonObject.optString("stringKey").equals("hello world!"));
+		assertTrue("opt stringKey with default should string",
+				jsonObject.optString("stringKey", "not found").equals("hello world!"));
+		JSONArray jsonArray = jsonObject.getJSONArray("arrayKey");
+		assertTrue("arrayKey should be JSONArray",
+				jsonArray.getInt(0) == 0 && jsonArray.getInt(1) == 1 && jsonArray.getInt(2) == 2);
+		jsonArray = jsonObject.optJSONArray("arrayKey");
+		assertTrue("opt arrayKey should be JSONArray",
+				jsonArray.getInt(0) == 0 && jsonArray.getInt(1) == 1 && jsonArray.getInt(2) == 2);
+		JSONObject jsonObjectInner = jsonObject.getJSONObject("objectKey");
+		assertTrue("objectKey should be JSONObject", jsonObjectInner.get("myKey").equals("myVal"));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Check whether JSONObject handles large or high precision numbers correctly
+	 */
+	@Test
+	public void stringToValueNumbersTest() {
+		assertTrue("-0 Should be a Double!", JSONObject.stringToValue("-0") instanceof Double);
+		assertTrue("-0.0 Should be a Double!", JSONObject.stringToValue("-0.0") instanceof Double);
+		assertTrue("'-' Should be a String!", JSONObject.stringToValue("-") instanceof String);
+		assertTrue("0.2 should be a BigDecimal!", JSONObject.stringToValue("0.2") instanceof BigDecimal);
+		assertTrue("Doubles should be BigDecimal, even when incorrectly converting floats!",
+				JSONObject.stringToValue(Double.valueOf("0.2f").toString()) instanceof BigDecimal);
+		/**
+		 * This test documents a need for BigDecimal conversion.
+		 */
+		Object obj = JSONObject.stringToValue("299792.457999999984");
+		assertTrue("does not evaluate to 299792.457999999984 BigDecimal!",
+				obj.equals(new BigDecimal("299792.457999999984")));
+		assertTrue("1 should be an Integer!", JSONObject.stringToValue("1") instanceof Integer);
+		assertTrue("Integer.MAX_VALUE should still be an Integer!",
+				JSONObject.stringToValue(Integer.valueOf(Integer.MAX_VALUE).toString()) instanceof Integer);
+		assertTrue("Large integers should be a Long!",
+				JSONObject.stringToValue(Long.valueOf(((long) Integer.MAX_VALUE) + 1).toString()) instanceof Long);
+		assertTrue("Long.MAX_VALUE should still be an Integer!",
+				JSONObject.stringToValue(Long.valueOf(Long.MAX_VALUE).toString()) instanceof Long);
+
+		String str = new BigInteger(Long.valueOf(Long.MAX_VALUE).toString()).add(BigInteger.ONE).toString();
+		assertTrue("Really large integers currently evaluate to BigInteger",
+				JSONObject.stringToValue(str).equals(new BigInteger("9223372036854775808")));
+	}
+
+	/**
+	 * This test documents numeric values which could be numerically handled as
+	 * BigDecimal or BigInteger. It helps determine what outputs will change if
+	 * those types are supported.
+	 */
+	@Test
+	public void jsonValidNumberValuesNeitherLongNorIEEE754Compatible() {
+		// Valid JSON Numbers, probably should return BigDecimal or BigInteger objects
+		String str = "{" + "\"numberWithDecimals\":299792.457999999984," + "\"largeNumber\":12345678901234567890,"
+				+ "\"preciseNumber\":0.2000000000000000111," + "\"largeExponent\":-23.45e2327" + "}";
+		JSONObject jsonObject = new JSONObject(str);
+		// Comes back as a double, but loses precision
+		assertTrue("numberWithDecimals currently evaluates to double 299792.458",
+				jsonObject.get("numberWithDecimals").equals(new BigDecimal("299792.457999999984")));
+		Object obj = jsonObject.get("largeNumber");
+		assertTrue("largeNumber currently evaluates to BigInteger", new BigInteger("12345678901234567890").equals(obj));
+		// comes back as a double but loses precision
+		assertEquals("preciseNumber currently evaluates to double 0.2", 0.2, jsonObject.getDouble("preciseNumber"),
+				0.0);
+		obj = jsonObject.get("largeExponent");
+		assertTrue("largeExponent should evaluate as a BigDecimal", new BigDecimal("-23.45e2327").equals(obj));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * This test documents how JSON-Java handles invalid numeric input.
+	 */
+	@Test
+	public void jsonInvalidNumberValues() {
+		// Number-notations supported by Java and invalid as JSON
+		String str = "{" + "\"hexNumber\":-0x123," + "\"tooManyZeros\":00," + "\"negativeInfinite\":-Infinity,"
+				+ "\"negativeNaN\":-NaN," + "\"negativeFraction\":-.01," + "\"tooManyZerosFraction\":00.001,"
+				+ "\"negativeHexFloat\":-0x1.fffp1," + "\"hexFloat\":0x1.0P-1074," + "\"floatIdentifier\":0.1f,"
+				+ "\"doubleIdentifier\":0.1d" + "}";
+
+		// Test should fail if default strictMode is true, pass if false
+		JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+		if (jsonParserConfiguration.isStrictMode()) {
+			try {
+				JSONObject jsonObject = new JSONObject(str);
+				assertEquals("Expected to throw exception due to invalid string", true, false);
+			} catch (JSONException e) {
+			}
+		} else {
+			JSONObject jsonObject = new JSONObject(str);
+			Object obj;
+			obj = jsonObject.get("hexNumber");
+			assertFalse("hexNumber must not be a number (should throw exception!?)", obj instanceof Number);
+			assertTrue("hexNumber currently evaluates to string", obj.equals("-0x123"));
+			assertTrue("tooManyZeros currently evaluates to string", jsonObject.get("tooManyZeros").equals("00"));
+			obj = jsonObject.get("negativeInfinite");
+			assertTrue("negativeInfinite currently evaluates to string", obj.equals("-Infinity"));
+			obj = jsonObject.get("negativeNaN");
+			assertTrue("negativeNaN currently evaluates to string", obj.equals("-NaN"));
+			assertTrue("negativeFraction currently evaluates to double -0.01",
+					jsonObject.get("negativeFraction").equals(BigDecimal.valueOf(-0.01)));
+			assertTrue("tooManyZerosFraction currently evaluates to double 0.001",
+					jsonObject.optLong("tooManyZerosFraction") == 0);
+			assertTrue("negativeHexFloat currently evaluates to double -3.99951171875",
+					jsonObject.get("negativeHexFloat").equals(Double.valueOf(-3.99951171875)));
+			assertTrue("hexFloat currently evaluates to double 4.9E-324",
+					jsonObject.get("hexFloat").equals(Double.valueOf(4.9E-324)));
+			assertTrue("floatIdentifier currently evaluates to double 0.1",
+					jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1)));
+			assertTrue("doubleIdentifier currently evaluates to double 0.1",
+					jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1)));
+			Util.checkJSONObjectMaps(jsonObject);
+		}
+	}
+
+	/**
+	 * Tests how JSONObject get[type] handles incorrect types
+	 */
+	@Test
+	public void jsonObjectNonAndWrongValues() {
+		String str = "{" + "\"trueKey\":true," + "\"falseKey\":false," + "\"trueStrKey\":\"true\","
+				+ "\"falseStrKey\":\"false\"," + "\"stringKey\":\"hello world!\"," + "\"intKey\":42,"
+				+ "\"intStrKey\":\"43\"," + "\"longKey\":1234567890123456789,"
+				+ "\"longStrKey\":\"987654321098765432\"," + "\"doubleKey\":-23.45e7,"
+				+ "\"doubleStrKey\":\"00001.000\"," + "\"arrayKey\":[0,1,2]," + "\"objectKey\":{\"myKey\":\"myVal\"}"
+				+ "}";
+		JSONObject jsonObject = new JSONObject(str);
+		try {
+			jsonObject.getBoolean("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getBoolean("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a Boolean (class java.lang.String : hello world!).",
+					e.getMessage());
+		}
+		try {
+			jsonObject.getString("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getString("trueKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"trueKey\"] is not a string (class java.lang.Boolean : true).", e.getMessage());
+		}
+		try {
+			jsonObject.getDouble("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getDouble("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a double (class java.lang.String : hello world!).",
+					e.getMessage());
+		}
+		try {
+			jsonObject.getFloat("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getFloat("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a float (class java.lang.String : hello world!).",
+					e.getMessage());
+		}
+		try {
+			jsonObject.getInt("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getInt("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a int (class java.lang.String : hello world!).", e.getMessage());
+		}
+		try {
+			jsonObject.getLong("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getLong("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a long (class java.lang.String : hello world!).", e.getMessage());
+		}
+		try {
+			jsonObject.getJSONArray("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getJSONArray("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a JSONArray (class java.lang.String : hello world!).",
+					e.getMessage());
+		}
+		try {
+			jsonObject.getJSONObject("nonKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"nonKey\"] not found.", e.getMessage());
+		}
+		try {
+			jsonObject.getJSONObject("stringKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"JSONObject[\"stringKey\"] is not a JSONObject (class java.lang.String : hello world!).",
+					e.getMessage());
+		}
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * This test documents an unexpected numeric behavior. A double that ends with
+	 * .0 is parsed, serialized, then parsed again. On the second parse, it has
+	 * become an int.
+	 */
+	@Test
+	public void unexpectedDoubleToIntConversion() {
+		String key30 = "key30";
+		String key31 = "key31";
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.put(key30, Double.valueOf(3.0));
+		jsonObject.put(key31, Double.valueOf(3.1));
+
+		assertTrue("3.0 should remain a double", jsonObject.getDouble(key30) == 3);
+		assertTrue("3.1 should remain a double", jsonObject.getDouble(key31) == 3.1);
+
+		// turns 3.0 into 3.
+		String serializedString = jsonObject.toString();
+		JSONObject deserialized = new JSONObject(serializedString);
+		assertTrue("3.0 is now an int", deserialized.get(key30) instanceof Integer);
+		assertTrue("3.0 can still be interpreted as a double", deserialized.getDouble(key30) == 3.0);
+		assertTrue("3.1 remains a double", deserialized.getDouble(key31) == 3.1);
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Document behaviors of big numbers. Includes both JSONObject and JSONArray
+	 * tests
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void bigNumberOperations() {
+		/**
+		 * JSONObject tries to parse BigInteger as a bean, but it only has one getter,
+		 * getLowestBitSet(). The value is lost and an unhelpful value is stored. This
+		 * should be fixed.
+		 */
+		BigInteger bigInteger = new BigInteger("123456789012345678901234567890");
+		JSONObject jsonObject0 = new JSONObject(bigInteger);
+		Object obj = jsonObject0.get("lowestSetBit");
+		assertTrue("JSONObject only has 1 value", jsonObject0.length() == 1);
+		assertTrue("JSONObject parses BigInteger as the Integer lowestBitSet", obj instanceof Integer);
+		assertTrue("this bigInteger lowestBitSet happens to be 1", obj.equals(1));
+
+		/**
+		 * JSONObject tries to parse BigDecimal as a bean, but it has no getters, The
+		 * value is lost and no value is stored. This should be fixed.
+		 */
+		BigDecimal bigDecimal = new BigDecimal("123456789012345678901234567890.12345678901234567890123456789");
+		JSONObject jsonObject1 = new JSONObject(bigDecimal);
+		assertTrue("large bigDecimal is not stored", jsonObject1.isEmpty());
+
+		/**
+		 * JSONObject put(String, Object) method stores and serializes bigInt and bigDec
+		 * correctly. Nothing needs to change.
+		 */
+		JSONObject jsonObject2 = new JSONObject();
+		jsonObject2.put("bigInt", bigInteger);
+		assertTrue("jsonObject.put() handles bigInt correctly", jsonObject2.get("bigInt").equals(bigInteger));
+		assertTrue("jsonObject.getBigInteger() handles bigInt correctly",
+				jsonObject2.getBigInteger("bigInt").equals(bigInteger));
+		assertTrue("jsonObject.optBigInteger() handles bigInt correctly",
+				jsonObject2.optBigInteger("bigInt", BigInteger.ONE).equals(bigInteger));
+		assertTrue("jsonObject serializes bigInt correctly",
+				jsonObject2.toString().equals("{\"bigInt\":123456789012345678901234567890}"));
+		assertTrue("BigInteger as BigDecimal", jsonObject2.getBigDecimal("bigInt").equals(new BigDecimal(bigInteger)));
+
+		JSONObject jsonObject3 = new JSONObject();
+		jsonObject3.put("bigDec", bigDecimal);
+		assertTrue("jsonObject.put() handles bigDec correctly", jsonObject3.get("bigDec").equals(bigDecimal));
+		assertTrue("jsonObject.getBigDecimal() handles bigDec correctly",
+				jsonObject3.getBigDecimal("bigDec").equals(bigDecimal));
+		assertTrue("jsonObject.optBigDecimal() handles bigDec correctly",
+				jsonObject3.optBigDecimal("bigDec", BigDecimal.ONE).equals(bigDecimal));
+		assertTrue("jsonObject serializes bigDec correctly", jsonObject3.toString()
+				.equals("{\"bigDec\":123456789012345678901234567890.12345678901234567890123456789}"));
+
+		assertTrue("BigDecimal as BigInteger", jsonObject3.getBigInteger("bigDec").equals(bigDecimal.toBigInteger()));
+		/**
+		 * exercise some exceptions
+		 */
+		try {
+			// bigInt key does not exist
+			jsonObject3.getBigDecimal("bigInt");
+			fail("expected an exeption");
+		} catch (JSONException ignored) {
+		}
+		obj = jsonObject3.optBigDecimal("bigInt", BigDecimal.ONE);
+		assertTrue("expected BigDecimal", obj.equals(BigDecimal.ONE));
+		jsonObject3.put("stringKey", "abc");
+		try {
+			jsonObject3.getBigDecimal("stringKey");
+			fail("expected an exeption");
+		} catch (JSONException ignored) {
+		}
+		obj = jsonObject3.optBigInteger("bigDec", BigInteger.ONE);
+		assertTrue("expected BigInteger", obj instanceof BigInteger);
+		assertEquals(bigDecimal.toBigInteger(), obj);
+
+		/**
+		 * JSONObject.numberToString() works correctly, nothing to change.
+		 */
+		String str = JSONObject.numberToString(bigInteger);
+		assertTrue("numberToString() handles bigInteger correctly", str.equals("123456789012345678901234567890"));
+		str = JSONObject.numberToString(bigDecimal);
+		assertTrue("numberToString() handles bigDecimal correctly",
+				str.equals("123456789012345678901234567890.12345678901234567890123456789"));
+
+		/**
+		 * JSONObject.stringToValue() turns bigInt into an accurate string, and rounds
+		 * bigDec. This incorrect, but users may have come to expect this behavior.
+		 * Change would be marginally better, but might inconvenience users.
+		 */
+		obj = JSONObject.stringToValue(bigInteger.toString());
+		assertTrue("stringToValue() turns bigInteger string into Number", obj instanceof Number);
+		obj = JSONObject.stringToValue(bigDecimal.toString());
+		assertTrue("stringToValue() changes bigDecimal Number", obj instanceof Number);
+
+		/**
+		 * wrap() vs put() big number behavior is now the same.
+		 */
+		// bigInt map ctor
+		Map map = new HashMap();
+		map.put("bigInt", bigInteger);
+		JSONObject jsonObject4 = new JSONObject(map);
+		String actualFromMapStr = jsonObject4.toString();
+		assertTrue("bigInt in map (or array or bean) is a string",
+				actualFromMapStr.equals("{\"bigInt\":123456789012345678901234567890}"));
+		// bigInt put
+		JSONObject jsonObject5 = new JSONObject();
+		jsonObject5.put("bigInt", bigInteger);
+		String actualFromPutStr = jsonObject5.toString();
+		assertTrue("bigInt from put is a number",
+				actualFromPutStr.equals("{\"bigInt\":123456789012345678901234567890}"));
+		// bigDec map ctor
+		map = new HashMap();
+		map.put("bigDec", bigDecimal);
+		JSONObject jsonObject6 = new JSONObject(map);
+		actualFromMapStr = jsonObject6.toString();
+		assertTrue("bigDec in map (or array or bean) is a bigDec",
+				actualFromMapStr.equals("{\"bigDec\":123456789012345678901234567890.12345678901234567890123456789}"));
+		// bigDec put
+		JSONObject jsonObject7 = new JSONObject();
+		jsonObject7.put("bigDec", bigDecimal);
+		actualFromPutStr = jsonObject7.toString();
+		assertTrue("bigDec from put is a number",
+				actualFromPutStr.equals("{\"bigDec\":123456789012345678901234567890.12345678901234567890123456789}"));
+		// bigInt,bigDec put
+		JSONArray jsonArray0 = new JSONArray();
+		jsonArray0.put(bigInteger);
+		jsonArray0.put(bigDecimal);
+		actualFromPutStr = jsonArray0.toString();
+		assertTrue("bigInt, bigDec from put is a number", actualFromPutStr.equals(
+				"[123456789012345678901234567890,123456789012345678901234567890.12345678901234567890123456789]"));
+		assertTrue("getBigInt is bigInt", jsonArray0.getBigInteger(0).equals(bigInteger));
+		assertTrue("getBigDec is bigDec", jsonArray0.getBigDecimal(1).equals(bigDecimal));
+		assertTrue("optBigInt is bigInt", jsonArray0.optBigInteger(0, BigInteger.ONE).equals(bigInteger));
+		assertTrue("optBigDec is bigDec", jsonArray0.optBigDecimal(1, BigDecimal.ONE).equals(bigDecimal));
+		jsonArray0.put(Boolean.TRUE);
+		try {
+			jsonArray0.getBigInteger(2);
+			fail("should not be able to get big int");
+		} catch (Exception ignored) {
+		}
+		try {
+			jsonArray0.getBigDecimal(2);
+			fail("should not be able to get big dec");
+		} catch (Exception ignored) {
+		}
+		assertTrue("optBigInt is default", jsonArray0.optBigInteger(2, BigInteger.ONE).equals(BigInteger.ONE));
+		assertTrue("optBigDec is default", jsonArray0.optBigDecimal(2, BigDecimal.ONE).equals(BigDecimal.ONE));
+
+		// bigInt,bigDec list ctor
+		List list = new ArrayList();
+		list.add(bigInteger);
+		list.add(bigDecimal);
+		JSONArray jsonArray1 = new JSONArray(list);
+		String actualFromListStr = jsonArray1.toString();
+		assertTrue("bigInt, bigDec in list is a bigInt, bigDec", actualFromListStr.equals(
+				"[123456789012345678901234567890,123456789012345678901234567890.12345678901234567890123456789]"));
+		// bigInt bean ctor
+		MyBigNumberBean myBigNumberBean = mock(MyBigNumberBean.class);
+		when(myBigNumberBean.getBigInteger()).thenReturn(new BigInteger("123456789012345678901234567890"));
+		JSONObject jsonObject8 = new JSONObject(myBigNumberBean);
+		String actualFromBeanStr = jsonObject8.toString();
+		// can't do a full string compare because mockery adds an extra key/value
+		assertTrue("bigInt from bean ctor is a bigInt", actualFromBeanStr.contains("123456789012345678901234567890"));
+		// bigDec bean ctor
+		myBigNumberBean = mock(MyBigNumberBean.class);
+		when(myBigNumberBean.getBigDecimal())
+				.thenReturn(new BigDecimal("123456789012345678901234567890.12345678901234567890123456789"));
+		jsonObject8 = new JSONObject(myBigNumberBean);
+		actualFromBeanStr = jsonObject8.toString();
+		// can't do a full string compare because mockery adds an extra key/value
+		assertTrue("bigDec from bean ctor is a bigDec",
+				actualFromBeanStr.contains("123456789012345678901234567890.12345678901234567890123456789"));
+		// bigInt,bigDec wrap()
+		obj = JSONObject.wrap(bigInteger);
+		assertTrue("wrap() returns big num", obj.equals(bigInteger));
+		obj = JSONObject.wrap(bigDecimal);
+		assertTrue("wrap() returns string", obj.equals(bigDecimal));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject0, jsonObject1, jsonObject2,
+				jsonObject3, jsonObject4, jsonObject5, jsonObject6, jsonObject7, jsonObject8)));
+		Util.checkJSONArrayMaps(jsonArray0, jsonObject0.getMapType());
+		Util.checkJSONArrayMaps(jsonArray1, jsonObject0.getMapType());
+	}
+
+	/**
+	 * Verifies that getBigInteger / optBigInteger do not attempt to materialise a
+	 * BigInteger whose decimal representation would exceed
+	 * ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH digits. A short exponent-notation
+	 * literal such as 1e100000000 is stored compactly as a BigDecimal at parse time
+	 * but would otherwise expand to ~100 000 000 digits in BigDecimal.toBigInteger(),
+	 * stalling the thread / OOM (CVE-2026-59171, issue #1063).
+	 */
+	@Test(timeout = 5000)
+	public void getBigIntegerHugeExponentReturnsDefault() {
+		// BigDecimal path: value arrives via the parser as a BigDecimal
+		JSONObject jo = new JSONObject("{\"x\":1e100000000}");
+		assertTrue("huge-exponent literal parses to BigDecimal", jo.get("x") instanceof BigDecimal);
+		assertNull("optBigInteger returns default for huge exponent", jo.optBigInteger("x", null));
+		try {
+			jo.getBigInteger("x");
+			fail("getBigInteger should throw for huge exponent");
+		} catch (JSONException expected) {
+			// expected: integer part exceeds DEFAULT_MAX_NUMBER_LENGTH digits
+		}
+
+		// String path: value put() as a String, exercised via objectToBigInteger's
+		// isDecimalNotation branch
+		JSONObject jo2 = new JSONObject();
+		jo2.put("x", "1e100000000");
+		assertNull("optBigInteger returns default for huge-exponent string", jo2.optBigInteger("x", null));
+
+		// JSONArray accessors delegate to the same helper
+		JSONArray ja = new JSONArray("[1e100000000]");
+		assertTrue("optBigInteger returns default for huge exponent (array)",
+				BigInteger.ONE.equals(ja.optBigInteger(0, BigInteger.ONE)));
+
+		// Boundary: a value at the limit still converts correctly
+		JSONObject jo3 = new JSONObject("{\"x\":1e999}");
+		assertEquals("1e999 still converts", 0,
+				jo3.getBigInteger("x").compareTo(BigInteger.TEN.pow(999)));
+	}
+
+	/**
+	 * Verifies that the JSONParserConfiguration.maxNumberLength setting is honoured
+	 * by the getBigInteger / optBigInteger overloads on JSONObject and JSONArray.
+	 */
+	@Test(timeout = 5000)
+	public void getBigIntegerHonorsMaxNumberLengthConfig() {
+		JSONObject jo = new JSONObject("{\"a\":1e1500,\"b\":1e2500}");
+
+		// Default config: DEFAULT_MAX_NUMBER_LENGTH == 1000, both rejected
+		assertNull("1e1500 rejected under default", jo.optBigInteger("a", null));
+		assertNull("1e2500 rejected under default", jo.optBigInteger("b", null));
+
+		// Custom raised limit
+		JSONParserConfiguration cfg2000 = new JSONParserConfiguration().withMaxNumberLength(2000);
+		assertEquals("1e1500 accepted under maxNumberLength=2000", 0,
+				jo.getBigInteger("a", cfg2000).compareTo(BigInteger.TEN.pow(1500)));
+		assertNull("1e2500 rejected under maxNumberLength=2000",
+				jo.optBigInteger("b", null, cfg2000));
+		try {
+			jo.getBigInteger("b", cfg2000);
+			fail("getBigInteger should throw for 1e2500 under maxNumberLength=2000");
+		} catch (JSONException expected) {
+			// expected: integer part exceeds configured maxNumberLength
+		}
+
+		// Custom lowered limit
+		JSONParserConfiguration cfg5 = new JSONParserConfiguration().withMaxNumberLength(5);
+		assertNull("1e1500 rejected under maxNumberLength=5",
+				jo.optBigInteger("a", null, cfg5));
+		JSONObject small = new JSONObject("{\"x\":1234}");
+		assertEquals("small value accepted under maxNumberLength=5",
+				BigInteger.valueOf(1234), small.getBigInteger("x", cfg5));
+
+		// Disabled: -1 turns the guard off. Use a moderate exponent so the test
+		// completes in a few ms while still exceeding the default limit.
+		JSONParserConfiguration cfgOff = new JSONParserConfiguration()
+				.withMaxNumberLength(ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH);
+		assertEquals("1e2500 accepted when maxNumberLength is disabled", 0,
+				jo.getBigInteger("b", cfgOff).compareTo(BigInteger.TEN.pow(2500)));
+
+		// null config falls back to default
+		assertNull("null config behaves like default", jo.optBigInteger("a", null, null));
+
+		// JSONArray overloads follow the same rules
+		JSONArray ja = new JSONArray("[1e1500]");
+		assertNull("array: 1e1500 rejected under default", ja.optBigInteger(0, null));
+		assertEquals("array: 1e1500 accepted under maxNumberLength=2000", 0,
+				ja.getBigInteger(0, cfg2000).compareTo(BigInteger.TEN.pow(1500)));
+		assertEquals("array: 1e1500 accepted when disabled", 0,
+				ja.optBigInteger(0, null, cfgOff).compareTo(BigInteger.TEN.pow(1500)));
+	}
+
+	/**
+	 * The purpose for the static method getNames() methods are not clear. This
+	 * method is not called from within JSON-Java. Most likely uses are to prep
+	 * names arrays for: JSONObject(JSONObject jo, String[] names) JSONObject(Object
+	 * object, String names[]),
+	 */
+	@Test
+	public void jsonObjectNames() {
+
+		// getNames() from null JSONObject
+		assertTrue("null names from null Object", null == JSONObject.getNames((Object) null));
+
+		// getNames() from object with no fields
+		assertTrue("null names from Object with no fields", null == JSONObject.getNames(new MyJsonString()));
+
+		// getNames from new JSONOjbect
+		JSONObject jsonObject0 = new JSONObject();
+		String[] names = JSONObject.getNames(jsonObject0);
+		assertTrue("names should be null", names == null);
+
+		// getNames() from empty JSONObject
+		String emptyStr = "{}";
+		JSONObject jsonObject1 = new JSONObject(emptyStr);
+		assertTrue("empty JSONObject should have null names", null == JSONObject.getNames(jsonObject1));
+
+		// getNames() from JSONObject
+		String str = "{" + "\"trueKey\":true," + "\"falseKey\":false," + "\"stringKey\":\"hello world!\"" + "}";
+		JSONObject jsonObject2 = new JSONObject(str);
+		names = JSONObject.getNames(jsonObject2);
+		JSONArray jsonArray0 = new JSONArray(names);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray0.toString());
+		List docList = JsonPath.read(doc, "$");
+		assertTrue("expected 3 items", docList.size() == 3);
+		assertTrue("expected to find trueKey", ((List) JsonPath.read(doc, "$[?(@=='trueKey')]")).size() == 1);
+		assertTrue("expected to find falseKey", ((List) JsonPath.read(doc, "$[?(@=='falseKey')]")).size() == 1);
+		assertTrue("expected to find stringKey", ((List) JsonPath.read(doc, "$[?(@=='stringKey')]")).size() == 1);
+
+		/**
+		 * getNames() from an enum with properties has an interesting result. It returns
+		 * the enum values, not the selected enum properties
+		 */
+		MyEnumField myEnumField = MyEnumField.VAL1;
+		names = JSONObject.getNames(myEnumField);
+
+		// validate JSON
+		JSONArray jsonArray1 = new JSONArray(names);
+		doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray1.toString());
+		docList = JsonPath.read(doc, "$");
+		assertTrue("expected 3 items", docList.size() == 3);
+		assertTrue("expected to find VAL1", ((List) JsonPath.read(doc, "$[?(@=='VAL1')]")).size() == 1);
+		assertTrue("expected to find VAL2", ((List) JsonPath.read(doc, "$[?(@=='VAL2')]")).size() == 1);
+		assertTrue("expected to find VAL3", ((List) JsonPath.read(doc, "$[?(@=='VAL3')]")).size() == 1);
+
+		/**
+		 * A bean is also an object. But in order to test the static method getNames(),
+		 * this particular bean needs some public data members.
+		 */
+		MyPublicClass myPublicClass = new MyPublicClass();
+		names = JSONObject.getNames(myPublicClass);
+
+		// validate JSON
+		JSONArray jsonArray2 = new JSONArray(names);
+		doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray2.toString());
+		docList = JsonPath.read(doc, "$");
+		assertTrue("expected 2 items", docList.size() == 2);
+		assertTrue("expected to find publicString",
+				((List) JsonPath.read(doc, "$[?(@=='publicString')]")).size() == 1);
+		assertTrue("expected to find publicInt", ((List) JsonPath.read(doc, "$[?(@=='publicInt')]")).size() == 1);
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject0, jsonObject1, jsonObject2)));
+		Util.checkJSONArrayMaps(jsonArray0, jsonObject0.getMapType());
+		Util.checkJSONArrayMaps(jsonArray1, jsonObject0.getMapType());
+		Util.checkJSONArrayMaps(jsonArray2, jsonObject0.getMapType());
+	}
+
+	/**
+	 * Populate a JSONArray from an empty JSONObject names() method. It should be
+	 * empty.
+	 */
+	@Test
+	public void emptyJsonObjectNamesToJsonAray() {
+		JSONObject jsonObject = new JSONObject();
+		JSONArray jsonArray = jsonObject.names();
+		assertTrue("jsonArray should be null", jsonArray == null);
+		Util.checkJSONObjectMaps(jsonObject);
+		Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
+	}
+
+	/**
+	 * Populate a JSONArray from a JSONObject names() method. Confirm that it
+	 * contains the expected names.
+	 */
+	@Test
+	public void jsonObjectNamesToJsonAray() {
+		String str = "{" + "\"trueKey\":true," + "\"falseKey\":false," + "\"stringKey\":\"hello world!\"" + "}";
+
+		JSONObject jsonObject = new JSONObject(str);
+		JSONArray jsonArray = jsonObject.names();
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray.toString());
+		assertTrue("expected 3 top level items", ((List) (JsonPath.read(doc, "$"))).size() == 3);
+		assertTrue("expected to find trueKey", ((List) JsonPath.read(doc, "$[?(@=='trueKey')]")).size() == 1);
+		assertTrue("expected to find falseKey", ((List) JsonPath.read(doc, "$[?(@=='falseKey')]")).size() == 1);
+		assertTrue("expected to find stringKey", ((List) JsonPath.read(doc, "$[?(@=='stringKey')]")).size() == 1);
+		Util.checkJSONObjectMaps(jsonObject);
+		Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
+	}
+
+	/**
+	 * Exercise the JSONObject increment() method.
+	 */
+	@SuppressWarnings("cast")
+	@Test
+	public void jsonObjectIncrement() {
+		String str = "{" + "\"keyLong\":9999999991," + "\"keyDouble\":1.1" + "}";
+		JSONObject jsonObject = new JSONObject(str);
+		jsonObject.increment("keyInt");
+		jsonObject.increment("keyInt");
+		jsonObject.increment("keyLong");
+		jsonObject.increment("keyDouble");
+		jsonObject.increment("keyInt");
+		jsonObject.increment("keyLong");
+		jsonObject.increment("keyDouble");
+		/**
+		 * JSONObject constructor won't handle these types correctly, but adding them
+		 * via put works.
+		 */
+		jsonObject.put("keyFloat", 1.1f);
+		jsonObject.put("keyBigInt", new BigInteger("123456789123456789123456789123456780"));
+		jsonObject.put("keyBigDec", new BigDecimal("123456789123456789123456789123456780.1"));
+		jsonObject.increment("keyFloat");
+		jsonObject.increment("keyFloat");
+		jsonObject.increment("keyBigInt");
+		jsonObject.increment("keyBigDec");
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 6 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 6);
+		assertTrue("expected 3", Integer.valueOf(3).equals(jsonObject.query("/keyInt")));
+		assertTrue("expected 9999999993", Long.valueOf(9999999993L).equals(jsonObject.query("/keyLong")));
+		assertTrue("expected 3.1", BigDecimal.valueOf(3.1).equals(jsonObject.query("/keyDouble")));
+		assertTrue("expected 123456789123456789123456789123456781",
+				new BigInteger("123456789123456789123456789123456781").equals(jsonObject.query("/keyBigInt")));
+		assertTrue("expected 123456789123456789123456789123456781.1",
+				new BigDecimal("123456789123456789123456789123456781.1").equals(jsonObject.query("/keyBigDec")));
+
+		/**
+		 * Should work the same way on any platform! @see https://docs.oracle
+		 * .com/javase/specs/jls/se7/html/jls-4.html#jls-4.2.3 This is the effect of a
+		 * float to double conversion and is inherent to the shortcomings of the IEEE
+		 * 754 format, when converting 32-bit into double-precision 64-bit. Java
+		 * type-casts float to double. A 32 bit float is type-casted to 64 bit double by
+		 * simply appending zero-bits to the mantissa (and extended the signed exponent
+		 * by 3 bits.) and there is no way to obtain more information than it is stored
+		 * in the 32-bits float.
+		 * 
+		 * Like 1/3 cannot be represented as base10 number because it is periodically,
+		 * 1/5 (for example) cannot be represented as base2 number since it is
+		 * periodically in base2 (take a look at
+		 * http://www.h-schmidt.net/FloatConverter/) The same happens to 3.1, that
+		 * decimal number (base10 representation) is periodic in base2 representation,
+		 * therefore appending zero-bits is inaccurate. Only repeating the periodically
+		 * occurring bits (0110) would be a proper conversion. However one cannot detect
+		 * from a 32 bit IEE754 representation which bits would "repeat infinitely",
+		 * since the missing bits would not fit into the 32 bit float, i.e. the
+		 * information needed simply is not there!
+		 */
+		assertEquals(Float.valueOf(3.1f), jsonObject.query("/keyFloat"));
+
+		/**
+		 * float f = 3.1f; double df = (double) f; double d = 3.1d; System.out.println
+		 * (Integer.toBinaryString(Float.floatToRawIntBits(f))); System.out.println
+		 * (Long.toBinaryString(Double.doubleToRawLongBits(df))); System.out.println
+		 * (Long.toBinaryString(Double.doubleToRawLongBits(d)));
+		 * 
+		 * - Float: seeeeeeeemmmmmmmmmmmmmmmmmmmmmmm 1000000010001100110011001100110 -
+		 * Double seeeeeeeeeeemmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
+		 * 10000000 10001100110011001100110
+		 * 100000000001000110011001100110011000000000000000000000000000000
+		 * 100000000001000110011001100110011001100110011001100110011001101
+		 */
+
+		/**
+		 * Examples of well documented but probably unexpected behavior in java / with
+		 * 32-bit float to 64-bit float conversion.
+		 */
+		assertFalse("Document unexpected behaviour with explicit type-casting float as double!", (double) 0.2f == 0.2d);
+		assertFalse("Document unexpected behaviour with implicit type-cast!", 0.2f == 0.2d);
+		Double d1 = Double.valueOf(1.1f);
+		Double d2 = Double.valueOf("1.1f");
+		assertFalse("Document implicit type cast from float to double before calling Double(double d) constructor",
+				d1.equals(d2));
+
+		assertTrue("Correctly converting float to double via base10 (string) representation!",
+				Double.valueOf(3.1d).equals(Double.valueOf(Float.valueOf(3.1f).toString())));
+
+		// Pinpointing the not so obvious "buggy" conversion from float to double in
+		// JSONObject
+		JSONObject jo = new JSONObject();
+		jo.put("bug", 3.1f); // will call put( String key, double value ) with implicit and "buggy" type-cast
+								// from float to double
+		assertFalse(
+				"The java-compiler did add some zero bits for you to the mantissa (unexpected, but well documented)",
+				jo.get("bug").equals(Double.valueOf(3.1d)));
+
+		JSONObject inc = new JSONObject();
+		inc.put("bug", Float.valueOf(3.1f)); // This will put in instance of Float into JSONObject, i.e. call put(
+												// String key, Object value )
+		assertTrue("Everything is ok here!", inc.get("bug") instanceof Float);
+		inc.increment("bug"); // after adding 1, increment will call put( String key, double value ) with
+								// implicit and "buggy" type-cast from float to double!
+		// this.put(key, (Float) value + 1);
+		// 1. The (Object)value will be typecasted to (Float)value since it is an
+		// instanceof Float actually nothing is done.
+		// 2. Float instance will be autoboxed into float because the + operator will
+		// work on primitives not Objects!
+		// 3. A float+float operation will be performed and results into a float
+		// primitive.
+		// 4. There is no method that matches the signature put( String key, float
+		// value), java-compiler will choose the method
+		// put( String key, double value) and does an implicit type-cast(!) by appending
+		// zero-bits to the mantissa
+		assertTrue("JSONObject increment converts Float to Double", jo.get("bug") instanceof Float);
+		// correct implementation (with change of behavior) would be:
+		// this.put(key, new Float((Float) value + 1));
+		// Probably it would be better to deprecate the method and remove some day,
+		// while convenient processing the "payload" is not
+		// really in the scope of a JSON-library (IMHO.)
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject, inc)));
+	}
+
+	/**
+	 * Exercise JSONObject numberToString() method
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void jsonObjectNumberToString() {
+		String str;
+		Double dVal;
+		Integer iVal = 1;
+		str = JSONObject.numberToString(iVal);
+		assertTrue("expected " + iVal + " actual " + str, iVal.toString().equals(str));
+		dVal = 12.34;
+		str = JSONObject.numberToString(dVal);
+		assertTrue("expected " + dVal + " actual " + str, dVal.toString().equals(str));
+		dVal = 12.34e27;
+		str = JSONObject.numberToString(dVal);
+		assertTrue("expected " + dVal + " actual " + str, dVal.toString().equals(str));
+		// trailing .0 is truncated, so it doesn't quite match toString()
+		dVal = 5000000.0000000;
+		str = JSONObject.numberToString(dVal);
+		assertTrue("expected 5000000 actual " + str, str.equals("5000000"));
+	}
+
+	/**
+	 * Exercise JSONObject put() and similar() methods
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void jsonObjectPut() {
+		String expectedStr = "{" + "\"trueKey\":true," + "\"falseKey\":false," + "\"arrayKey\":[0,1,2],"
+				+ "\"objectKey\":{" + "\"myKey1\":\"myVal1\"," + "\"myKey2\":\"myVal2\"," + "\"myKey3\":\"myVal3\","
+				+ "\"myKey4\":\"myVal4\"" + "}" + "}";
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.put("trueKey", true);
+		jsonObject.put("falseKey", false);
+		Integer[] intArray = { 0, 1, 2 };
+		jsonObject.put("arrayKey", Arrays.asList(intArray));
+		Map myMap = new HashMap();
+		myMap.put("myKey1", "myVal1");
+		myMap.put("myKey2", "myVal2");
+		myMap.put("myKey3", "myVal3");
+		myMap.put("myKey4", "myVal4");
+		jsonObject.put("objectKey", myMap);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 4 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 4);
+		assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
+		assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
+		assertTrue("expected 3 arrayKey items", ((List) (JsonPath.read(doc, "$.arrayKey"))).size() == 3);
+		assertTrue("expected 0", Integer.valueOf(0).equals(jsonObject.query("/arrayKey/0")));
+		assertTrue("expected 1", Integer.valueOf(1).equals(jsonObject.query("/arrayKey/1")));
+		assertTrue("expected 2", Integer.valueOf(2).equals(jsonObject.query("/arrayKey/2")));
+		assertTrue("expected 4 objectKey items", ((Map) (JsonPath.read(doc, "$.objectKey"))).size() == 4);
+		assertTrue("expected myVal1", "myVal1".equals(jsonObject.query("/objectKey/myKey1")));
+		assertTrue("expected myVal2", "myVal2".equals(jsonObject.query("/objectKey/myKey2")));
+		assertTrue("expected myVal3", "myVal3".equals(jsonObject.query("/objectKey/myKey3")));
+		assertTrue("expected myVal4", "myVal4".equals(jsonObject.query("/objectKey/myKey4")));
+
+		jsonObject.remove("trueKey");
+		JSONObject expectedJsonObject = new JSONObject(expectedStr);
+		assertTrue("unequal jsonObjects should not be similar", !jsonObject.similar(expectedJsonObject));
+		assertTrue("jsonObject should not be similar to jsonArray", !jsonObject.similar(new JSONArray()));
+
+		String aCompareValueStr = "{\"a\":\"aval\",\"b\":true}";
+		String bCompareValueStr = "{\"a\":\"notAval\",\"b\":true}";
+		JSONObject aCompareValueJsonObject = new JSONObject(aCompareValueStr);
+		JSONObject bCompareValueJsonObject = new JSONObject(bCompareValueStr);
+		assertTrue("different values should not be similar", !aCompareValueJsonObject.similar(bCompareValueJsonObject));
+
+		String aCompareObjectStr = "{\"a\":\"aval\",\"b\":{}}";
+		String bCompareObjectStr = "{\"a\":\"aval\",\"b\":true}";
+		JSONObject aCompareObjectJsonObject = new JSONObject(aCompareObjectStr);
+		JSONObject bCompareObjectJsonObject = new JSONObject(bCompareObjectStr);
+		assertTrue("different nested JSONObjects should not be similar",
+				!aCompareObjectJsonObject.similar(bCompareObjectJsonObject));
+
+		String aCompareArrayStr = "{\"a\":\"aval\",\"b\":[]}";
+		String bCompareArrayStr = "{\"a\":\"aval\",\"b\":true}";
+		JSONObject aCompareArrayJsonObject = new JSONObject(aCompareArrayStr);
+		JSONObject bCompareArrayJsonObject = new JSONObject(bCompareArrayStr);
+		assertTrue("different nested JSONArrays should not be similar",
+				!aCompareArrayJsonObject.similar(bCompareArrayJsonObject));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject, expectedJsonObject,
+				aCompareValueJsonObject, aCompareArrayJsonObject, aCompareObjectJsonObject, aCompareArrayJsonObject,
+				bCompareValueJsonObject, bCompareArrayJsonObject, bCompareObjectJsonObject, bCompareArrayJsonObject)));
+	}
+
+	/**
+	 * Exercise JSONObject toString() method
+	 */
+	@Test
+	public void jsonObjectToString() {
+		String str = "{" + "\"trueKey\":true," + "\"falseKey\":false," + "\"arrayKey\":[0,1,2]," + "\"objectKey\":{"
+				+ "\"myKey1\":\"myVal1\"," + "\"myKey2\":\"myVal2\"," + "\"myKey3\":\"myVal3\","
+				+ "\"myKey4\":\"myVal4\"" + "}" + "}";
+		JSONObject jsonObject = new JSONObject(str);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 4 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 4);
+		assertTrue("expected true", Boolean.TRUE.equals(jsonObject.query("/trueKey")));
+		assertTrue("expected false", Boolean.FALSE.equals(jsonObject.query("/falseKey")));
+		assertTrue("expected 3 arrayKey items", ((List) (JsonPath.read(doc, "$.arrayKey"))).size() == 3);
+		assertTrue("expected 0", Integer.valueOf(0).equals(jsonObject.query("/arrayKey/0")));
+		assertTrue("expected 1", Integer.valueOf(1).equals(jsonObject.query("/arrayKey/1")));
+		assertTrue("expected 2", Integer.valueOf(2).equals(jsonObject.query("/arrayKey/2")));
+		assertTrue("expected 4 objectKey items", ((Map) (JsonPath.read(doc, "$.objectKey"))).size() == 4);
+		assertTrue("expected myVal1", "myVal1".equals(jsonObject.query("/objectKey/myKey1")));
+		assertTrue("expected myVal2", "myVal2".equals(jsonObject.query("/objectKey/myKey2")));
+		assertTrue("expected myVal3", "myVal3".equals(jsonObject.query("/objectKey/myKey3")));
+		assertTrue("expected myVal4", "myVal4".equals(jsonObject.query("/objectKey/myKey4")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise JSONObject toString() method with various indent levels.
+	 */
+	@Test
+	public void jsonObjectToStringIndent() {
+		String jsonObject0Str = "{" + "\"key1\":" + "[1,2," + "{\"key3\":true}" + "]," + "\"key2\":"
+				+ "{\"key1\":\"val1\",\"key2\":" + "{\"key2\":\"val2\"}" + "}," + "\"key3\":" + "[" + "[1,2.1]" + ","
+				+ "[null]" + "]" + "}";
+
+		String jsonObject1Str = "{\n" + " \"key1\": [\n" + "  1,\n" + "  2,\n" + "  {\"key3\": true}\n" + " ],\n"
+				+ " \"key2\": {\n" + "  \"key1\": \"val1\",\n" + "  \"key2\": {\"key2\": \"val2\"}\n" + " },\n"
+				+ " \"key3\": [\n" + "  [\n" + "   1,\n" + "   2.1\n" + "  ],\n" + "  [null]\n" + " ]\n" + "}";
+		String jsonObject4Str = "{\n" + "    \"key1\": [\n" + "        1,\n" + "        2,\n"
+				+ "        {\"key3\": true}\n" + "    ],\n" + "    \"key2\": {\n" + "        \"key1\": \"val1\",\n"
+				+ "        \"key2\": {\"key2\": \"val2\"}\n" + "    },\n" + "    \"key3\": [\n" + "        [\n"
+				+ "            1,\n" + "            2.1\n" + "        ],\n" + "        [null]\n" + "    ]\n" + "}";
+		JSONObject jsonObject = new JSONObject(jsonObject0Str);
+		// contents are tested in other methods, in this case just validate the spacing
+		// by
+		// checking length
+		assertEquals("toString() length", jsonObject0Str.length(), jsonObject.toString().length());
+		assertEquals("toString(0) length", jsonObject0Str.length(), jsonObject.toString(0).length());
+		assertEquals("toString(1) length", jsonObject1Str.length(), jsonObject.toString(1).length());
+		assertEquals("toString(4) length", jsonObject4Str.length(), jsonObject.toString(4).length());
+
+		JSONObject jo = new JSONObject().put("TABLE", new JSONObject().put("yhoo", new JSONObject()));
+		assertEquals("toString(2)", "{\"TABLE\": {\"yhoo\": {}}}", jo.toString(2));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject, jo)));
+	}
+
+	/**
+	 * Explores how JSONObject handles maps. Insert a string/string map as a value
+	 * in a JSONObject. It will remain a map. Convert the JSONObject to string, then
+	 * create a new JSONObject from the string. In the new JSONObject, the value
+	 * will be stored as a nested JSONObject. Confirm that map and nested JSONObject
+	 * have the same contents.
+	 */
+	@Test
+	public void jsonObjectToStringSuppressWarningOnCastToMap() {
+		JSONObject jsonObject = new JSONObject();
+		Map map = new HashMap<>();
+		map.put("abc", "def");
+		jsonObject.put("key", map);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 1 top level item", ((Map) (JsonPath.read(doc, "$"))).size() == 1);
+		assertTrue("expected 1 key item", ((Map) (JsonPath.read(doc, "$.key"))).size() == 1);
+		assertTrue("expected def", "def".equals(jsonObject.query("/key/abc")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Explores how JSONObject handles collections. Insert a string collection as a
+	 * value in a JSONObject. It will remain a collection. Convert the JSONObject to
+	 * string, then create a new JSONObject from the string. In the new JSONObject,
+	 * the value will be stored as a nested JSONArray. Confirm that collection and
+	 * nested JSONArray have the same contents.
+	 */
+	@Test
+	public void jsonObjectToStringSuppressWarningOnCastToCollection() {
+		JSONObject jsonObject = new JSONObject();
+		Collection collection = new ArrayList();
+		collection.add("abc");
+		// ArrayList will be added as an object
+		jsonObject.put("key", collection);
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
+		assertTrue("expected 1 top level item", ((Map) (JsonPath.read(doc, "$"))).size() == 1);
+		assertTrue("expected 1 key item", ((List) (JsonPath.read(doc, "$.key"))).size() == 1);
+		assertTrue("expected abc", "abc".equals(jsonObject.query("/key/0")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercises the JSONObject.valueToString() method for various types
+	 */
+	@Test
+	public void valueToString() {
+
+		assertTrue("null valueToString() incorrect", "null".equals(JSONObject.valueToString(null)));
+		MyJsonString jsonString = new MyJsonString();
+		assertTrue("jsonstring valueToString() incorrect", "my string".equals(JSONObject.valueToString(jsonString)));
+		assertTrue("boolean valueToString() incorrect", "true".equals(JSONObject.valueToString(Boolean.TRUE)));
+		assertTrue("non-numeric double", "null".equals(JSONObject.doubleToString(Double.POSITIVE_INFINITY)));
+		String jsonObjectStr = "{" + "\"key1\":\"val1\"," + "\"key2\":\"val2\"," + "\"key3\":\"val3\"" + "}";
+		JSONObject jsonObject = new JSONObject(jsonObjectStr);
+		assertTrue("jsonObject valueToString() incorrect",
+				new JSONObject(JSONObject.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()));
+		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))));
+		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)));
+		Integer[] array = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) };
+		assertTrue("array valueToString() incorrect", jsonArray.toString().equals(JSONObject.valueToString(array)));
+		Util.checkJSONObjectMaps(jsonObject);
+		Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
+	}
+
+	/**
+	 * Confirm that https://github.com/douglascrockford/JSON-java/issues/167 is
+	 * fixed. The following code was throwing a ClassCastException in the
+	 * JSONObject(Map) constructor
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	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);
+		// confirm result, just in case
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(str);
+		assertTrue("expected 1 top level item", ((Map) (JsonPath.read(doc, "$"))).size() == 1);
+		assertTrue("expected myValue", "myValue".equals(JsonPath.read(doc, "$.1")));
+	}
+
+	/**
+	 * Exercise the JSONObject wrap() method. Sometimes wrap() will change the
+	 * object being wrapped, other times not. The purpose of wrap() is to ensure the
+	 * value is packaged in a way that is compatible with how a JSONObject value or
+	 * JSONArray value is supposed to be stored.
+	 */
+	@Test
+	public void wrapObject() {
+		// wrap(null) returns NULL
+		assertTrue("null wrap() incorrect", JSONObject.NULL == JSONObject.wrap(null));
+
+		// wrap(Integer) returns Integer
+		Integer in = Integer.valueOf(1);
+		assertTrue("Integer wrap() incorrect", in == JSONObject.wrap(in));
+
+		/**
+		 * This test is to document the preferred behavior if BigDecimal is supported.
+		 * Previously bd returned as a string, since it is recognized as being a Java
+		 * package class. Now with explicit support for big numbers, it remains a
+		 * BigDecimal
+		 */
+		Object bdWrap = JSONObject.wrap(BigDecimal.ONE);
+		assertTrue("BigDecimal.ONE evaluates to ONE", bdWrap.equals(BigDecimal.ONE));
+
+		// wrap JSONObject returns JSONObject
+		String jsonObjectStr = "{" + "\"key1\":\"val1\"," + "\"key2\":\"val2\"," + "\"key3\":\"val3\"" + "}";
+		JSONObject jsonObject = new JSONObject(jsonObjectStr);
+		assertTrue("JSONObject wrap() incorrect", jsonObject == JSONObject.wrap(jsonObject));
+
+		// wrap collection returns JSONArray
+		Collection collection = new ArrayList();
+		collection.add(Integer.valueOf(1));
+		collection.add(Integer.valueOf(2));
+		collection.add(Integer.valueOf(3));
+		JSONArray jsonArray = (JSONArray) (JSONObject.wrap(collection));
+
+		// validate JSON
+		Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray.toString());
+		assertTrue("expected 3 top level items", ((List) (JsonPath.read(doc, "$"))).size() == 3);
+		assertTrue("expected 1", Integer.valueOf(1).equals(jsonArray.query("/0")));
+		assertTrue("expected 2", Integer.valueOf(2).equals(jsonArray.query("/1")));
+		assertTrue("expected 3", Integer.valueOf(3).equals(jsonArray.query("/2")));
+
+		// wrap Array returns JSONArray
+		Integer[] array = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) };
+		JSONArray integerArrayJsonArray = (JSONArray) (JSONObject.wrap(array));
+
+		// validate JSON
+		doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonArray.toString());
+		assertTrue("expected 3 top level items", ((List) (JsonPath.read(doc, "$"))).size() == 3);
+		assertTrue("expected 1", Integer.valueOf(1).equals(jsonArray.query("/0")));
+		assertTrue("expected 2", Integer.valueOf(2).equals(jsonArray.query("/1")));
+		assertTrue("expected 3", Integer.valueOf(3).equals(jsonArray.query("/2")));
+
+		// validate JSON
+		doc = Configuration.defaultConfiguration().jsonProvider().parse(integerArrayJsonArray.toString());
+		assertTrue("expected 3 top level items", ((List) (JsonPath.read(doc, "$"))).size() == 3);
+		assertTrue("expected 1", Integer.valueOf(1).equals(jsonArray.query("/0")));
+		assertTrue("expected 2", Integer.valueOf(2).equals(jsonArray.query("/1")));
+		assertTrue("expected 3", Integer.valueOf(3).equals(jsonArray.query("/2")));
+
+		// wrap map returns JSONObject
+		Map map = new HashMap();
+		map.put("key1", "val1");
+		map.put("key2", "val2");
+		map.put("key3", "val3");
+		JSONObject mapJsonObject = (JSONObject) (JSONObject.wrap(map));
+
+		// validate JSON
+		doc = Configuration.defaultConfiguration().jsonProvider().parse(mapJsonObject.toString());
+		assertTrue("expected 3 top level items", ((Map) (JsonPath.read(doc, "$"))).size() == 3);
+		assertTrue("expected val1", "val1".equals(mapJsonObject.query("/key1")));
+		assertTrue("expected val2", "val2".equals(mapJsonObject.query("/key2")));
+		assertTrue("expected val3", "val3".equals(mapJsonObject.query("/key3")));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObject, mapJsonObject)));
+		Util.checkJSONArrayMaps(jsonArray, jsonObject.getMapType());
+		Util.checkJSONArrayMaps(integerArrayJsonArray, jsonObject.getMapType());
+	}
+
+	/**
+	 * RFC 7159 defines control characters to be U+0000 through U+001F. This test
+	 * verifies that the parser is checking for these in expected ways.
+	 */
+	@Test
+	public void jsonObjectParseControlCharacters() {
+		for (int i = 0; i <= 0x001f; i++) {
+			final String charString = String.valueOf((char) i);
+			final String source = "{\"key\":\"" + charString + "\"}";
+			try {
+				JSONObject jo = new JSONObject(source);
+				assertTrue("Expected " + charString + "(" + i + ") in the JSON Object but did not find it.",
+						charString.equals(jo.getString("key")));
+				Util.checkJSONObjectMaps(jo);
+			} catch (JSONException ex) {
+				assertTrue("Only \\0 (U+0000), \\n (U+000A), and \\r (U+000D) should cause an error. Instead "
+						+ charString + "(" + i + ") caused an error", i == '\0' || i == '\n' || i == '\r');
+			}
+		}
+	}
+
+	@Test
+	public void jsonObjectParseControlCharacterEOFAssertExceptionMessage() {
+		char c = '\0';
+		final String source = "{\"key\":\"" + c + "\"}";
+		try {
+			JSONObject jo = new JSONObject(source);
+			fail("JSONException should be thrown");
+		} catch (JSONException ex) {
+			assertEquals("Unterminated string. " + "Character with int code 0"
+					+ " is not allowed within a quoted string. at 8 [character 9 line 1]", ex.getMessage());
+		}
+	}
+
+	@Test
+	public void jsonObjectParseControlCharacterNewLineAssertExceptionMessage() {
+		char[] chars = { '\n', '\r' };
+		for (char c : chars) {
+			final String source = "{\"key\":\"" + c + "\"}";
+			try {
+				JSONObject jo = new JSONObject(source);
+				fail("JSONException should be thrown");
+			} catch (JSONException ex) {
+				assertEquals("Unterminated string. " + "Character with int code " + (int) c
+						+ " is not allowed within a quoted string. at 9 [character 0 line 2]", ex.getMessage());
+			}
+		}
+	}
+
+	@Test
+	public void jsonObjectParseUTF8EncodingAssertExceptionMessage() {
+		String c = "\\u123x";
+		final String source = "{\"key\":\"" + c + "\"}";
+		try {
+			JSONObject jo = new JSONObject(source);
+			fail("JSONException should be thrown");
+		} catch (JSONException ex) {
+			assertEquals("Illegal escape. \\u must be followed by a 4 digit hexadecimal number. "
+					+ "\\123x is not valid. at 14 [character 15 line 1]", ex.getMessage());
+		}
+	}
+
+	@Test
+	public void jsonObjectParseIllegalEscapeAssertExceptionMessage() {
+		String c = "\\x";
+		final String source = "{\"key\":\"" + c + "\"}";
+		try {
+			JSONObject jo = new JSONObject(source);
+			fail("JSONException should be thrown");
+		} catch (JSONException ex) {
+			assertEquals("Illegal escape. Escape sequence  " + c + " is not valid." + " at 10 [character 11 line 1]",
+					ex.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorTrailingCurlyBrace() {
+		try {
+			// does not end with '}'
+			String str = "{";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"A JSONObject text must end with '}' at 1 [character 2 line 1]", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorInitialCurlyBrace() {
+		try {
+			// does not start with '{'
+			String str = "abc";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"A JSONObject text must begin with '{' at 1 [character 2 line 1]", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorNoColon() {
+		try {
+			// key with no ':'
+			String str = "{\"myKey\" = true}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Expected a ':' after a key at 10 [character 11 line 1]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorNoCommaSeparator() {
+		try {
+			// entries with no ',' separator
+			String str = "{\"myKey\":true \"myOtherKey\":false}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Expected a ',' or '}' at 15 [character 16 line 1]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorKeyIsNestedMap() {
+		try {
+			// key is a nested map
+			String str = "{{\"foo\": \"bar\"}: \"baz\"}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Missing value at 1 [character 2 line 1]", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorKeyIsNestedArrayWithMap() {
+		try {
+			// key is a nested array containing a map
+			String str = "{\"a\": 1, [{\"foo\": \"bar\"}]: \"baz\"}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Missing value at 9 [character 10 line 1]", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorKeyContainsCurlyBrace() {
+		try {
+			// key contains }
+			String str = "{foo}: 2}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+//            assertEquals("Expecting an exception message",
+//                    "Expected a ':' after a key at 5 [character 6 line 1]",
+//                    e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorKeyContainsSquareBrace() {
+		try {
+			// key contains ]
+			String str = "{foo]: 2}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+//            assertEquals("Expecting an exception message",
+//                    "Expected a ':' after a key at 5 [character 6 line 1]",
+//                    e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorKeyContainsBinaryZero() {
+		try {
+			// \0 after ,
+			String str = "{\"myKey\":true, \0\"myOtherKey\":false}";
+			assertNull("Expected an exception", new JSONObject(str));
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message",
+					"A JSONObject text must end with '}' at 15 [character 16 line 1]", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorAppendToWrongValue() {
+		try {
+			// append to wrong value
+			String str = "{\"myKey\":true, \"myOtherKey\":false}";
+			JSONObject jsonObject = new JSONObject(str);
+			jsonObject.append("myKey", "hello");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "JSONObject[\"myKey\"] is not a JSONArray (null).",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorIncrementWrongValue() {
+		try {
+			// increment wrong value
+			String str = "{\"myKey\":true, \"myOtherKey\":false}";
+			JSONObject jsonObject = new JSONObject(str);
+			jsonObject.increment("myKey");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Unable to increment [\"myKey\"].", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorInvalidKey() {
+		try {
+			// invalid key
+			String str = "{\"myKey\":true, \"myOtherKey\":false}";
+			JSONObject jsonObject = new JSONObject(str);
+			jsonObject.get(null);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Null key.", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorNumberToString() {
+		try {
+			// invalid numberToString()
+			JSONObject.numberToString((Number) null);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an exception message", "Null pointer", e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorPutOnceDuplicateKey() {
+		try {
+			// multiple putOnce key
+			JSONObject jsonObject = new JSONObject("{}");
+			jsonObject.putOnce("hello", "world");
+			jsonObject.putOnce("hello", "world!");
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertTrue("", true);
+		}
+	}
+
+	@Test
+	public void parsingErrorInvalidDouble() {
+		try {
+			// test validity of invalid double
+			JSONObject.testValidity(Double.NaN);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertTrue("", true);
+		}
+	}
+
+	@Test
+	public void parsingErrorInvalidFloat() {
+		try {
+			// test validity of invalid float
+			JSONObject.testValidity(Float.NEGATIVE_INFINITY);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertTrue("", true);
+		}
+	}
+
+	@Test
+	public void parsingErrorDuplicateKeyException() {
+		try {
+			// test exception message when including a duplicate key (level 0)
+			String str = "{\n" + "    \"attr01\":\"value-01\",\n" + "    \"attr02\":\"value-02\",\n"
+					+ "    \"attr03\":\"value-03\",\n" + "    \"attr03\":\"value-04\"\n" + "}";
+			new JSONObject(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr03\" at 90 [character 13 line 5]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorNestedDuplicateKeyException() {
+		try {
+			// test exception message when including a duplicate key (level 0) holding an
+			// object
+			String str = "{\n" + "    \"attr01\":\"value-01\",\n" + "    \"attr02\":\"value-02\",\n"
+					+ "    \"attr03\":\"value-03\",\n" + "    \"attr03\": {" + "        \"attr04-01\":\"value-04-01\",n"
+					+ "        \"attr04-02\":\"value-04-02\",n" + "        \"attr04-03\":\"value-04-03\"n" + "    }\n"
+					+ "}";
+			new JSONObject(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr03\" at 90 [character 13 line 5]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorNestedDuplicateKeyWithArrayException() {
+		try {
+			// test exception message when including a duplicate key (level 0) holding an
+			// array
+			String str = "{\n" + "    \"attr01\":\"value-01\",\n" + "    \"attr02\":\"value-02\",\n"
+					+ "    \"attr03\":\"value-03\",\n" + "    \"attr03\": [\n" + "        {"
+					+ "            \"attr04-01\":\"value-04-01\",n" + "            \"attr04-02\":\"value-04-02\",n"
+					+ "            \"attr04-03\":\"value-04-03\"n" + "        }\n" + "    ]\n" + "}";
+			new JSONObject(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr03\" at 90 [character 13 line 5]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorDuplicateKeyWithinNestedDictExceptionMessage() {
+		try {
+			// test exception message when including a duplicate key (level 1)
+			String str = "{\n" + "    \"attr01\":\"value-01\",\n" + "    \"attr02\":\"value-02\",\n"
+					+ "    \"attr03\":\"value-03\",\n" + "    \"attr04\": {\n"
+					+ "        \"attr04-01\":\"value04-01\",\n" + "        \"attr04-02\":\"value04-02\",\n"
+					+ "        \"attr04-03\":\"value04-03\",\n" + "        \"attr04-03\":\"value04-04\"\n" + "    }\n"
+					+ "}";
+			new JSONObject(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorDuplicateKeyDoubleNestedDictExceptionMessage() {
+		try {
+			// test exception message when including a duplicate key (level 1) holding an
+			// object
+			String str = "{\n" + "    \"attr01\":\"value-01\",\n" + "    \"attr02\":\"value-02\",\n"
+					+ "    \"attr03\":\"value-03\",\n" + "    \"attr04\": {\n"
+					+ "        \"attr04-01\":\"value04-01\",\n" + "        \"attr04-02\":\"value04-02\",\n"
+					+ "        \"attr04-03\":\"value04-03\",\n" + "        \"attr04-03\": {\n"
+					+ "            \"attr04-04-01\":\"value04-04-01\",\n"
+					+ "            \"attr04-04-02\":\"value04-04-02\",\n"
+					+ "            \"attr04-04-03\":\"value04-04-03\",\n" + "        }\n" + "    }\n" + "}";
+			new JSONObject(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorDuplicateKeyNestedWithArrayExceptionMessage() {
+		try {
+			// test exception message when including a duplicate key (level 1) holding an
+			// array
+			String str = "{\n" + "    \"attr01\":\"value-01\",\n" + "    \"attr02\":\"value-02\",\n"
+					+ "    \"attr03\":\"value-03\",\n" + "    \"attr04\": {\n"
+					+ "        \"attr04-01\":\"value04-01\",\n" + "        \"attr04-02\":\"value04-02\",\n"
+					+ "        \"attr04-03\":\"value04-03\",\n" + "        \"attr04-03\": [\n" + "            {\n"
+					+ "                \"attr04-04-01\":\"value04-04-01\",\n"
+					+ "                \"attr04-04-02\":\"value04-04-02\",\n"
+					+ "                \"attr04-04-03\":\"value04-04-03\",\n" + "            }\n" + "        ]\n"
+					+ "    }\n" + "}";
+			new JSONObject(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorDuplicateKeyWithinArrayExceptionMessage() {
+		try {
+			// test exception message when including a duplicate key in object (level 0)
+			// within an array
+			String str = "[\n" + "    {\n" + "        \"attr01\":\"value-01\",\n" + "        \"attr02\":\"value-02\"\n"
+					+ "    },\n" + "    {\n" + "        \"attr01\":\"value-01\",\n"
+					+ "        \"attr01\":\"value-02\"\n" + "    }\n" + "]";
+			new JSONArray(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr01\" at 124 [character 17 line 8]",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void parsingErrorDuplicateKeyDoubleNestedWithinArrayExceptionMessage() {
+		try {
+			// test exception message when including a duplicate key in object (level 1)
+			// within an array
+			String str = "[\n" + "    {\n" + "        \"attr01\":\"value-01\",\n" + "        \"attr02\": {\n"
+					+ "            \"attr02-01\":\"value-02-01\",\n" + "            \"attr02-02\":\"value-02-02\"\n"
+					+ "        }\n" + "    },\n" + "    {\n" + "        \"attr01\":\"value-01\",\n"
+					+ "        \"attr02\": {\n" + "            \"attr02-01\":\"value-02-01\",\n"
+					+ "            \"attr02-01\":\"value-02-02\"\n" + "        }\n" + "    }\n" + "]";
+			new JSONArray(str);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			assertEquals("Expecting an expection message", "Duplicate key \"attr02-01\" at 269 [character 24 line 13]",
+					e.getMessage());
+		}
+	}
+
+	/**
+	 * Confirm behavior when putOnce() is called with null parameters
+	 */
+	@Test
+	public void jsonObjectPutOnceNull() {
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.putOnce(null, null);
+		assertTrue("jsonObject should be empty", jsonObject.isEmpty());
+		jsonObject.putOnce("", null);
+		assertTrue("jsonObject should be empty", jsonObject.isEmpty());
+		jsonObject.putOnce(null, "");
+		assertTrue("jsonObject should be empty", jsonObject.isEmpty());
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise JSONObject opt(key, default) method.
+	 */
+	@Test
+	public void jsonObjectOptDefault() {
+
+		String str = "{\"myKey\": \"myval\", \"hiKey\": null}";
+		JSONObject jsonObject = new JSONObject(str);
+
+		assertTrue("optBigDecimal() should return default BigDecimal",
+				BigDecimal.TEN.compareTo(jsonObject.optBigDecimal("myKey", BigDecimal.TEN)) == 0);
+		assertTrue("optBigInteger() should return default BigInteger",
+				BigInteger.TEN.compareTo(jsonObject.optBigInteger("myKey", BigInteger.TEN)) == 0);
+		assertTrue("optBoolean() should return default boolean", jsonObject.optBoolean("myKey", true));
+		assertTrue("optBooleanObject() should return default Boolean", jsonObject.optBooleanObject("myKey", true));
+		assertTrue("optInt() should return default int", 42 == jsonObject.optInt("myKey", 42));
+		assertTrue("optIntegerObject() should return default Integer",
+				Integer.valueOf(42).equals(jsonObject.optIntegerObject("myKey", 42)));
+		assertTrue("optEnum() should return default Enum",
+				MyEnum.VAL1.equals(jsonObject.optEnum(MyEnum.class, "myKey", MyEnum.VAL1)));
+		assertTrue("optJSONArray() should return null ", null == jsonObject.optJSONArray("myKey"));
+		assertTrue("optJSONArray() should return default JSONArray",
+				"value".equals(jsonObject.optJSONArray("myKey", new JSONArray("[\"value\"]")).getString(0)));
+		assertTrue("optJSONObject() should return default JSONObject ",
+				jsonObject.optJSONObject("myKey", new JSONObject("{\"testKey\":\"testValue\"}")).getString("testKey")
+						.equals("testValue"));
+		assertTrue("optLong() should return default long", 42l == jsonObject.optLong("myKey", 42l));
+		assertTrue("optLongObject() should return default Long",
+				Long.valueOf(42l).equals(jsonObject.optLongObject("myKey", 42l)));
+		assertTrue("optDouble() should return default double", 42.3d == jsonObject.optDouble("myKey", 42.3d));
+		assertTrue("optDoubleObject() should return default Double",
+				Double.valueOf(42.3d).equals(jsonObject.optDoubleObject("myKey", 42.3d)));
+		assertTrue("optFloat() should return default float", 42.3f == jsonObject.optFloat("myKey", 42.3f));
+		assertTrue("optFloatObject() should return default Float",
+				Float.valueOf(42.3f).equals(jsonObject.optFloatObject("myKey", 42.3f)));
+		assertTrue("optNumber() should return default Number",
+				42l == jsonObject.optNumber("myKey", Long.valueOf(42)).longValue());
+		assertTrue("optString() should return default string", "hi".equals(jsonObject.optString("hiKey", "hi")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Exercise JSONObject opt(key, default) method when the key doesn't exist.
+	 */
+	@Test
+	public void jsonObjectOptNoKey() {
+
+		JSONObject jsonObject = new JSONObject();
+
+		assertNull(jsonObject.opt(null));
+
+		assertTrue("optBigDecimal() should return default BigDecimal",
+				BigDecimal.TEN.compareTo(jsonObject.optBigDecimal("myKey", BigDecimal.TEN)) == 0);
+		assertTrue("optBigInteger() should return default BigInteger",
+				BigInteger.TEN.compareTo(jsonObject.optBigInteger("myKey", BigInteger.TEN)) == 0);
+		assertTrue("optBoolean() should return default boolean", jsonObject.optBoolean("myKey", true));
+		assertTrue("optBooleanObject() should return default Boolean", jsonObject.optBooleanObject("myKey", true));
+		assertTrue("optInt() should return default int", 42 == jsonObject.optInt("myKey", 42));
+		assertTrue("optIntegerObject() should return default Integer",
+				Integer.valueOf(42).equals(jsonObject.optIntegerObject("myKey", 42)));
+		assertTrue("optEnum() should return default Enum",
+				MyEnum.VAL1.equals(jsonObject.optEnum(MyEnum.class, "myKey", MyEnum.VAL1)));
+		assertTrue("optJSONArray() should return default JSONArray",
+				"value".equals(jsonObject.optJSONArray("myKey", new JSONArray("[\"value\"]")).getString(0)));
+		assertTrue("optJSONArray() should return null ", null == jsonObject.optJSONArray("myKey"));
+		assertTrue("optJSONObject() should return default JSONObject ",
+				jsonObject.optJSONObject("myKey", new JSONObject("{\"testKey\":\"testValue\"}")).getString("testKey")
+						.equals("testValue"));
+		assertTrue("optLong() should return default long", 42l == jsonObject.optLong("myKey", 42l));
+		assertTrue("optLongObject() should return default Long",
+				Long.valueOf(42l).equals(jsonObject.optLongObject("myKey", 42l)));
+		assertTrue("optDouble() should return default double", 42.3d == jsonObject.optDouble("myKey", 42.3d));
+		assertTrue("optDoubleObject() should return default Double",
+				Double.valueOf(42.3d).equals(jsonObject.optDoubleObject("myKey", 42.3d)));
+		assertTrue("optFloat() should return default float", 42.3f == jsonObject.optFloat("myKey", 42.3f));
+		assertTrue("optFloatObject() should return default Float",
+				Float.valueOf(42.3f).equals(jsonObject.optFloatObject("myKey", 42.3f)));
+		assertTrue("optNumber() should return default Number",
+				42l == jsonObject.optNumber("myKey", Long.valueOf(42)).longValue());
+		assertTrue("optString() should return default string", "hi".equals(jsonObject.optString("hiKey", "hi")));
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Verifies that the opt methods properly convert string values.
+	 */
+	@Test
+	public void jsonObjectOptStringConversion() {
+		JSONObject jo = new JSONObject("{\"int\":\"123\",\"true\":\"true\",\"false\":\"false\"}");
+		assertTrue("unexpected optBoolean value", jo.optBoolean("true", false) == true);
+		assertTrue("unexpected optBooleanObject value",
+				Boolean.valueOf(true).equals(jo.optBooleanObject("true", false)));
+		assertTrue("unexpected optBoolean value", jo.optBoolean("false", true) == false);
+		assertTrue("unexpected optBooleanObject value",
+				Boolean.valueOf(false).equals(jo.optBooleanObject("false", true)));
+		assertTrue("unexpected optInt value", jo.optInt("int", 0) == 123);
+		assertTrue("unexpected optIntegerObject value", Integer.valueOf(123).equals(jo.optIntegerObject("int", 0)));
+		assertTrue("unexpected optLong value", jo.optLong("int", 0) == 123l);
+		assertTrue("unexpected optLongObject value", Long.valueOf(123l).equals(jo.optLongObject("int", 0L)));
+		assertTrue("unexpected optDouble value", jo.optDouble("int", 0.0d) == 123.0d);
+		assertTrue("unexpected optDoubleObject value", Double.valueOf(123.0d).equals(jo.optDoubleObject("int", 0.0d)));
+		assertTrue("unexpected optFloat value", jo.optFloat("int", 0.0f) == 123.0f);
+		assertTrue("unexpected optFloatObject value", Float.valueOf(123.0f).equals(jo.optFloatObject("int", 0.0f)));
+		assertTrue("unexpected optBigInteger value",
+				jo.optBigInteger("int", BigInteger.ZERO).compareTo(new BigInteger("123")) == 0);
+		assertTrue("unexpected optBigDecimal value",
+				jo.optBigDecimal("int", BigDecimal.ZERO).compareTo(new BigDecimal("123")) == 0);
+		assertTrue("unexpected optBigDecimal value",
+				jo.optBigDecimal("int", BigDecimal.ZERO).compareTo(new BigDecimal("123")) == 0);
+		assertTrue("unexpected optNumber value", jo.optNumber("int", BigInteger.ZERO).longValue() == 123l);
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Verifies that the opt methods properly convert string values to numbers and
+	 * coerce them consistently.
+	 */
+	@Test
+	public void jsonObjectOptCoercion() {
+		JSONObject jo = new JSONObject(
+				"{\"largeNumberStr\":\"19007199254740993.35481234487103587486413587843213584\"}");
+		// currently the parser doesn't recognize BigDecimal, to we have to put it
+		// manually
+		jo.put("largeNumber", new BigDecimal("19007199254740993.35481234487103587486413587843213584"));
+
+		// Test type coercion from larger to smaller
+		assertEquals(new BigDecimal("19007199254740993.35481234487103587486413587843213584"),
+				jo.optBigDecimal("largeNumber", null));
+		assertEquals(new BigInteger("19007199254740993"), jo.optBigInteger("largeNumber", null));
+		assertEquals(1.9007199254740992E16, jo.optDouble("largeNumber"), 0.0);
+		assertEquals(1.9007199254740992E16, jo.optDoubleObject("largeNumber"), 0.0);
+		assertEquals(1.90071995E16f, jo.optFloat("largeNumber"), 0.0f);
+		assertEquals(1.90071995E16f, jo.optFloatObject("largeNumber"), 0.0f);
+		assertEquals(19007199254740993l, jo.optLong("largeNumber"));
+		assertEquals(Long.valueOf(19007199254740993l), jo.optLongObject("largeNumber"));
+		assertEquals(1874919425, jo.optInt("largeNumber"));
+		assertEquals(Integer.valueOf(1874919425), jo.optIntegerObject("largeNumber"));
+
+		// conversion from a string
+		assertEquals(new BigDecimal("19007199254740993.35481234487103587486413587843213584"),
+				jo.optBigDecimal("largeNumberStr", null));
+		assertEquals(new BigInteger("19007199254740993"), jo.optBigInteger("largeNumberStr", null));
+		assertEquals(1.9007199254740992E16, jo.optDouble("largeNumberStr"), 0.0);
+		assertEquals(1.9007199254740992E16, jo.optDoubleObject("largeNumberStr"), 0.0);
+		assertEquals(1.90071995E16f, jo.optFloat("largeNumberStr"), 0.0f);
+		assertEquals(1.90071995E16f, jo.optFloatObject("largeNumberStr"), 0.0f);
+		assertEquals(19007199254740993l, jo.optLong("largeNumberStr"));
+		assertEquals(Long.valueOf(19007199254740993l), jo.optLongObject("largeNumberStr"));
+		assertEquals(1874919425, jo.optInt("largeNumberStr"));
+		assertEquals(Integer.valueOf(1874919425), jo.optIntegerObject("largeNumberStr"));
+
+		// the integer portion of the actual value is larger than a double can hold.
+		assertNotEquals((long) Double.parseDouble("19007199254740993.35481234487103587486413587843213584"),
+				jo.optLong("largeNumber"));
+		assertNotEquals(
+				Long.valueOf((long) Double.parseDouble("19007199254740993.35481234487103587486413587843213584")),
+				jo.optLongObject("largeNumber"));
+		assertNotEquals((int) Double.parseDouble("19007199254740993.35481234487103587486413587843213584"),
+				jo.optInt("largeNumber"));
+		assertNotEquals(
+				Integer.valueOf((int) Double.parseDouble("19007199254740993.35481234487103587486413587843213584")),
+				jo.optIntegerObject("largeNumber"));
+		assertNotEquals((long) Double.parseDouble("19007199254740993.35481234487103587486413587843213584"),
+				jo.optLong("largeNumberStr"));
+		assertNotEquals(
+				Long.valueOf((long) Double.parseDouble("19007199254740993.35481234487103587486413587843213584")),
+				jo.optLongObject("largeNumberStr"));
+		assertNotEquals((int) Double.parseDouble("19007199254740993.35481234487103587486413587843213584"),
+				jo.optInt("largeNumberStr"));
+		assertNotEquals(
+				Integer.valueOf((int) Double.parseDouble("19007199254740993.35481234487103587486413587843213584")),
+				jo.optIntegerObject("largeNumberStr"));
+		assertEquals(19007199254740992l,
+				(long) Double.parseDouble("19007199254740993.35481234487103587486413587843213584"));
+		assertEquals(2147483647, (int) Double.parseDouble("19007199254740993.35481234487103587486413587843213584"));
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Verifies that the optBigDecimal method properly converts values to BigDecimal
+	 * and coerce them consistently.
+	 */
+	@Test
+	public void jsonObjectOptBigDecimal() {
+		JSONObject jo = new JSONObject().put("int", 123).put("long", 654L).put("float", 1.234f).put("double", 2.345d)
+				.put("bigInteger", new BigInteger("1234")).put("bigDecimal", new BigDecimal("1234.56789"))
+				.put("nullVal", JSONObject.NULL);
+
+		assertEquals(new BigDecimal("123"), jo.optBigDecimal("int", null));
+		assertEquals(new BigDecimal("654"), jo.optBigDecimal("long", null));
+		assertEquals(new BigDecimal(1.234f), jo.optBigDecimal("float", null));
+		assertEquals(new BigDecimal(2.345d), jo.optBigDecimal("double", null));
+		assertEquals(new BigDecimal("1234"), jo.optBigDecimal("bigInteger", null));
+		assertEquals(new BigDecimal("1234.56789"), jo.optBigDecimal("bigDecimal", null));
+		assertNull(jo.optBigDecimal("nullVal", null));
+		assertEquals(jo.optBigDecimal("float", null), jo.getBigDecimal("float"));
+		assertEquals(jo.optBigDecimal("double", null), jo.getBigDecimal("double"));
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Verifies that the optBigDecimal method properly converts values to BigDecimal
+	 * and coerce them consistently.
+	 */
+	@Test
+	public void jsonObjectOptBigInteger() {
+		JSONObject jo = new JSONObject().put("int", 123).put("long", 654L).put("float", 1.234f).put("double", 2.345d)
+				.put("bigInteger", new BigInteger("1234")).put("bigDecimal", new BigDecimal("1234.56789"))
+				.put("nullVal", JSONObject.NULL);
+
+		assertEquals(new BigInteger("123"), jo.optBigInteger("int", null));
+		assertEquals(new BigInteger("654"), jo.optBigInteger("long", null));
+		assertEquals(new BigInteger("1"), jo.optBigInteger("float", null));
+		assertEquals(new BigInteger("2"), jo.optBigInteger("double", null));
+		assertEquals(new BigInteger("1234"), jo.optBigInteger("bigInteger", null));
+		assertEquals(new BigInteger("1234"), jo.optBigInteger("bigDecimal", null));
+		assertNull(jo.optBigDecimal("nullVal", null));
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Confirm behavior when JSONObject put(key, null object) is called
+	 */
+	@Test
+	public void jsonObjectputNull() {
+
+		// put null should remove the item.
+		String str = "{\"myKey\": \"myval\"}";
+		JSONObject jsonObjectRemove = new JSONObject(str);
+		jsonObjectRemove.remove("myKey");
+		assertTrue("jsonObject should be empty", jsonObjectRemove.isEmpty());
+
+		JSONObject jsonObjectPutNull = new JSONObject(str);
+		jsonObjectPutNull.put("myKey", (Object) null);
+		assertTrue("jsonObject should be empty", jsonObjectPutNull.isEmpty());
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObjectRemove, jsonObjectPutNull)));
+	}
+
+	/**
+	 * Exercise JSONObject quote() method This purpose of quote() is to ensure that
+	 * for strings with embedded quotes, the quotes are properly escaped.
+	 */
+	@Test
+	public void jsonObjectQuote() {
+		String str;
+		str = "";
+		String quotedStr;
+		quotedStr = JSONObject.quote(str);
+		assertTrue("quote() expected escaped quotes, found " + quotedStr, "\"\"".equals(quotedStr));
+		str = "\"\"";
+		quotedStr = JSONObject.quote(str);
+		assertTrue("quote() expected escaped quotes, found " + quotedStr, "\"\\\"\\\"\"".equals(quotedStr));
+		str = "null and null will be emitted as ""
-         */
-        String sJONull = XML.toString(jsonObjectJONull);
-        assertTrue("JSONObject.NULL should emit a null value", 
-                "null".equals(sJONull));
-        String sNull = XML.toString(jsonObjectNull);
-        assertTrue("null should emit an empty string", "".equals(sNull));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jsonObjectJONull, jsonObjectNull
-        )));
-    }
-    
-    @Test(expected = JSONPointerException.class)
-    public void queryWithNoResult() {
-        new JSONObject().query("/a/b");
-    }
-    
-    @Test
-    public void optQueryWithNoResult() {
-        assertNull(new JSONObject().optQuery("/a/b"));
-    }
-    
-    @Test(expected = IllegalArgumentException.class)
-    public void optQueryWithSyntaxError() {
-        new JSONObject().optQuery("invalid");
-    }
-
-    @Test(expected = JSONException.class)
-    public void invalidEscapeSequence() {
-      String json = "{ \"\\url\": \"value\" }";
-      assertNull("Expected an exception",new JSONObject(json));
-    }
-
-    /**
-     * Exercise JSONObject toMap() method.
-     */
-    @Test
-    public void toMap() {
-        String jsonObjectStr =
-                "{" +
-                "\"key1\":" +
-                    "[1,2," +
-                        "{\"key3\":true}" +
-                    "]," +
-                "\"key2\":" +
-                    "{\"key1\":\"val1\",\"key2\":" +
-                        "{\"key2\":null}," +
-                    "\"key3\":42" +
-                    "}," +
-                "\"key3\":" +
-                    "[" +
-                        "[\"value1\",2.1]" +
-                    "," +
-                        "[null]" +
-                    "]" +
-                "}";
-
-        JSONObject jsonObject = new JSONObject(jsonObjectStr);
-        Map map = jsonObject.toMap();
-
-        assertTrue("Map should not be null", map != null);
-        assertTrue("Map should have 3 elements", map.size() == 3);
-
-        List key1List = (List)map.get("key1");
-        assertTrue("key1 should not be null", key1List != null);
-        assertTrue("key1 list should have 3 elements", key1List.size() == 3);
-        assertTrue("key1 value 1 should be 1", key1List.get(0).equals(Integer.valueOf(1)));
-        assertTrue("key1 value 2 should be 2", key1List.get(1).equals(Integer.valueOf(2)));
-
-        Map key1Value3Map = (Map)key1List.get(2);
-        assertTrue("Map should not be null", key1Value3Map != null);
-        assertTrue("Map should have 1 element", key1Value3Map.size() == 1);
-        assertTrue("Map key3 should be true", key1Value3Map.get("key3").equals(Boolean.TRUE));
-
-        Map key2Map = (Map)map.get("key2");
-        assertTrue("key2 should not be null", key2Map != null);
-        assertTrue("key2 map should have 3 elements", key2Map.size() == 3);
-        assertTrue("key2 map key 1 should be val1", key2Map.get("key1").equals("val1"));
-        assertTrue("key2 map key 3 should be 42", key2Map.get("key3").equals(Integer.valueOf(42)));
-
-        Map key2Val2Map = (Map)key2Map.get("key2");
-        assertTrue("key2 map key 2 should not be null", key2Val2Map != null);
-        assertTrue("key2 map key 2 should have an entry", key2Val2Map.containsKey("key2"));
-        assertTrue("key2 map key 2 value should be null", key2Val2Map.get("key2") == null);
-
-        List key3List = (List)map.get("key3");
-        assertTrue("key3 should not be null", key3List != null);
-        assertTrue("key3 list should have 3 elements", key3List.size() == 2);
-
-        List key3Val1List = (List)key3List.get(0);
-        assertTrue("key3 list val 1 should not be null", key3Val1List != null);
-        assertTrue("key3 list val 1 should have 2 elements", key3Val1List.size() == 2);
-        assertTrue("key3 list val 1 list element 1 should be value1", key3Val1List.get(0).equals("value1"));
-        assertTrue("key3 list val 1 list element 2 should be 2.1", key3Val1List.get(1).equals(new BigDecimal("2.1")));
-
-        List key3Val2List = (List)key3List.get(1);
-        assertTrue("key3 list val 2 should not be null", key3Val2List != null);
-        assertTrue("key3 list val 2 should have 1 element", key3Val2List.size() == 1);
-        assertTrue("key3 list val 2 list element 1 should be null", key3Val2List.get(0) == null);
-
-        // Assert that toMap() is a deep copy
-        jsonObject.getJSONArray("key3").getJSONArray(0).put(0, "still value 1");
-        assertTrue("key3 list val 1 list element 1 should be value1", key3Val1List.get(0).equals("value1"));
-
-        // assert that the new map is mutable
-        assertTrue("Removing a key should succeed", map.remove("key3") != null);
-        assertTrue("Map should have 2 elements", map.size() == 2);
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-    
-    /**
-     * test that validates a singleton can be serialized as a bean.
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void testSingletonBean() {
-        final JSONObject jo = new JSONObject(Singleton.getInstance());
-        assertEquals(jo.keySet().toString(), 1, jo.length());
-        assertEquals(0, jo.get("someInt"));
-        assertEquals(null, jo.opt("someString"));
-        
-        // Update the singleton values
-        Singleton.getInstance().setSomeInt(42);
-        Singleton.getInstance().setSomeString("Something");
-        final JSONObject jo2 = new JSONObject(Singleton.getInstance());
-        assertEquals(2, jo2.length());
-        assertEquals(42, jo2.get("someInt"));
-        assertEquals("Something", jo2.get("someString"));
-
-        // ensure our original jo hasn't changed.
-        assertEquals(0, jo.get("someInt"));
-        assertEquals(null, jo.opt("someString"));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jo, jo2
-        )));
-    }
-
-    /**
-     * test that validates a singleton can be serialized as a bean.
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void testSingletonEnumBean() {
-        final JSONObject jo = new JSONObject(SingletonEnum.getInstance());
-        assertEquals(jo.keySet().toString(), 1, jo.length());
-        assertEquals(0, jo.get("someInt"));
-        assertEquals(null, jo.opt("someString"));
-        
-        // Update the singleton values
-        SingletonEnum.getInstance().setSomeInt(42);
-        SingletonEnum.getInstance().setSomeString("Something");
-        final JSONObject jo2 = new JSONObject(SingletonEnum.getInstance());
-        assertEquals(2, jo2.length());
-        assertEquals(42, jo2.get("someInt"));
-        assertEquals("Something", jo2.get("someString"));
-
-        // ensure our original jo hasn't changed.
-        assertEquals(0, jo.get("someInt"));
-        assertEquals(null, jo.opt("someString"));
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                jo, jo2
-        )));
-    }
-    
-    /**
-     * Test to validate that a generic class can be serialized as a bean.
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void testGenericBean() {
-        GenericBean bean = new GenericBean<>(42);
-        final JSONObject jo = new JSONObject(bean);
-        assertEquals(jo.keySet().toString(), 8, jo.length());
-        assertEquals(42, jo.get("genericValue"));
-        assertEquals("Expected the getter to only be called once",
-                1, bean.genericGetCounter);
-        assertEquals(0, bean.genericSetCounter);
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    /**
-     * Test to validate that a generic class can be serialized as a bean.
-     */
-    @SuppressWarnings("boxing")
-    @Test
-    public void testGenericIntBean() {
-        GenericBeanInt bean = new GenericBeanInt(42);
-        final JSONObject jo = new JSONObject(bean);
-        assertEquals(jo.keySet().toString(), 10, jo.length());
-        assertEquals(42, jo.get("genericValue"));
-        assertEquals("Expected the getter to only be called once",
-                1, bean.genericGetCounter);
-        assertEquals(0, bean.genericSetCounter);
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    /**
-     * Test to verify key limitations in the JSONObject bean serializer.
-     */
-    @Test
-    public void testWierdListBean() {
-        @SuppressWarnings("boxing")
-        WeirdList bean = new WeirdList(42, 43, 44);
-        final JSONObject jo = new JSONObject(bean);
-        // get() should have a key of 0 length
-        // get(int) should be ignored base on parameter count
-        // getInt(int) should also be ignored based on parameter count
-        // add(Integer) should be ignore as it doesn't start with get/is and also has a parameter
-        // getALL should be mapped
-        assertEquals("Expected 1 key to be mapped. Instead found: "+jo.keySet().toString(),
-                1, jo.length());
-        assertNotNull(jo.get("ALL"));
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    /**
-     * Sample test case from https://github.com/stleary/JSON-java/issues/531
-     * which verifies that no regression in double/BigDecimal support is present.
-     */
-    @Test
-    public void testObjectToBigDecimal() {  
-        double value = 1412078745.01074;  
-        Reader reader = new StringReader("[{\"value\": " + value + "}]");
-        JSONTokener tokener = new JSONTokener(reader);
-        JSONArray array = new JSONArray(tokener);
-        JSONObject jsonObject = array.getJSONObject(0);
-
-        BigDecimal current = jsonObject.getBigDecimal("value");
-        BigDecimal wantedValue = BigDecimal.valueOf(value);
-
-        assertEquals(current, wantedValue);
-        Util.checkJSONObjectMaps(jsonObject);
-        Util.checkJSONArrayMaps(array, jsonObject.getMapType());
-     }
-    
-    /**
-     * Tests the exception portions of populateMap.
-     */
-    @Test
-    public void testExceptionalBean() {
-        ExceptionalBean bean = new ExceptionalBean();
-        final JSONObject jo = new JSONObject(bean);
-        assertEquals("Expected 1 key to be mapped. Instead found: "+jo.keySet().toString(),
-                1, jo.length());
-        assertTrue(jo.get("closeable") instanceof JSONObject);
-        assertTrue(jo.getJSONObject("closeable").has("string"));
-        Util.checkJSONObjectMaps(jo);
-    }
-    
-    @Test(expected=NullPointerException.class)
-    public void testPutNullBoolean() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, false);
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullCollection() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, Collections.emptySet());
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullDouble() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, 0.0d);
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullFloat() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, 0.0f);
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullInt() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, 0);
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullLong() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, 0L);
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullMap() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, Collections.emptyMap());
-        fail("Expected an exception");
-    }
-    @Test(expected=NullPointerException.class)
-    public void testPutNullObject() {
-        // null put key 
-        JSONObject jsonObject = new JSONObject("{}");
-        jsonObject.put(null, new Object());
-        fail("Expected an exception");
-    }
-    @Test(expected=JSONException.class)
-    public void testSelfRecursiveObject() {
-        // A -> A ...
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        ObjA.setRef(ObjA);
-        new JSONObject(ObjA);
-        fail("Expected an exception");
-    }
-    @Test(expected=JSONException.class)
-    public void testLongSelfRecursiveObject() {
-        // B -> A -> A ...
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        RecursiveBean ObjB = new RecursiveBean("ObjB");
-        ObjB.setRef(ObjA);
-        ObjA.setRef(ObjA);
-        new JSONObject(ObjB);
-        fail("Expected an exception");
-    }
-    @Test(expected=JSONException.class)
-    public void testSimpleRecursiveObject() {
-        // B -> A -> B ...
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        RecursiveBean ObjB = new RecursiveBean("ObjB");
-        ObjB.setRef(ObjA);
-        ObjA.setRef(ObjB);
-        new JSONObject(ObjA);
-        fail("Expected an exception");
-    }
-    @Test(expected=JSONException.class)
-    public void testLongRecursiveObject() {
-        // D -> C -> B -> A -> D ...
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        RecursiveBean ObjB = new RecursiveBean("ObjB");
-        RecursiveBean ObjC = new RecursiveBean("ObjC");
-        RecursiveBean ObjD = new RecursiveBean("ObjD");
-        ObjC.setRef(ObjB);
-        ObjB.setRef(ObjA);
-        ObjD.setRef(ObjC);
-        ObjA.setRef(ObjD);
-        new JSONObject(ObjB);
-        fail("Expected an exception");
-    }
-    @Test(expected=JSONException.class)
-    public void testRepeatObjectRecursive() {
-        // C -> B -> A -> D -> C ...
-        //        -> D -> C ...
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        RecursiveBean ObjB = new RecursiveBean("ObjB");
-        RecursiveBean ObjC = new RecursiveBean("ObjC");
-        RecursiveBean ObjD = new RecursiveBean("ObjD");
-        ObjC.setRef(ObjB);
-        ObjB.setRef(ObjA);
-        ObjB.setRef2(ObjD);
-        ObjA.setRef(ObjD);
-        ObjD.setRef(ObjC);
-        new JSONObject(ObjC);
-        fail("Expected an exception");
-    }
-    @Test
-    public void testRepeatObjectNotRecursive() {
-        // C -> B -> A
-        //        -> A
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        RecursiveBean ObjB = new RecursiveBean("ObjB");
-        RecursiveBean ObjC = new RecursiveBean("ObjC");
-        ObjC.setRef(ObjA);
-        ObjB.setRef(ObjA);
-        ObjB.setRef2(ObjA);
-        JSONObject j0 = new JSONObject(ObjC);
-        JSONObject j1 = new JSONObject(ObjB);
-        JSONObject j2 = new JSONObject(ObjA);
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                j0, j1, j2
-        )));
-    }
-    @Test
-    public void testLongRepeatObjectNotRecursive() {
-        // C -> B -> A -> D -> E
-        //        -> D -> E
-        RecursiveBean ObjA = new RecursiveBean("ObjA");
-        RecursiveBean ObjB = new RecursiveBean("ObjB");
-        RecursiveBean ObjC = new RecursiveBean("ObjC");
-        RecursiveBean ObjD = new RecursiveBean("ObjD");
-        RecursiveBean ObjE = new RecursiveBean("ObjE");
-        ObjC.setRef(ObjB);
-        ObjB.setRef(ObjA);
-        ObjB.setRef2(ObjD);
-        ObjA.setRef(ObjD);
-        ObjD.setRef(ObjE);
-        JSONObject j0 = new JSONObject(ObjC);
-        JSONObject j1 = new JSONObject(ObjB);
-        JSONObject j2 = new JSONObject(ObjA);
-        JSONObject j3 = new JSONObject(ObjD);
-        JSONObject j4 = new JSONObject(ObjE);
-        Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(
-                j0, j1, j2, j3, j4
-        )));
-    }
-    @Test(expected=JSONException.class)
-    public void testRecursiveEquals() {
-        RecursiveBeanEquals a = new RecursiveBeanEquals("same");
-        a.setRef(a);
-        JSONObject j0 = new JSONObject(a);
-        Util.checkJSONObjectMaps(j0);
-    }
-    @Test
-    public void testNotRecursiveEquals() {
-        RecursiveBeanEquals a = new RecursiveBeanEquals("same");
-        RecursiveBeanEquals b = new RecursiveBeanEquals("same");
-        RecursiveBeanEquals c = new RecursiveBeanEquals("same");
-        a.setRef(b);
-        b.setRef(c);
-        JSONObject j0 = new JSONObject(a);
-        Util.checkJSONObjectMaps(j0);
-    }
-
-
-    @Test
-    public void testIssue548ObjectWithEmptyJsonArray() {
-        JSONObject jsonObject = new JSONObject("{\"empty_json_array\": []}");
-        assertTrue("missing expected key 'empty_json_array'", jsonObject.has("empty_json_array"));
-        assertNotNull("'empty_json_array' should be an array", jsonObject.getJSONArray("empty_json_array"));
-        assertEquals("'empty_json_array' should have a length of 0", 0, jsonObject.getJSONArray("empty_json_array").length());
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-    * Tests if calling JSONObject clear() method actually makes the JSONObject empty
-    */
-    @Test(expected = JSONException.class)
-    public void jsonObjectClearMethodTest() {
-        //Adds random stuff to the JSONObject
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.put("key1", 123);
-        jsonObject.put("key2", "456");
-        jsonObject.put("key3", new JSONObject());
-        jsonObject.clear(); //Clears the JSONObject
-        assertTrue("expected jsonObject.length() == 0", jsonObject.length() == 0); //Check if its length is 0
-        jsonObject.getInt("key1"); //Should throws org.json.JSONException: JSONObject["asd"] not found
-        Util.checkJSONObjectMaps(jsonObject);
-    }
-
-    /**
-    * Tests for stack overflow. See https://github.com/stleary/JSON-java/issues/654
-    */
-    @Test(expected = JSONException.class)
-    public void issue654StackOverflowInput() {
-        //String base64Bytes ="eyJHWiI6Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7c3t7e3t7e3vPAAAAAAAAAHt7e3t7e3t7e3t7e3t7e3t7e3t7e1ste3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e88AAAAAAAAAe3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7f3syMv//e3t7e3t7e3t7e3t7e3sx//////8=";
-        //String input = new String(java.util.Base64.getDecoder().decode(base64Bytes));
-        String input = "{\"GZ\":[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{  {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{  {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{  {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{s{{{{{{{";
-        JSONObject json_input = new JSONObject(input);
-        assertNotNull(json_input);
-        fail("Excepected Exception.");
-        Util.checkJSONObjectMaps(json_input);
-    }
-
-    /**
-    * Tests for incorrect object/array nesting. See https://github.com/stleary/JSON-java/issues/654
-    */
-    @Test(expected = JSONException.class)
-    public void issue654IncorrectNestingNoKey1() {
-        JSONObject json_input = new JSONObject("{{\"a\":0}}");
-        assertNotNull(json_input);
-        fail("Expected Exception.");
-    }
-
-    /**
-    * Tests for incorrect object/array nesting. See https://github.com/stleary/JSON-java/issues/654
-    */
-    @Test(expected = JSONException.class)
-    public void issue654IncorrectNestingNoKey2() {
-        JSONObject json_input = new JSONObject("{[\"a\"]}");
-        assertNotNull(json_input);
-        fail("Excepected Exception.");
-    }
-    
-    /**
-    * Tests for stack overflow. See https://github.com/stleary/JSON-java/issues/654
-    */
-    @Ignore("This test relies on system constraints and may not always pass. See: https://github.com/stleary/JSON-java/issues/821")
-    @Test(expected = JSONException.class)
-    public void issue654StackOverflowInputWellFormed() {
-        //String input = new String(java.util.Base64.getDecoder().decode(base64Bytes));
-        final InputStream resourceAsStream = JSONObjectTest.class.getClassLoader().getResourceAsStream("Issue654WellFormedObject.json");
-        JSONTokener tokener = new JSONTokener(resourceAsStream);
-        JSONObject json_input = new JSONObject(tokener);
-        assertNotNull(json_input);
-        fail("Excepected Exception due to stack overflow.");
-    }
-
-    @Test
-    public void testIssue682SimilarityOfJSONString() {
-        JSONObject jo1 = new JSONObject()
-                .put("a", new MyJsonString())
-                .put("b", 2);
-        JSONObject jo2 = new JSONObject()
-                .put("a", new MyJsonString())
-                .put("b", 2);
-        assertTrue(jo1.similar(jo2));
-
-        JSONObject jo3 = new JSONObject()
-                .put("a", new JSONString() {
-                    @Override
-                    public String toJSONString() {
-                        return "\"different value\"";
-                    }
-                })
-                .put("b", 2);
-        assertFalse(jo1.similar(jo3));
-    }
-
-    private static final Number[] NON_FINITE_NUMBERS = { Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN,
-            Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN };
-
-    @Test
-    public void issue713MapConstructorWithNonFiniteNumbers() {
-        for (Number nonFinite : NON_FINITE_NUMBERS) {
-            Map map = new HashMap<>();
-            map.put("a", nonFinite);
-
-            assertThrows(JSONException.class, () -> new JSONObject(map));
-        }
-    }
-
-    @Test
-    public void issue713BeanConstructorWithNonFiniteNumbers() {
-        for (Number nonFinite : NON_FINITE_NUMBERS) {
-            GenericBean bean = new GenericBean<>(nonFinite);
-            assertThrows(JSONException.class, () -> new JSONObject(bean));
-        }
-    }
-
-    @Test(expected = JSONException.class)
-    public void issue743SerializationMap() {
-      HashMap map = new HashMap<>();
-      map.put("t", map);
-      JSONObject object = new JSONObject(map);
-      String jsonString = object.toString();
-    }
-
-    @Test(expected = JSONException.class)
-    public void testCircularReferenceMultipleLevel() {
-      HashMap inside = new HashMap<>();
-      HashMap jsonObject = new HashMap<>();
-      inside.put("inside", jsonObject);
-      jsonObject.put("test", inside);
-      new JSONObject(jsonObject);
-    }
-
-    @Test
-    public void issue743SerializationMapWith512Objects() {
-        HashMap map  = buildNestedMap(ParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH);
-        JSONObject object = new JSONObject(map);
-        String jsonString = object.toString();
-    }
-
-    @Test
-    public void issue743SerializationMapWith1000Objects() {
-      HashMap map  = buildNestedMap(1000);
-      JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(1000);
-      JSONObject object = new JSONObject(map, parserConfiguration);
-      String jsonString = object.toString();
-    }
-
-    @Test(expected = JSONException.class)
-    public void issue743SerializationMapWith1001Objects() {
-        HashMap map  = buildNestedMap(1001);
-        JSONObject object = new JSONObject(map);
-        String jsonString = object.toString();
-    }
-
-    @Test(expected = JSONException.class)
-    public void testCircleReferenceFirstLevel() {
-        Map jsonObject = new HashMap<>();
-
-        jsonObject.put("test", jsonObject);
-
-        new JSONObject(jsonObject, new JSONParserConfiguration());
-    }
-
-    @Test(expected = StackOverflowError.class)
-    public void testCircleReferenceMultiplyLevel_notConfigured_expectedStackOverflow() {
-        Map inside = new HashMap<>();
-
-        Map jsonObject = new HashMap<>();
-        inside.put("test", jsonObject);
-        jsonObject.put("test", inside);
-
-        new JSONObject(jsonObject, new JSONParserConfiguration().withMaxNestingDepth(99999));
-    }
-
-    @Test(expected = JSONException.class)
-    public void testCircleReferenceMultiplyLevel_configured_expectedJSONException() {
-        Map inside = new HashMap<>();
-
-        Map jsonObject = new HashMap<>();
-        inside.put("test", jsonObject);
-        jsonObject.put("test", inside);
-
-        new JSONObject(jsonObject, new JSONParserConfiguration());
-    }
-
-    @Test
-    public void testDifferentKeySameInstanceNotACircleReference() {
-        Map map1 = new HashMap<>();
-        Map map2 = new HashMap<>();
-
-        map1.put("test1", map2);
-        map1.put("test2", map2);
-
-        new JSONObject(map1);
-    }
-
-    @Test
-    public void clarifyCurrentBehavior() {
-        // Behavior documented in #653 optLong vs getLong inconsistencies
-        // This problem still exists.
-        // Internally, both number_1 and number_2 are stored as strings. This is reasonable since they are parsed as strings.
-        // However, getLong and optLong should return similar results
-        JSONObject json = new JSONObject("{\"number_1\":\"01234\", \"number_2\": \"332211\"}");
-        assertEquals(json.getLong("number_1"), 1234L);
-        assertEquals(json.optLong("number_1"), 0); //THIS VALUE IS NOT RETURNED AS A NUMBER
-        assertEquals(json.getLong("number_2"), 332211L);
-        assertEquals(json.optLong("number_2"), 332211L);
-
-        // Behavior documented in #826 JSONObject parsing 0-led numeric strings as ints
-        // After reverting the code, personId is stored as a string, and the behavior is as expected
-        String personId = "\"0123\"";
-        JSONObject j1 = new JSONObject("{\"personId\": " + personId + "}");
-        assertEquals(j1.getString("personId"), "0123");
-
-        // Also #826. Here is input with missing quotes. Because of the leading zero, it should not be parsed as a number.
-        // This example was mentioned in the same ticket
-        // After reverting the code, personId is stored as a string, and the behavior is as expected
-        JSONObject j2 = new JSONObject("{\"personId\":\"0123\"}");
-        assertEquals(j2.getString("personId"), "0123");
-
-        // Behavior uncovered while working on the code
-        // All of the values are stored as strings except for hex4, which is stored as a number. This is probably incorrect
-        JSONObject j3 = new JSONObject("{ " +
-                "\"hex1\": \"010e4\", \"hex2\": \"00f0\", \"hex3\": \"0011\", " +
-                "\"hex4\": 00e0, \"hex5\": \"00f0\", \"hex6\": \"0011\" }");
-        assertEquals(j3.getString("hex1"), "010e4");
-        assertEquals(j3.getString("hex2"), "00f0");
-        assertEquals(j3.getString("hex3"), "0011");
-        assertEquals(j3.getLong("hex4"), 0, .1);
-        assertEquals(j3.getString("hex5"), "00f0");
-        assertEquals(j3.getString("hex6"), "0011");
-    }
-
-
-    @Test
-    public void testStrictModeJSONTokener_expectException(){
-        JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
-        JSONTokener tokener = new JSONTokener("{\"key\":\"value\"}invalidCharacters", jsonParserConfiguration);
-
-        assertThrows(JSONException.class, () -> { new JSONObject(tokener); });
-    }
-
-    /**
-     * Method to build nested map of max maxDepth
-     *
-     * @param maxDepth
-     * @return
-     */
-    public static HashMap buildNestedMap(int maxDepth) {
-        if (maxDepth <= 0) {
-            return new HashMap<>();
-        }
-        HashMap nestedMap = new HashMap<>();
-        nestedMap.put("t", buildNestedMap(maxDepth - 1));
-        return nestedMap;
-    }
+		try {
+			value = jsonObjectNull.get("key");
+			fail("get() null should throw exception");
+		} catch (Exception ignored) {
+		}
+
+		/**
+		 * XML.toString() then goes on to do something with the value if the key val is
+		 * "content", then value.toString() will be called. This will evaluate to "null"
+		 * for JSONObject.NULL, and the empty string for null. But if the key is
+		 * anything else, then JSONObject.NULL will be emitted as null and
+		 * null will be emitted as ""
+		 */
+		String sJONull = XML.toString(jsonObjectJONull);
+		assertTrue("JSONObject.NULL should emit a null value", "null".equals(sJONull));
+		String sNull = XML.toString(jsonObjectNull);
+		assertTrue("null should emit an empty string", "".equals(sNull));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jsonObjectJONull, jsonObjectNull)));
+	}
+
+	@Test(expected = JSONPointerException.class)
+	public void queryWithNoResult() {
+		new JSONObject().query("/a/b");
+	}
+
+	@Test
+	public void optQueryWithNoResult() {
+		assertNull(new JSONObject().optQuery("/a/b"));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void optQueryWithSyntaxError() {
+		new JSONObject().optQuery("invalid");
+	}
+
+	@Test(expected = JSONException.class)
+	public void invalidEscapeSequence() {
+		String json = "{ \"\\url\": \"value\" }";
+		assertNull("Expected an exception", new JSONObject(json));
+	}
+
+	/**
+	 * Exercise JSONObject toMap() method.
+	 */
+	@Test
+	public void toMap() {
+		String jsonObjectStr = "{" + "\"key1\":" + "[1,2," + "{\"key3\":true}" + "]," + "\"key2\":"
+				+ "{\"key1\":\"val1\",\"key2\":" + "{\"key2\":null}," + "\"key3\":42" + "}," + "\"key3\":" + "["
+				+ "[\"value1\",2.1]" + "," + "[null]" + "]" + "}";
+
+		JSONObject jsonObject = new JSONObject(jsonObjectStr);
+		Map map = jsonObject.toMap();
+
+		assertTrue("Map should not be null", map != null);
+		assertTrue("Map should have 3 elements", map.size() == 3);
+
+		List key1List = (List) map.get("key1");
+		assertTrue("key1 should not be null", key1List != null);
+		assertTrue("key1 list should have 3 elements", key1List.size() == 3);
+		assertTrue("key1 value 1 should be 1", key1List.get(0).equals(Integer.valueOf(1)));
+		assertTrue("key1 value 2 should be 2", key1List.get(1).equals(Integer.valueOf(2)));
+
+		Map key1Value3Map = (Map) key1List.get(2);
+		assertTrue("Map should not be null", key1Value3Map != null);
+		assertTrue("Map should have 1 element", key1Value3Map.size() == 1);
+		assertTrue("Map key3 should be true", key1Value3Map.get("key3").equals(Boolean.TRUE));
+
+		Map key2Map = (Map) map.get("key2");
+		assertTrue("key2 should not be null", key2Map != null);
+		assertTrue("key2 map should have 3 elements", key2Map.size() == 3);
+		assertTrue("key2 map key 1 should be val1", key2Map.get("key1").equals("val1"));
+		assertTrue("key2 map key 3 should be 42", key2Map.get("key3").equals(Integer.valueOf(42)));
+
+		Map key2Val2Map = (Map) key2Map.get("key2");
+		assertTrue("key2 map key 2 should not be null", key2Val2Map != null);
+		assertTrue("key2 map key 2 should have an entry", key2Val2Map.containsKey("key2"));
+		assertTrue("key2 map key 2 value should be null", key2Val2Map.get("key2") == null);
+
+		List key3List = (List) map.get("key3");
+		assertTrue("key3 should not be null", key3List != null);
+		assertTrue("key3 list should have 3 elements", key3List.size() == 2);
+
+		List key3Val1List = (List) key3List.get(0);
+		assertTrue("key3 list val 1 should not be null", key3Val1List != null);
+		assertTrue("key3 list val 1 should have 2 elements", key3Val1List.size() == 2);
+		assertTrue("key3 list val 1 list element 1 should be value1", key3Val1List.get(0).equals("value1"));
+		assertTrue("key3 list val 1 list element 2 should be 2.1", key3Val1List.get(1).equals(new BigDecimal("2.1")));
+
+		List key3Val2List = (List) key3List.get(1);
+		assertTrue("key3 list val 2 should not be null", key3Val2List != null);
+		assertTrue("key3 list val 2 should have 1 element", key3Val2List.size() == 1);
+		assertTrue("key3 list val 2 list element 1 should be null", key3Val2List.get(0) == null);
+
+		// Assert that toMap() is a deep copy
+		jsonObject.getJSONArray("key3").getJSONArray(0).put(0, "still value 1");
+		assertTrue("key3 list val 1 list element 1 should be value1", key3Val1List.get(0).equals("value1"));
+
+		// assert that the new map is mutable
+		assertTrue("Removing a key should succeed", map.remove("key3") != null);
+		assertTrue("Map should have 2 elements", map.size() == 2);
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * test that validates a singleton can be serialized as a bean.
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void testSingletonBean() {
+		final JSONObject jo = new JSONObject(Singleton.getInstance());
+		assertEquals(jo.keySet().toString(), 1, jo.length());
+		assertEquals(0, jo.get("someInt"));
+		assertEquals(null, jo.opt("someString"));
+
+		// Update the singleton values
+		Singleton.getInstance().setSomeInt(42);
+		Singleton.getInstance().setSomeString("Something");
+		final JSONObject jo2 = new JSONObject(Singleton.getInstance());
+		assertEquals(2, jo2.length());
+		assertEquals(42, jo2.get("someInt"));
+		assertEquals("Something", jo2.get("someString"));
+
+		// ensure our original jo hasn't changed.
+		assertEquals(0, jo.get("someInt"));
+		assertEquals(null, jo.opt("someString"));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jo, jo2)));
+	}
+
+	/**
+	 * test that validates a singleton can be serialized as a bean.
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void testSingletonEnumBean() {
+		final JSONObject jo = new JSONObject(SingletonEnum.getInstance());
+		assertEquals(jo.keySet().toString(), 1, jo.length());
+		assertEquals(0, jo.get("someInt"));
+		assertEquals(null, jo.opt("someString"));
+
+		// Update the singleton values
+		SingletonEnum.getInstance().setSomeInt(42);
+		SingletonEnum.getInstance().setSomeString("Something");
+		final JSONObject jo2 = new JSONObject(SingletonEnum.getInstance());
+		assertEquals(2, jo2.length());
+		assertEquals(42, jo2.get("someInt"));
+		assertEquals("Something", jo2.get("someString"));
+
+		// ensure our original jo hasn't changed.
+		assertEquals(0, jo.get("someInt"));
+		assertEquals(null, jo.opt("someString"));
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(jo, jo2)));
+	}
+
+	/**
+	 * Test to validate that a generic class can be serialized as a bean.
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void testGenericBean() {
+		GenericBean bean = new GenericBean<>(42);
+		final JSONObject jo = new JSONObject(bean);
+		assertEquals(jo.keySet().toString(), 8, jo.length());
+		assertEquals(42, jo.get("genericValue"));
+		assertEquals("Expected the getter to only be called once", 1, bean.genericGetCounter);
+		assertEquals(0, bean.genericSetCounter);
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Test to validate that a generic class can be serialized as a bean.
+	 */
+	@SuppressWarnings("boxing")
+	@Test
+	public void testGenericIntBean() {
+		GenericBeanInt bean = new GenericBeanInt(42);
+		final JSONObject jo = new JSONObject(bean);
+		assertEquals(jo.keySet().toString(), 10, jo.length());
+		assertEquals(42, jo.get("genericValue"));
+		assertEquals("Expected the getter to only be called once", 1, bean.genericGetCounter);
+		assertEquals(0, bean.genericSetCounter);
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Test to verify key limitations in the JSONObject bean
+	 * serializer.
+	 */
+	@Test
+	public void testWierdListBean() {
+		@SuppressWarnings("boxing")
+		WeirdList bean = new WeirdList(42, 43, 44);
+		final JSONObject jo = new JSONObject(bean);
+		// get() should have a key of 0 length
+		// get(int) should be ignored base on parameter count
+		// getInt(int) should also be ignored based on parameter count
+		// add(Integer) should be ignore as it doesn't start with get/is and also has a
+		// parameter
+		// getALL should be mapped
+		assertEquals("Expected 1 key to be mapped. Instead found: " + jo.keySet().toString(), 1, jo.length());
+		assertNotNull(jo.get("ALL"));
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	/**
+	 * Sample test case from https://github.com/stleary/JSON-java/issues/531 which
+	 * verifies that no regression in double/BigDecimal support is present.
+	 */
+	@Test
+	public void testObjectToBigDecimal() {
+		double value = 1412078745.01074;
+		Reader reader = new StringReader("[{\"value\": " + value + "}]");
+		JSONTokener tokener = new JSONTokener(reader);
+		JSONArray array = new JSONArray(tokener);
+		JSONObject jsonObject = array.getJSONObject(0);
+
+		BigDecimal current = jsonObject.getBigDecimal("value");
+		BigDecimal wantedValue = BigDecimal.valueOf(value);
+
+		assertEquals(current, wantedValue);
+		Util.checkJSONObjectMaps(jsonObject);
+		Util.checkJSONArrayMaps(array, jsonObject.getMapType());
+	}
+
+	/**
+	 * Tests the exception portions of populateMap.
+	 */
+	@Test
+	public void testExceptionalBean() {
+		ExceptionalBean bean = new ExceptionalBean();
+		final JSONObject jo = new JSONObject(bean);
+		assertEquals("Expected 1 key to be mapped. Instead found: " + jo.keySet().toString(), 1, jo.length());
+		assertTrue(jo.get("closeable") instanceof JSONObject);
+		assertTrue(jo.getJSONObject("closeable").has("string"));
+		Util.checkJSONObjectMaps(jo);
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullBoolean() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, false);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullCollection() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, Collections.emptySet());
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullDouble() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, 0.0d);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullFloat() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, 0.0f);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullInt() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, 0);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullLong() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, 0L);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullMap() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, Collections.emptyMap());
+		fail("Expected an exception");
+	}
+
+	@Test(expected = NullPointerException.class)
+	public void testPutNullObject() {
+		// null put key
+		JSONObject jsonObject = new JSONObject("{}");
+		jsonObject.put(null, new Object());
+		fail("Expected an exception");
+	}
+
+	@Test(expected = JSONException.class)
+	public void testSelfRecursiveObject() {
+		// A -> A ...
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		ObjA.setRef(ObjA);
+		new JSONObject(ObjA);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = JSONException.class)
+	public void testLongSelfRecursiveObject() {
+		// B -> A -> A ...
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		RecursiveBean ObjB = new RecursiveBean("ObjB");
+		ObjB.setRef(ObjA);
+		ObjA.setRef(ObjA);
+		new JSONObject(ObjB);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = JSONException.class)
+	public void testSimpleRecursiveObject() {
+		// B -> A -> B ...
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		RecursiveBean ObjB = new RecursiveBean("ObjB");
+		ObjB.setRef(ObjA);
+		ObjA.setRef(ObjB);
+		new JSONObject(ObjA);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = JSONException.class)
+	public void testLongRecursiveObject() {
+		// D -> C -> B -> A -> D ...
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		RecursiveBean ObjB = new RecursiveBean("ObjB");
+		RecursiveBean ObjC = new RecursiveBean("ObjC");
+		RecursiveBean ObjD = new RecursiveBean("ObjD");
+		ObjC.setRef(ObjB);
+		ObjB.setRef(ObjA);
+		ObjD.setRef(ObjC);
+		ObjA.setRef(ObjD);
+		new JSONObject(ObjB);
+		fail("Expected an exception");
+	}
+
+	@Test(expected = JSONException.class)
+	public void testRepeatObjectRecursive() {
+		// C -> B -> A -> D -> C ...
+		// -> D -> C ...
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		RecursiveBean ObjB = new RecursiveBean("ObjB");
+		RecursiveBean ObjC = new RecursiveBean("ObjC");
+		RecursiveBean ObjD = new RecursiveBean("ObjD");
+		ObjC.setRef(ObjB);
+		ObjB.setRef(ObjA);
+		ObjB.setRef2(ObjD);
+		ObjA.setRef(ObjD);
+		ObjD.setRef(ObjC);
+		new JSONObject(ObjC);
+		fail("Expected an exception");
+	}
+
+	@Test
+	public void testRepeatObjectNotRecursive() {
+		// C -> B -> A
+		// -> A
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		RecursiveBean ObjB = new RecursiveBean("ObjB");
+		RecursiveBean ObjC = new RecursiveBean("ObjC");
+		ObjC.setRef(ObjA);
+		ObjB.setRef(ObjA);
+		ObjB.setRef2(ObjA);
+		JSONObject j0 = new JSONObject(ObjC);
+		JSONObject j1 = new JSONObject(ObjB);
+		JSONObject j2 = new JSONObject(ObjA);
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(j0, j1, j2)));
+	}
+
+	@Test
+	public void testLongRepeatObjectNotRecursive() {
+		// C -> B -> A -> D -> E
+		// -> D -> E
+		RecursiveBean ObjA = new RecursiveBean("ObjA");
+		RecursiveBean ObjB = new RecursiveBean("ObjB");
+		RecursiveBean ObjC = new RecursiveBean("ObjC");
+		RecursiveBean ObjD = new RecursiveBean("ObjD");
+		RecursiveBean ObjE = new RecursiveBean("ObjE");
+		ObjC.setRef(ObjB);
+		ObjB.setRef(ObjA);
+		ObjB.setRef2(ObjD);
+		ObjA.setRef(ObjD);
+		ObjD.setRef(ObjE);
+		JSONObject j0 = new JSONObject(ObjC);
+		JSONObject j1 = new JSONObject(ObjB);
+		JSONObject j2 = new JSONObject(ObjA);
+		JSONObject j3 = new JSONObject(ObjD);
+		JSONObject j4 = new JSONObject(ObjE);
+		Util.checkJSONObjectsMaps(new ArrayList(Arrays.asList(j0, j1, j2, j3, j4)));
+	}
+
+	@Test(expected = JSONException.class)
+	public void testRecursiveEquals() {
+		RecursiveBeanEquals a = new RecursiveBeanEquals("same");
+		a.setRef(a);
+		JSONObject j0 = new JSONObject(a);
+		Util.checkJSONObjectMaps(j0);
+	}
+
+	@Test
+	public void testNotRecursiveEquals() {
+		RecursiveBeanEquals a = new RecursiveBeanEquals("same");
+		RecursiveBeanEquals b = new RecursiveBeanEquals("same");
+		RecursiveBeanEquals c = new RecursiveBeanEquals("same");
+		a.setRef(b);
+		b.setRef(c);
+		JSONObject j0 = new JSONObject(a);
+		Util.checkJSONObjectMaps(j0);
+	}
+
+	@Test
+	public void testIssue548ObjectWithEmptyJsonArray() {
+		JSONObject jsonObject = new JSONObject("{\"empty_json_array\": []}");
+		assertTrue("missing expected key 'empty_json_array'", jsonObject.has("empty_json_array"));
+		assertNotNull("'empty_json_array' should be an array", jsonObject.getJSONArray("empty_json_array"));
+		assertEquals("'empty_json_array' should have a length of 0", 0,
+				jsonObject.getJSONArray("empty_json_array").length());
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Tests if calling JSONObject clear() method actually makes the JSONObject
+	 * empty
+	 */
+	@Test(expected = JSONException.class)
+	public void jsonObjectClearMethodTest() {
+		// Adds random stuff to the JSONObject
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.put("key1", 123);
+		jsonObject.put("key2", "456");
+		jsonObject.put("key3", new JSONObject());
+		jsonObject.clear(); // Clears the JSONObject
+		assertTrue("expected jsonObject.length() == 0", jsonObject.length() == 0); // Check if its length is 0
+		jsonObject.getInt("key1"); // Should throws org.json.JSONException: JSONObject["asd"] not found
+		Util.checkJSONObjectMaps(jsonObject);
+	}
+
+	/**
+	 * Tests for stack overflow. See https://github.com/stleary/JSON-java/issues/654
+	 */
+	@Test(expected = JSONException.class)
+	public void issue654StackOverflowInput() {
+		// String base64Bytes
+		// ="eyJHWiI6Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMCkwLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sJe3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTApMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7ewl7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7c3t7e3t7e3vPAAAAAAAAAHt7e3t7e3t7e3t7e3t7e3t7e3t7e1ste3t7e3t7e3t7e3t7e3t7e3t7e3t7CXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3tbLTAtMCx7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e1stMC0wLHt7e3t7e3t7e3t7e3t7e3t7e88AAAAAAAAAe3t7e3t7e3t7e3t7e3t7e3t7e3t7Wy0wLTAse3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7f3syMv//e3t7e3t7e3t7e3t7e3sx//////8=";
+		// String input = new String(java.util.Base64.getDecoder().decode(base64Bytes));
+		String input = "{\"GZ\":[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{  {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{  {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{  {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{    {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0)0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{   {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{{{{{{{{{{{{{{{{{{{{[-0-0,{{{{{{{{{{s{{{{{{{";
+		JSONObject json_input = new JSONObject(input);
+		assertNotNull(json_input);
+		fail("Excepected Exception.");
+		Util.checkJSONObjectMaps(json_input);
+	}
+
+	/**
+	 * Tests for incorrect object/array nesting. See
+	 * https://github.com/stleary/JSON-java/issues/654
+	 */
+	@Test(expected = JSONException.class)
+	public void issue654IncorrectNestingNoKey1() {
+		JSONObject json_input = new JSONObject("{{\"a\":0}}");
+		assertNotNull(json_input);
+		fail("Expected Exception.");
+	}
+
+	/**
+	 * Tests for incorrect object/array nesting. See
+	 * https://github.com/stleary/JSON-java/issues/654
+	 */
+	@Test(expected = JSONException.class)
+	public void issue654IncorrectNestingNoKey2() {
+		JSONObject json_input = new JSONObject("{[\"a\"]}");
+		assertNotNull(json_input);
+		fail("Excepected Exception.");
+	}
+
+	/**
+	 * Tests for stack overflow. See https://github.com/stleary/JSON-java/issues/654
+	 */
+	@Ignore("This test relies on system constraints and may not always pass. See: https://github.com/stleary/JSON-java/issues/821")
+	@Test(expected = JSONException.class)
+	public void issue654StackOverflowInputWellFormed() {
+		// String input = new String(java.util.Base64.getDecoder().decode(base64Bytes));
+		final InputStream resourceAsStream = JSONObjectTest.class.getClassLoader()
+				.getResourceAsStream("Issue654WellFormedObject.json");
+		JSONTokener tokener = new JSONTokener(resourceAsStream);
+		JSONObject json_input = new JSONObject(tokener);
+		assertNotNull(json_input);
+		fail("Excepected Exception due to stack overflow.");
+	}
+
+	@Test
+	public void testIssue682SimilarityOfJSONString() {
+		JSONObject jo1 = new JSONObject().put("a", new MyJsonString()).put("b", 2);
+		JSONObject jo2 = new JSONObject().put("a", new MyJsonString()).put("b", 2);
+		assertTrue(jo1.similar(jo2));
+
+		JSONObject jo3 = new JSONObject().put("a", new JSONString() {
+			@Override
+			public String toJSONString() {
+				return "\"different value\"";
+			}
+		}).put("b", 2);
+		assertFalse(jo1.similar(jo3));
+	}
+
+	private static final Number[] NON_FINITE_NUMBERS = { Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN,
+			Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN };
+
+	@Test
+	public void issue713MapConstructorWithNonFiniteNumbers() {
+		for (Number nonFinite : NON_FINITE_NUMBERS) {
+			Map map = new HashMap<>();
+			map.put("a", nonFinite);
+
+			assertThrows(JSONException.class, () -> new JSONObject(map));
+		}
+	}
+
+	@Test
+	public void issue713BeanConstructorWithNonFiniteNumbers() {
+		for (Number nonFinite : NON_FINITE_NUMBERS) {
+			GenericBean bean = new GenericBean<>(nonFinite);
+			assertThrows(JSONException.class, () -> new JSONObject(bean));
+		}
+	}
+
+	@Test(expected = JSONException.class)
+	public void issue743SerializationMap() {
+		HashMap map = new HashMap<>();
+		map.put("t", map);
+		JSONObject object = new JSONObject(map);
+		String jsonString = object.toString();
+	}
+
+	@Test(expected = JSONException.class)
+	public void testCircularReferenceMultipleLevel() {
+		HashMap inside = new HashMap<>();
+		HashMap jsonObject = new HashMap<>();
+		inside.put("inside", jsonObject);
+		jsonObject.put("test", inside);
+		new JSONObject(jsonObject);
+	}
+
+	@Test
+	public void issue743SerializationMapWith512Objects() {
+		HashMap map = buildNestedMap(ParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH);
+		JSONObject object = new JSONObject(map);
+		String jsonString = object.toString();
+	}
+
+	@Test
+	public void issue743SerializationMapWith500Objects() {
+		// TODO: find out why 1000 objects no longer works
+		HashMap map = buildNestedMap(500);
+		JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(500);
+		JSONObject object = new JSONObject(map, parserConfiguration);
+		String jsonString = object.toString();
+	}
+
+	@Test(expected = JSONException.class)
+	public void issue743SerializationMapWith1001Objects() {
+		HashMap map = buildNestedMap(1001);
+		JSONObject object = new JSONObject(map);
+		String jsonString = object.toString();
+	}
+
+	@Test(expected = JSONException.class)
+	public void testCircleReferenceFirstLevel() {
+		Map jsonObject = new HashMap<>();
+
+		jsonObject.put("test", jsonObject);
+
+		new JSONObject(jsonObject, new JSONParserConfiguration());
+	}
+
+	@Test(expected = StackOverflowError.class)
+	public void testCircleReferenceMultiplyLevel_notConfigured_expectedStackOverflow() {
+		Map inside = new HashMap<>();
+
+		Map jsonObject = new HashMap<>();
+		inside.put("test", jsonObject);
+		jsonObject.put("test", inside);
+
+		new JSONObject(jsonObject, new JSONParserConfiguration().withMaxNestingDepth(99999));
+	}
+
+	@Test(expected = JSONException.class)
+	public void testCircleReferenceMultiplyLevel_configured_expectedJSONException() {
+		Map inside = new HashMap<>();
+
+		Map jsonObject = new HashMap<>();
+		inside.put("test", jsonObject);
+		jsonObject.put("test", inside);
+
+		new JSONObject(jsonObject, new JSONParserConfiguration());
+	}
+
+	@Test
+	public void testDifferentKeySameInstanceNotACircleReference() {
+		Map map1 = new HashMap<>();
+		Map map2 = new HashMap<>();
+
+		map1.put("test1", map2);
+		map1.put("test2", map2);
+
+		new JSONObject(map1);
+	}
+
+	@Test
+	public void clarifyCurrentBehavior() {
+		// Behavior documented in #653 optLong vs getLong inconsistencies
+		// This problem still exists.
+		// Internally, both number_1 and number_2 are stored as strings. This is
+		// reasonable since they are parsed as strings.
+		// However, getLong and optLong should return similar results
+		JSONObject json = new JSONObject("{\"number_1\":\"01234\", \"number_2\": \"332211\"}");
+		assertEquals(json.getLong("number_1"), 1234L);
+		assertEquals(json.optLong("number_1"), 0); // THIS VALUE IS NOT RETURNED AS A NUMBER
+		assertEquals(json.getLong("number_2"), 332211L);
+		assertEquals(json.optLong("number_2"), 332211L);
+
+		// Behavior documented in #826 JSONObject parsing 0-led numeric strings as ints
+		// After reverting the code, personId is stored as a string, and the behavior is
+		// as expected
+		String personId = "\"0123\"";
+		JSONObject j1 = new JSONObject("{\"personId\": " + personId + "}");
+		assertEquals(j1.getString("personId"), "0123");
+
+		// Also #826. Here is input with missing quotes. Because of the leading zero, it
+		// should not be parsed as a number.
+		// This example was mentioned in the same ticket
+		// After reverting the code, personId is stored as a string, and the behavior is
+		// as expected
+		JSONObject j2 = new JSONObject("{\"personId\":\"0123\"}");
+		assertEquals(j2.getString("personId"), "0123");
+
+		// Behavior uncovered while working on the code
+		// All of the values are stored as strings except for hex4, which is stored as a
+		// number. This is probably incorrect
+		JSONObject j3 = new JSONObject("{ " + "\"hex1\": \"010e4\", \"hex2\": \"00f0\", \"hex3\": \"0011\", "
+				+ "\"hex4\": 00e0, \"hex5\": \"00f0\", \"hex6\": \"0011\" }");
+		assertEquals(j3.getString("hex1"), "010e4");
+		assertEquals(j3.getString("hex2"), "00f0");
+		assertEquals(j3.getString("hex3"), "0011");
+		assertEquals(j3.getLong("hex4"), 0, .1);
+		assertEquals(j3.getString("hex5"), "00f0");
+		assertEquals(j3.getString("hex6"), "0011");
+	}
+
+	@Test
+	public void testStrictModeJSONTokener_expectException() {
+		JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+		JSONTokener tokener = new JSONTokener("{\"key\":\"value\"}invalidCharacters", jsonParserConfiguration);
+
+		assertThrows(JSONException.class, () -> {
+			new JSONObject(tokener);
+		});
+	}
+
+	@Test
+	public void test_strictModeWithMisCasedBooleanOrNullValue() {
+		JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+		try {
+			new JSONObject("{\"a\":True}", jsonParserConfiguration);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			// No action, expected outcome
+		}
+		try {
+			new JSONObject("{\"a\":TRUE}", jsonParserConfiguration);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			// No action, expected outcome
+		}
+		try {
+			new JSONObject("{\"a\":nUlL}", jsonParserConfiguration);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			// No action, expected outcome
+		}
+	}
+
+	@Test
+	public void test_strictModeWithInappropriateKey() {
+		JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+
+		// Parsing the following objects should fail
+		try {
+			new JSONObject("{true : 3}", jsonParserConfiguration);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			// No action, expected outcome
+		}
+		try {
+			new JSONObject("{TRUE : 3}", jsonParserConfiguration);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			// No action, expected outcome
+		}
+		try {
+			new JSONObject("{1 : 3}", jsonParserConfiguration);
+			fail("Expected an exception");
+		} catch (JSONException e) {
+			// No action, expected outcome
+		}
+
+	}
+
+	/**
+	 * Method to build nested map of max maxDepth
+	 *
+	 * @param maxDepth
+	 * @return
+	 */
+	public static HashMap buildNestedMap(int maxDepth) {
+		if (maxDepth <= 0) {
+			return new HashMap<>();
+		}
+		HashMap nestedMap = new HashMap<>();
+		nestedMap.put("t", buildNestedMap(maxDepth - 1));
+		return nestedMap;
+	}
+
+	/**
+	 * Tests the behavior of the {@link JSONObject} when parsing a bean with null
+	 * fields using a custom {@link JSONParserConfiguration} that enables the use of
+	 * native nulls.
+	 * 
+	 * 

+ * This test ensures that uninitialized fields in the bean are serialized + * correctly into the resulting JSON object, and their keys are present in the + * JSON string output. + *

+ */ + @Test + public void jsonObjectParseNullFieldsWithParserConfiguration() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + RecursiveBean bean = new RecursiveBean(null); + JSONObject jsonObject = new JSONObject(bean, jsonParserConfiguration.withUseNativeNulls(true)); + assertTrue("name key should be present", jsonObject.has("name")); + assertTrue("ref key should be present", jsonObject.has("ref")); + assertTrue("ref2 key should be present", jsonObject.has("ref2")); + } + + /** + * Tests the behavior of the {@link JSONObject} when parsing a bean with null + * fields without using a custom {@link JSONParserConfiguration}. + * + *

+ * This test ensures that uninitialized fields in the bean are not serialized + * into the resulting JSON object, and the object remains empty. + *

+ */ + @Test + public void jsonObjectParseNullFieldsWithoutParserConfiguration() { + RecursiveBean bean = new RecursiveBean(null); + JSONObject jsonObject = new JSONObject(bean); + assertTrue("JSONObject should be empty", jsonObject.isEmpty()); + } + + @Test + public void jsonObjectParseFromJson_0() { + JSONObject object = new JSONObject(); + object.put("number", 12); + object.put("name", "Alex"); + object.put("longNumber", 1500000000L); + CustomClass customClass = object.fromJson(CustomClass.class); + CustomClass compareClass = new CustomClass(12, "Alex", 1500000000L); + assertEquals(customClass, compareClass); + } + + @Test + public void jsonObjectParseFromJson_1() { + JSONObject object = new JSONObject(); + + BigInteger largeInt = new BigInteger("123"); + object.put("largeInt", largeInt.toString()); + CustomClassA customClassA = object.fromJson(CustomClassA.class); + CustomClassA compareClassClassA = new CustomClassA(largeInt); + assertEquals(customClassA, compareClassClassA); + } + + @Test + public void jsonObjectParseFromJson_2() { + JSONObject object = new JSONObject(); + object.put("number", 12); + + JSONObject classC = new JSONObject(); + classC.put("stringName", "Alex"); + classC.put("longNumber", 123456L); + + object.put("classC", classC); + + CustomClassB customClassB = object.fromJson(CustomClassB.class); + CustomClassC classCObject = new CustomClassC("Alex", 123456L); + CustomClassB compareClassB = new CustomClassB(12, classCObject); + assertEquals(customClassB, compareClassB); + } + + @Test + public void jsonObjectParseFromJson_3() { + JSONObject object = new JSONObject(); + JSONArray array = new JSONArray(); + array.put("test1"); + array.put("test2"); + array.put("test3"); + object.put("stringList", array); + + CustomClassD customClassD = object.fromJson(CustomClassD.class); + CustomClassD compareClassD = new CustomClassD(Arrays.asList("test1", "test2", "test3")); + assertEquals(customClassD, compareClassD); + } + + @Test + public void jsonObjectParseFromJson_4() { + JSONObject object = new JSONObject(); + JSONArray array = new JSONArray(); + array.put(new CustomClassC("test1", 1L).toJSON()); + array.put(new CustomClassC("test2", 2L).toJSON()); + object.put("listClassC", array); + + CustomClassE customClassE = object.fromJson(CustomClassE.class); + CustomClassE compareClassE = new CustomClassE( + java.util.Arrays.asList(new CustomClassC("test1", 1L), new CustomClassC("test2", 2L))); + assertEquals(customClassE, compareClassE); + } + + @Test + public void jsonObjectParseFromJson_5() { + JSONObject object = new JSONObject(); + JSONArray array = new JSONArray(); + array.put(Arrays.asList("A", "B", "C")); + array.put(Arrays.asList("D", "E")); + object.put("listOfString", array); + + CustomClassF customClassF = object.fromJson(CustomClassF.class); + List> listOfString = new ArrayList<>(); + listOfString.add(Arrays.asList("A", "B", "C")); + listOfString.add(Arrays.asList("D", "E")); + CustomClassF compareClassF = new CustomClassF(listOfString); + assertEquals(customClassF, compareClassF); + } + + @Test + public void jsonObjectParseFromJson_6() { + JSONObject object = new JSONObject(); + Map dataList = new HashMap<>(); + dataList.put("A", "Aa"); + dataList.put("B", "Bb"); + dataList.put("C", "Cc"); + object.put("dataList", dataList); + + CustomClassG customClassG = object.fromJson(CustomClassG.class); + CustomClassG compareClassG = new CustomClassG(dataList); + assertEquals(customClassG, compareClassG); + } + + @Test + public void jsonObjectParseFromJson_7() { + JSONObject object = new JSONObject(); + Map> dataList = new HashMap<>(); + dataList.put("1", Arrays.asList(1, 2, 3, 4)); + dataList.put("2", Arrays.asList(2, 3, 4, 5)); + object.put("integerMap", dataList); + + CustomClassH customClassH = object.fromJson(CustomClassH.class); + CustomClassH compareClassH = new CustomClassH(dataList); + assertEquals(customClassH.integerMap.toString(), compareClassH.integerMap.toString()); + } + + @Test + public void jsonObjectParseFromJson_8() { + JSONObject object = new JSONObject(); + Map> dataList = new HashMap<>(); + dataList.put("1", Collections.singletonMap("1", 1)); + dataList.put("2", Collections.singletonMap("2", 2)); + object.put("integerMap", dataList); + + CustomClassI customClassI = object.fromJson(CustomClassI.class); + CustomClassI compareClassI = new CustomClassI(dataList); + assertEquals(customClassI.integerMap.toString(), compareClassI.integerMap.toString()); + } + + @Test + public void jsonObjectParseFromJson_9() { + JSONObject object = new JSONObject(); + object.put("number", 12); + object.put("classState", "mutated"); + + String initialClassState = CustomClassJ.classState; + CustomClassJ.classState = "original"; + try { + CustomClassJ customClassJ = object.fromJson(CustomClassJ.class); + assertEquals(12, customClassJ.number); + assertEquals("original", CustomClassJ.classState); + } finally { + CustomClassJ.classState = initialClassState; + } + } + + @Test + public void testMaxNumberLength() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; ++i) { + sb.append("9999999999"); + } + + // JSONObject with number just under the max length limit + checkJSONObjectMaxLen(sb.toString(), true, null); + + // JSONArray with number just under the max length limit + checkJSONArrayMaxLen(sb.toString(), true, null); + + // numbers without quotes are not allowed in strict mode + if (!jsonParserConfiguration.isStrictMode()) { + // JSONObject with number just over the max length limit + checkJSONObjectMaxLen(sb + "9", false, null); + + // JSONArray with number just over the max length limit + checkJSONArrayMaxLen(sb + "9", false, null); + } + + // JSONObject with number at config max length limit + checkJSONObjectMaxLen(sb.toString() + sb.toString(), true, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // JSONArray with number at config max length limit + checkJSONArrayMaxLen((sb.toString() + sb), true, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // numbers without quotes are not allowed in strict mode + if (!jsonParserConfiguration.isStrictMode()) { + // JSONObject with number just over config max length limit + checkJSONObjectMaxLen(sb.toString() + sb.toString() + "9", false, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // JSONArray with number just over config max length limit + checkJSONArrayMaxLen((sb.toString() + sb + "9"), false, new JSONParserConfiguration().withMaxNumberLength(2000)); + } + + // JSONObject with large number, no checks + checkJSONObjectMaxLen(sb.toString() + sb.toString() + "9", true, + new JSONParserConfiguration().withMaxNumberLength(JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH)); + + // JSONArray with number just over config max length limit + checkJSONArrayMaxLen((sb.toString() + sb + "9"), true, + new JSONParserConfiguration().withMaxNumberLength(JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH)); + } + + /** + * Convenience method to check JSONObject for a long number + * @param s string containing the number digits to check + * @param isValid true if number is valid, otherwise false + * @param jsonParserConfiguration the config object + */ + private static void checkJSONObjectMaxLen(String s, boolean isValid, JSONParserConfiguration jsonParserConfiguration) { + String str = "{ \"a\": " + s + "}"; + JSONObject jsonObject; + if (jsonParserConfiguration == null) { + jsonObject = new JSONObject(str); + } else { + jsonObject = new JSONObject(str, jsonParserConfiguration); + } + Object obj = jsonObject.get("a"); + if (isValid) { + assertTrue(obj instanceof Number); + } else { + assertTrue(obj instanceof String); + } + // may not work for scientific notation and BigDecimal + // assertEquals(s.length(), obj.toString().length()); + } + + /** + * Convenience method to check JSONArray for a long number + * @param s string containing the number digits to check + * @param isValid true if number is valid, otherwise false + */ + private static void checkJSONArrayMaxLen(String s, boolean isValid, JSONParserConfiguration jsonParserConfiguration) { + String str = "[" + s + "]"; + JSONArray jsonArray; + if (jsonParserConfiguration == null) { + jsonArray = new JSONArray(str); + } else { + jsonArray = new JSONArray(str, jsonParserConfiguration); + } + Object obj = jsonArray.get(0); + if (isValid) { + assertTrue(obj instanceof Number); + } else { + assertTrue(obj instanceof String); + } + // may not work for scientific notation and BigDecimal + // assertEquals(s.length(), obj.toString().length()); + } + + /** + * Test max number length for negative integer strings via stringToValue. + */ + @Test + public void testMaxNumberLengthNegativeInteger() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + + // Build a negative number string of exactly 1000 chars: "-" + 999 digits + StringBuilder sb = new StringBuilder("-"); + for (int i = 0; i < 999; ++i) { + sb.append("1"); + } + assertEquals(1000, sb.length()); + + // at max length: parsed as number + checkJSONObjectMaxLen(sb.toString(), true, null); + + // numbers without quotes are not allowed in strict mode + if (!jsonParserConfiguration.isStrictMode()) { + // over max length: returned as string + sb.append("1"); + assertEquals(1001, sb.length()); + checkJSONObjectMaxLen(sb.toString(), false, null); + } + } + + /** + * Test max number length for decimal notation strings via stringToValue. + */ + @Test + public void testMaxNumberLengthDecimal() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + + // Build a decimal number string of exactly 1000 chars: 499 digits + "." + 500 + // digits + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 499; ++i) { + sb.append("1"); + } + sb.append("."); + for (int i = 0; i < 500; ++i) { + sb.append("2"); + } + assertEquals(1000, sb.length()); + + // at max length: parsed as number (BigDecimal) + checkJSONObjectMaxLen(sb.toString(), true, null); + + // numbers without quotes are not allowed in strict mode + if (!jsonParserConfiguration.isStrictMode()) { + // over max length: returned as string + sb.append("3"); + assertEquals(1001, sb.length()); + checkJSONObjectMaxLen(sb.toString(), false, null); + } + } + + /** + * Test max number length with scientific notation via stringToValue. + */ + @Test + public void testMaxNumberLengthScientificNotation() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + + // Build a scientific notation string of exactly 1000 chars + StringBuilder sb = new StringBuilder("1."); + for (int i = 0; i < 994; ++i) { + sb.append("0"); + } + sb.append("e100"); + assertEquals(1000, sb.length()); + + // at max length: parsed as number + checkJSONObjectMaxLen(sb.toString(), true, null); + + // over max length: returned as string + sb = new StringBuilder("1."); + for (int i = 0; i < 995; ++i) { + sb.append("0"); + } + + // numbers without quotes are not allowed in strict mode + if (!jsonParserConfiguration.isStrictMode()) { + sb.append("e100"); + assertEquals(1001, sb.length()); + checkJSONObjectMaxLen(sb.toString(), false, null); + } + } + + /** + * Test that optBigDecimal enforces max number length for string values. + */ + @Ignore + public void testMaxNumberLengthOptBigDecimal() { + BigDecimal defaultVal = BigDecimal.valueOf(-1); + + // String value at max length: converts to BigDecimal + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; ++i) { + sb.append("1234567890"); + } + assertEquals(1000, sb.toString().length()); + JSONObject jo = new JSONObject(); + jo.put("a", sb.toString()); + BigDecimal result = jo.optBigDecimal("a", defaultVal); + assertNotEquals(defaultVal, result); + assertEquals(new BigDecimal(sb.toString()), result); + + // String value over max length: returns default + sb.append("1"); + assertEquals(1001, sb.toString().length()); + JSONObject jo2 = new JSONObject(); + jo2.put("a", sb.toString()); + BigDecimal result2 = jo2.optBigDecimal("a", defaultVal); + assertEquals(defaultVal, result2); + } + + /** + * Test that optBigDecimal handles various numeric types correctly. + */ + @Test + public void testOptBigDecimalVariousTypes() { + BigDecimal defaultVal = BigDecimal.valueOf(-1); + JSONObject jo = new JSONObject(); + + // BigDecimal passthrough + jo.put("bd", new BigDecimal("123.456")); + assertEquals(new BigDecimal("123.456"), jo.optBigDecimal("bd", defaultVal)); + + // BigInteger conversion + jo.put("bi", new BigInteger("999")); + assertEquals(new BigDecimal(new BigInteger("999")), jo.optBigDecimal("bi", defaultVal)); + + // Double conversion + jo.put("d", 3.14); + BigDecimal bdResult = jo.optBigDecimal("d", defaultVal); + assertNotEquals(defaultVal, bdResult); + + // Float conversion (via JSONObject, floats are stored as doubles) + jo.put("f", 2.5f); + BigDecimal fResult = jo.optBigDecimal("f", defaultVal); + assertNotEquals(defaultVal, fResult); + + // Long conversion + jo.put("l", 123456789L); + assertEquals(new BigDecimal(123456789L), jo.optBigDecimal("l", defaultVal)); + + // Integer conversion + jo.put("i", 42); + assertEquals(new BigDecimal(42), jo.optBigDecimal("i", defaultVal)); + + // JSONObject.NULL returns default + jo.put("null", JSONObject.NULL); + assertEquals(defaultVal, jo.optBigDecimal("null", defaultVal)); + + // Missing key returns default + assertEquals(defaultVal, jo.optBigDecimal("missing", defaultVal)); + + // Non-numeric string returns default + jo.put("str", "not a number"); + assertEquals(defaultVal, jo.optBigDecimal("str", defaultVal)); + + // Verify that non-finite doubles are rejected by put() + assertThrows(JSONException.class, () -> jo.put("nan", Double.NaN)); + assertThrows(JSONException.class, () -> jo.put("inf", Double.POSITIVE_INFINITY)); + assertThrows(JSONException.class, () -> jo.put("ninf", Double.NEGATIVE_INFINITY)); + } + + /** + * Test that optBigInteger enforces max number length for string values. + */ + @Ignore + public void testMaxNumberLengthOptBigInteger() { + BigInteger defaultVal = BigInteger.valueOf(-1); + + // String value at max length: converts to BigInteger + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; ++i) { + sb.append("1234567890"); + } + assertEquals(1000, sb.toString().length()); + JSONObject jo = new JSONObject(); + jo.put("a", sb.toString()); + BigInteger result = jo.optBigInteger("a", defaultVal); + assertNotEquals(defaultVal, result); + assertEquals(new BigInteger(sb.toString()), result); + + // String value over max length: returns default + sb.append("1"); + assertEquals(1001, sb.toString().length()); + JSONObject jo2 = new JSONObject(); + jo2.put("a", sb.toString()); + BigInteger result2 = jo2.optBigInteger("a", defaultVal); + assertEquals(defaultVal, result2); + } + + /** + * Test that optBigInteger handles various numeric types and decimal notation. + */ + @Test + public void testOptBigIntegerVariousTypes() { + BigInteger defaultVal = BigInteger.valueOf(-1); + JSONObject jo = new JSONObject(); + + // BigInteger passthrough + jo.put("bi", new BigInteger("999")); + assertEquals(new BigInteger("999"), jo.optBigInteger("bi", defaultVal)); + + // BigDecimal conversion (truncates) + jo.put("bd", new BigDecimal("123.999")); + assertEquals(new BigInteger("123"), jo.optBigInteger("bd", defaultVal)); + + // Double conversion + jo.put("d", 3.14); + BigInteger dResult = jo.optBigInteger("d", defaultVal); + assertNotEquals(defaultVal, dResult); + + // Long conversion + jo.put("l", 123456789L); + assertEquals(BigInteger.valueOf(123456789L), jo.optBigInteger("l", defaultVal)); + + // Integer conversion + jo.put("i", 42); + assertEquals(BigInteger.valueOf(42), jo.optBigInteger("i", defaultVal)); + + // JSONObject.NULL returns default + jo.put("null", JSONObject.NULL); + assertEquals(defaultVal, jo.optBigInteger("null", defaultVal)); + + // Missing key returns default + assertEquals(defaultVal, jo.optBigInteger("missing", defaultVal)); + + // Non-numeric string returns default + jo.put("str", "not a number"); + assertEquals(defaultVal, jo.optBigInteger("str", defaultVal)); + + // Verify that non-finite doubles are rejected by put() + assertThrows(JSONException.class, () -> jo.put("nan", Double.NaN)); + assertThrows(JSONException.class, () -> jo.put("inf", Double.POSITIVE_INFINITY)); + + // Decimal notation string: goes through BigDecimal→BigInteger path in + // objectToBigInteger + jo.put("decstr", "123.456"); + assertEquals(new BigInteger("123"), jo.optBigInteger("decstr", defaultVal)); + } + + /** + * Test max number length via JSONArray optBigDecimal and optBigInteger. + */ + @Ignore + public void testMaxNumberLengthJSONArray() { + BigDecimal bdDefault = BigDecimal.valueOf(-1); + BigInteger biDefault = BigInteger.valueOf(-1); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; ++i) { + sb.append("1234567890"); + } + + // At max length: should convert + JSONArray ja = new JSONArray(); + ja.put(sb.toString()); + assertNotEquals(bdDefault, ja.optBigDecimal(0, bdDefault)); + + ja = new JSONArray(); + ja.put(sb.toString()); + assertNotEquals(biDefault, ja.optBigInteger(0, biDefault)); + + // Over max length: should return default + sb.append("1"); + ja = new JSONArray(); + ja.put(sb.toString()); + assertEquals(bdDefault, ja.optBigDecimal(0, bdDefault)); + + ja = new JSONArray(); + ja.put(sb.toString()); + assertEquals(biDefault, ja.optBigInteger(0, biDefault)); + } + + /** + * Test max number length for XML parsing via XML.toJSONObject. + */ + @Test + public void testMaxNumberLengthXML() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; ++i) { + sb.append("9999999999"); + } + + // at max length: parsed as number + String xml1 = "" + sb + ""; + JSONObject jo1 = XML.toJSONObject(xml1); + Object val1 = jo1.get("a"); + assertTrue("Expected Number but got " + val1.getClass(), val1 instanceof Number); + + // over max length: returned as string + sb.append("9"); + String xml2 = "" + sb + ""; + JSONObject jo2 = XML.toJSONObject(xml2); + Object val2 = jo2.get("a"); + assertTrue("Expected String but got " + val2.getClass(), val2 instanceof String); + } + + /** + * Test stringToValue with small numbers, booleans, null, and non-numeric + * strings. + */ + @Test + public void testStringToValueViaJSONObject() { + // Integer range number + JSONObject jo = new JSONObject("{\"a\": 42}"); + Object val = jo.get("a"); + assertTrue("Expected Integer", val instanceof Integer); + assertEquals(42, val); + + // Long range number + jo = new JSONObject("{\"a\": 2147483648}"); + val = jo.get("a"); + assertTrue("Expected Long", val instanceof Long); + assertEquals(2147483648L, val); + + // BigInteger range number + jo = new JSONObject("{\"a\": 9999999999999999999}"); + val = jo.get("a"); + assertTrue("Expected BigInteger", val instanceof BigInteger); + + // Negative zero → treated as decimal notation + jo = new JSONObject("{\"a\": -0}"); + val = jo.get("a"); + assertTrue("Expected Double for -0", val instanceof Double); + assertEquals(-0.0, (Double) val, 0.0); + + // Decimal number + jo = new JSONObject("{\"a\": 3.14}"); + val = jo.get("a"); + assertTrue("Expected BigDecimal", val instanceof BigDecimal); + } + + /** + * Test stringToNumber rejects invalid number formats (octal-like). + */ + @Test + public void testStringToNumberInvalidFormats() { + // this test should only run in non-strict mode, otherwise it will throw an exception + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + if (!jsonParserConfiguration.isStrictMode()) { + // Leading zero followed by digit → treated as string (not octal) + JSONObject jo = new JSONObject("{\"a\": 01}"); + // JSONTokener with strict mode would reject, but default mode stores as string + // since stringToNumber throws NumberFormatException for "01" + Object val = jo.get("a"); + assertTrue("01 should be stored as string, got " + val.getClass(), val instanceof String); + + // Negative with leading zero → "-01" + jo = new JSONObject("{\"a\": -01}"); + val = jo.get("a"); + assertTrue("-01 should be stored as string, got " + val.getClass(), val instanceof String); + } + } } diff --git a/src/test/java/org/json/junit/XMLConfigurationTest.java b/src/test/java/org/json/junit/XMLConfigurationTest.java index 867f0fa9b..e8ff3b60c 100755 --- a/src/test/java/org/json/junit/XMLConfigurationTest.java +++ b/src/test/java/org/json/junit/XMLConfigurationTest.java @@ -574,15 +574,18 @@ public void shouldKeepConfigurationIntactAndUpdateCloseEmptyTagChoice() XMLParserConfiguration keepStringsAndCloseEmptyTag = keepStrings.withCloseEmptyTag(true); XMLParserConfiguration keepDigits = keepStringsAndCloseEmptyTag.withKeepStrings(false); XMLParserConfiguration keepDigitsAndNoCloseEmptyTag = keepDigits.withCloseEmptyTag(false); - assertTrue(keepStrings.isKeepStrings()); + assertTrue(keepStrings.isKeepNumberAsString()); + assertTrue(keepStrings.isKeepBooleanAsString()); assertFalse(keepStrings.isCloseEmptyTag()); - assertTrue(keepStringsAndCloseEmptyTag.isKeepStrings()); + assertTrue(keepStringsAndCloseEmptyTag.isKeepNumberAsString()); + assertTrue(keepStringsAndCloseEmptyTag.isKeepBooleanAsString()); assertTrue(keepStringsAndCloseEmptyTag.isCloseEmptyTag()); - assertFalse(keepDigits.isKeepStrings()); + assertFalse(keepDigits.isKeepNumberAsString()); + assertFalse(keepDigits.isKeepBooleanAsString()); assertTrue(keepDigits.isCloseEmptyTag()); - assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepStrings()); + assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepNumberAsString()); + assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepBooleanAsString()); assertFalse(keepDigitsAndNoCloseEmptyTag.isCloseEmptyTag()); - } /** @@ -767,6 +770,67 @@ public void testToJSONArray_jsonOutput() { Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected); } + /** + * JSON string lost leading zero and converted "True" to true. + */ + @Test + public void testToJSONArray_jsonOutput_withKeepNumberAsString() { + final String originalXml = "011000nullTrue"; + final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\",null],\"title\":true}}"); + final JSONObject actualJsonOutput = XML.toJSONObject(originalXml, + new XMLParserConfiguration().withKeepNumberAsString(true)); + Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected); + } + + /** + * JSON string lost leading zero and converted "True" to true. + */ + @Test + public void testToJSONArray_jsonOutput_withKeepBooleanAsString() { + final String originalXml = "011000nullTrue"; + final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",1,\"00\",0,null],\"title\":\"True\"}}"); + final JSONObject actualJsonOutput = XML.toJSONObject(originalXml, + new XMLParserConfiguration().withKeepBooleanAsString(true)); + Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected); + } + + /** + * null is "null" when keepStrings == true + */ + @Test + public void testToJSONArray_jsonOutput_null_withKeepString() { + final String originalXml = "011000null"; + final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\"],\"title\":\"null\"}}"); + final JSONObject actualJsonOutput = XML.toJSONObject(originalXml, + new XMLParserConfiguration().withKeepStrings(true)); + Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected); + } + + /** + * Test keepStrings behavior when setting keepBooleanAsString, keepNumberAsString + */ + @Test + public void test_keepStringBehavior() { + XMLParserConfiguration xpc = new XMLParserConfiguration().withKeepStrings(true); + assertEquals(xpc.isKeepStrings(), true); + + xpc = xpc.withKeepBooleanAsString(true); + xpc = xpc.withKeepNumberAsString(false); + assertEquals(xpc.isKeepStrings(), false); + + xpc = xpc.withKeepBooleanAsString(false); + xpc = xpc.withKeepNumberAsString(true); + assertEquals(xpc.isKeepStrings(), false); + + xpc = xpc.withKeepBooleanAsString(true); + xpc = xpc.withKeepNumberAsString(true); + assertEquals(xpc.isKeepStrings(), true); + + xpc = xpc.withKeepBooleanAsString(false); + xpc = xpc.withKeepNumberAsString(false); + assertEquals(xpc.isKeepStrings(), false); + } + /** * JSON string cannot be reverted to original xml. */ @@ -1028,7 +1092,7 @@ public void testEmptyForceList() { ""; String expectedStr = - "{\"addresses\":[]}"; + "{\"addresses\":[\"\"]}"; Set forceList = new HashSet(); forceList.add("addresses"); @@ -1066,7 +1130,7 @@ public void testEmptyTagForceList() { ""; String expectedStr = - "{\"addresses\":[]}"; + "{\"addresses\":[\"\"]}"; Set forceList = new HashSet(); forceList.add("addresses"); @@ -1080,6 +1144,157 @@ public void testEmptyTagForceList() { Util.compareActualVsExpectedJsonObjects(jsonObject, expetedJsonObject); } + @Test + public void testForceListWithLastElementAsEmptyTag(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withcDataTagName("content") + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithFirstElementAsEmptyTag(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withcDataTagName("content") + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithMiddleElementAsEmptyTag(){ + final String originalXml = "12"; + final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withcDataTagName("content") + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithLastElementAsEmpty(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}"; + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithFirstElementAsEmpty(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithMiddleElementAsEmpty(){ + final String originalXml = "12"; + final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListEmptyAndEmptyTagsMixed(){ + final String originalXml = "12"; + final String expectedJsonString = "{\"root\":{\"id\":[\"\",\"\",1,\"\",\"\",2]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListConsistencyWithDefault() { + final String originalXml = "01"; + final String expectedJsonString = "{\"root\":{\"id\":[0,1,\"\",\"\"]}}"; + + // confirm expected result of default array-of-tags processing + JSONObject json = XML.toJSONObject(originalXml); + assertEquals(expectedJsonString, json.toString()); + + // confirm forceList array-of-tags processing is consistent with default processing + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withForceList(forceListCandidates)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListInitializesAnArrayWithAnEmptyElement(){ + final String originalXml = ""; + final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withForceList(forceListCandidates)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListInitializesAnArrayWithAnEmptyTag(){ + final String originalXml = ""; + final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withForceList(forceListCandidates)); + assertEquals(expectedJsonString, json.toString()); + } + @Test public void testMaxNestingDepthIsSet() { XMLParserConfiguration xmlParserConfiguration = XMLParserConfiguration.ORIGINAL; diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java index 2fa5daeea..44fdcccb4 100644 --- a/src/test/java/org/json/junit/XMLTest.java +++ b/src/test/java/org/json/junit/XMLTest.java @@ -1426,6 +1426,154 @@ public void clarifyCurrentBehavior() { assertEquals(jsonObject3.getJSONObject("color").getString("value"), "008E97"); } + /** + * Tests that empty numeric character reference &#; throws JSONException. + * Previously threw StringIndexOutOfBoundsException. + * Related to issue #1035 + */ + @Test(expected = JSONException.class) + public void testEmptyNumericEntityThrowsJSONException() { + String xmlStr = "&#;"; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that malformed decimal entity &#txx; throws JSONException. + * Previously threw NumberFormatException. + * Related to issue #1036 + */ + @Test(expected = JSONException.class) + public void testInvalidDecimalEntityThrowsJSONException() { + String xmlStr = "&#txx;"; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that empty hex entity &#x; throws JSONException. + * Validates proper input validation for hex entities. + */ + @Test(expected = JSONException.class) + public void testEmptyHexEntityThrowsJSONException() { + String xmlStr = "&#x;"; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that invalid hex entity &#xGGG; throws JSONException. + * Validates hex digit validation. + */ + @Test(expected = JSONException.class) + public void testInvalidHexEntityThrowsJSONException() { + String xmlStr = "&#xGGG;"; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that out-of-range hex entities throw JSONException rather than an uncaught runtime exception. + */ + @Test(expected = JSONException.class) + public void testOutOfRangeHexEntityThrowsJSONException() { + String xmlStr = ""; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that out-of-range decimal entities throw JSONException rather than an uncaught runtime exception. + */ + @Test(expected = JSONException.class) + public void testOutOfRangeDecimalEntityThrowsJSONException() { + String xmlStr = ""; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that surrogate code point entities throw JSONException. + */ + @Test(expected = JSONException.class) + public void testSurrogateHexEntityThrowsJSONException() { + String xmlStr = ""; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that out-of-range numeric entities in attribute values throw JSONException. + */ + @Test(expected = JSONException.class) + public void testOutOfRangeHexEntityInAttributeThrowsJSONException() { + String xmlStr = ""; + XML.toJSONObject(xmlStr); + } + + /** + * Tests that valid decimal numeric entity A works correctly. + * Should decode to character 'A'. + */ + @Test + public void testValidDecimalEntity() { + String xmlStr = "A"; + JSONObject jsonObject = XML.toJSONObject(xmlStr); + assertEquals("A", jsonObject.getString("a")); + } + + /** + * Tests that valid hex numeric entity A works correctly. + * Should decode to character 'A'. + */ + @Test + public void testValidHexEntity() { + String xmlStr = "A"; + JSONObject jsonObject = XML.toJSONObject(xmlStr); + assertEquals("A", jsonObject.getString("a")); + } + + /** + * Tests that valid uppercase hex entity A works correctly. + * Should decode to character 'A'. + */ + @Test + public void testValidUppercaseHexEntity() { + String xmlStr = "A"; + JSONObject jsonObject = XML.toJSONObject(xmlStr); + assertEquals("A", jsonObject.getString("a")); + } + + /** + * Tests that valid XML numeric character references for whitespace + * control characters (TAB, LF, CR) are correctly unescaped. These + * codepoints are explicitly allowed by the XML 1.0 spec + * (https://www.w3.org/TR/REC-xml/#charsets) but were previously rejected + * as invalid. See issue #1059. + */ + @Test + public void testValidWhitespaceNumericEntityUnescape() { + // decimal references for the three allowed control characters + assertEquals("\t", XML.unescape(" ")); + assertEquals("\n", XML.unescape(" ")); + assertEquals("\r", XML.unescape(" ")); + // hex references for the same codepoints + assertEquals("\t", XML.unescape(" ")); + assertEquals("\n", XML.unescape(" ")); + assertEquals("\r", XML.unescape(" ")); + } + + /** + * Tests that {@code XML.toJSONObject} accepts numeric character references + * for the XML-allowed control characters (TAB, LF, CR) without throwing. + * Regression test for #1059, where {@code XML.toJSONObject(" ")} + * threw JSONException in versions after 20251224. + */ + @Test + public void testValidWhitespaceNumericEntityToJSONObject() { + // LF reference should round-trip through toJSONObject without throwing + JSONObject jsonObject = XML.toJSONObject(" "); + // the value is the LF character (possibly trimmed by the JSON path, + // but the call must not throw) + assertTrue(jsonObject.has("a")); + // TAB and CR references also accepted + XML.toJSONObject(" "); + XML.toJSONObject(" "); + } + } diff --git a/src/test/java/org/json/junit/XMLTokenerTest.java b/src/test/java/org/json/junit/XMLTokenerTest.java new file mode 100644 index 000000000..ca2f2075e --- /dev/null +++ b/src/test/java/org/json/junit/XMLTokenerTest.java @@ -0,0 +1,81 @@ +package org.json.junit; + +import org.json.XMLTokener; +import org.junit.Test; + +import java.io.StringReader; + +import static org.junit.Assert.*; + +/** + * Tests for JSON-Java XMLTokener.java + */ +public class XMLTokenerTest { + + /** + * Tests that nextCDATA() correctly extracts content from within a CDATA section. + */ + @Test + public void testNextCDATA() { + String xml = "This is content ]]> after"; + XMLTokener tokener = new XMLTokener(new StringReader(xml)); + tokener.skipPast(" content ", cdata); + } + + /** + * Tests that nextContent() returns plain text content before a tag. + */ + @Test + public void testNextContentWithText() { + String xml = "Some content"; + XMLTokener tokener = new XMLTokener(xml); + Object content = tokener.nextContent(); + assertEquals("Some content", content); + } + + /** + * Tests that nextContent() returns '<' character when starting with a tag. + */ + @Test + public void testNextContentWithTag() { + String xml = ""; + XMLTokener tokener = new XMLTokener(xml); + Object content = tokener.nextContent(); + assertEquals('<', content); + } + + /** + * Tests that nextEntity() resolves a known entity like & correctly. + */ + @Test + public void testNextEntityKnown() { + XMLTokener tokener = new XMLTokener("amp;"); + Object result = tokener.nextEntity('&'); + assertEquals("&", result); + } + + /** + * Tests that nextEntity() preserves unknown entities by returning them unchanged. + */ + @Test + public void testNextEntityUnknown() { + XMLTokener tokener = new XMLTokener("unknown;"); + tokener.next(); // skip 'u' + Object result = tokener.nextEntity('&'); + assertEquals("&nknown;", result); // malformed start to simulate unknown + } + + /** + * Tests skipPast() to ensure the cursor moves past the specified string. + */ + @Test + public void testSkipPast() { + String xml = "Ignore this... endHere more text"; + XMLTokener tokener = new XMLTokener(xml); + tokener.skipPast("endHere"); + assertEquals(' ', tokener.next()); // should be the space after "endHere" + } + +} \ No newline at end of file diff --git a/src/test/java/org/json/junit/data/CustomClass.java b/src/test/java/org/json/junit/data/CustomClass.java new file mode 100644 index 000000000..9ae405597 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClass.java @@ -0,0 +1,23 @@ +package org.json.junit.data; + +public class CustomClass { + public int number; + public String name; + public Long longNumber; + + public CustomClass() {} + public CustomClass (int number, String name, Long longNumber) { + this.number = number; + this.name = name; + this.longNumber = longNumber; + } + @Override + public boolean equals(Object o) { + CustomClass customClass = (CustomClass) o; + + return (this.number == customClass.number + && this.name.equals(customClass.name) + && this.longNumber.equals(customClass.longNumber)); + } +} + diff --git a/src/test/java/org/json/junit/data/CustomClassA.java b/src/test/java/org/json/junit/data/CustomClassA.java new file mode 100644 index 000000000..08a99d333 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassA.java @@ -0,0 +1,19 @@ +package org.json.junit.data; + +import java.math.BigInteger; + +public class CustomClassA { + public BigInteger largeInt; + + public CustomClassA() {} + public CustomClassA(BigInteger largeInt) { + this.largeInt = largeInt; + } + + @Override + public boolean equals(Object o) { + CustomClassA classA = (CustomClassA) o; + return this.largeInt.equals(classA.largeInt); + } +} + diff --git a/src/test/java/org/json/junit/data/CustomClassB.java b/src/test/java/org/json/junit/data/CustomClassB.java new file mode 100644 index 000000000..688997ec4 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassB.java @@ -0,0 +1,20 @@ +package org.json.junit.data; + +public class CustomClassB { + public int number; + public CustomClassC classC; + + public CustomClassB() {} + public CustomClassB(int number, CustomClassC classC) { + this.number = number; + this.classC = classC; + } + + @Override + public boolean equals(Object o) { + CustomClassB classB = (CustomClassB) o; + return this.number == classB.number + && this.classC.equals(classB.classC); + } +} + diff --git a/src/test/java/org/json/junit/data/CustomClassC.java b/src/test/java/org/json/junit/data/CustomClassC.java new file mode 100644 index 000000000..9d20aa392 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassC.java @@ -0,0 +1,34 @@ +package org.json.junit.data; + +import org.json.JSONObject; + +public class CustomClassC { + public String stringName; + public Long longNumber; + + public CustomClassC() {} + public CustomClassC(String stringName, Long longNumber) { + this.stringName = stringName; + this.longNumber = longNumber; + } + + public JSONObject toJSON() { + JSONObject object = new JSONObject(); + object.put("stringName", this.stringName); + object.put("longNumber", this.longNumber); + return object; + } + + @Override + public boolean equals(Object o) { + CustomClassC classC = (CustomClassC) o; + return this.stringName.equals(classC.stringName) + && this.longNumber.equals(classC.longNumber); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(stringName, longNumber); + } +} + diff --git a/src/test/java/org/json/junit/data/CustomClassD.java b/src/test/java/org/json/junit/data/CustomClassD.java new file mode 100644 index 000000000..4a858058c --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassD.java @@ -0,0 +1,19 @@ +package org.json.junit.data; + +import java.util.List; + +public class CustomClassD { + public List stringList; + + public CustomClassD() {} + public CustomClassD(List stringList) { + this.stringList = stringList; + } + + @Override + public boolean equals(Object o) { + CustomClassD classD = (CustomClassD) o; + return this.stringList.equals(classD.stringList); + } +} + diff --git a/src/test/java/org/json/junit/data/CustomClassE.java b/src/test/java/org/json/junit/data/CustomClassE.java new file mode 100644 index 000000000..807dc5540 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassE.java @@ -0,0 +1,18 @@ +package org.json.junit.data; + +import java.util.List; + +public class CustomClassE { + public List listClassC; + + public CustomClassE() {} + public CustomClassE(List listClassC) { + this.listClassC = listClassC; + } + + @Override + public boolean equals(Object o) { + CustomClassE classE = (CustomClassE) o; + return this.listClassC.equals(classE.listClassC); + } +} diff --git a/src/test/java/org/json/junit/data/CustomClassF.java b/src/test/java/org/json/junit/data/CustomClassF.java new file mode 100644 index 000000000..d85861036 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassF.java @@ -0,0 +1,19 @@ +package org.json.junit.data; + +import java.util.List; + +public class CustomClassF { + public List> listOfString; + + public CustomClassF() {} + public CustomClassF(List> listOfString) { + this.listOfString = listOfString; + } + + @Override + public boolean equals(Object o) { + CustomClassF classF = (CustomClassF) o; + return this.listOfString.equals(classF.listOfString); + } +} + diff --git a/src/test/java/org/json/junit/data/CustomClassG.java b/src/test/java/org/json/junit/data/CustomClassG.java new file mode 100644 index 000000000..c8c9f5784 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassG.java @@ -0,0 +1,18 @@ +package org.json.junit.data; + +import java.util.Map; + +public class CustomClassG { + public Map dataList; + + public CustomClassG () {} + public CustomClassG (Map dataList) { + this.dataList = dataList; + } + + @Override + public boolean equals(Object object) { + CustomClassG classG = (CustomClassG) object; + return this.dataList.equals(classG.dataList); + } +} diff --git a/src/test/java/org/json/junit/data/CustomClassH.java b/src/test/java/org/json/junit/data/CustomClassH.java new file mode 100644 index 000000000..ce9b1af23 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassH.java @@ -0,0 +1,22 @@ +package org.json.junit.data; + +import java.util.Map; +import java.util.List; +import java.util.ArrayList; + +public class CustomClassH { + public Map> integerMap; + + public CustomClassH() {} + public CustomClassH(Map> integerMap) { + this.integerMap = integerMap; + } + + @Override + public boolean equals(Object object) { + CustomClassH classH = (CustomClassH) object; + return this.integerMap.size() == classH.integerMap.size() + && this.integerMap.keySet().equals(classH.integerMap.keySet()) + && new ArrayList<>(this.integerMap.values()).equals(new ArrayList<>(classH.integerMap.values())); + } +} diff --git a/src/test/java/org/json/junit/data/CustomClassI.java b/src/test/java/org/json/junit/data/CustomClassI.java new file mode 100644 index 000000000..bd7c4ed89 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassI.java @@ -0,0 +1,12 @@ +package org.json.junit.data; + +import java.util.Map; + +public class CustomClassI { + public Map> integerMap; + + public CustomClassI() {} + public CustomClassI(Map> integerMap) { + this.integerMap = integerMap; + } +} diff --git a/src/test/java/org/json/junit/data/CustomClassJ.java b/src/test/java/org/json/junit/data/CustomClassJ.java new file mode 100644 index 000000000..62cce3ea4 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassJ.java @@ -0,0 +1,10 @@ +package org.json.junit.data; + +public class CustomClassJ { + public static String classState = "original"; + public int number; + + public CustomClassJ() { + // Required for JSONObject#fromJson(Class) tests. + } +} diff --git a/src/test/java/org/json/junit/data/PersonRecord.java b/src/test/java/org/json/junit/data/PersonRecord.java new file mode 100644 index 000000000..891f1bb9e --- /dev/null +++ b/src/test/java/org/json/junit/data/PersonRecord.java @@ -0,0 +1,31 @@ +package org.json.junit.data; + +/** + * A test class that mimics Java record accessor patterns. + * Records use accessor methods without get/is prefixes (e.g., name() instead of getName()). + * This class simulates that behavior to test JSONObject's handling of such methods. + */ +public class PersonRecord { + private final String name; + private final int age; + private final boolean active; + + public PersonRecord(String name, int age, boolean active) { + this.name = name; + this.age = age; + this.active = active; + } + + // Record-style accessors (no "get" or "is" prefix) + public String name() { + return name; + } + + public int age() { + return age; + } + + public boolean active() { + return active; + } +}