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 bb4cf0723..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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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@v3
+ 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 78920a180..40acb8f06 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,9 @@ JSON in Java [package org.json]
[](https://mvnrepository.com/artifact/org.json/json)
[](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml)
[](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml)
+[](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/20240303/json-20240303.jar)**
+**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20260522/json-20260522.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
@@ -97,6 +111,18 @@ Execute the test suite with Gradlew:
gradlew clean build test
```
+*Optional* Execute the test suite in strict mode with Gradlew:
+
+```shell
+gradlew testWithStrictMode
+```
+
+*Optional* Execute the test suite in strict mode with Maven:
+
+```shell
+mvn test -P test-strict-mode
+```
+
# Notes
For more information, please see [NOTES.md](https://github.com/stleary/JSON-java/blob/master/docs/NOTES.md)
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 30a85785b..d8b69805f 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,9 +20,20 @@ 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.4.0'
+ testImplementation 'com.jayway.jsonpath:json-path:2.9.0'
testImplementation 'org.mockito:mockito-core:4.2.0'
}
@@ -30,7 +42,7 @@ subprojects {
}
group = 'org.json'
-version = 'v20230618-SNAPSHOT'
+version = 'v20260522-SNAPSHOT'
description = 'JSON in Java'
sourceCompatibility = '1.8'
@@ -53,3 +65,75 @@ publishing {
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
+// Add these imports at the top of your build.gradle file
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.StandardCopyOption
+
+// Your existing build configurations...
+
+// Add a new task to modify the file
+task modifyStrictMode {
+ doLast {
+ println "Modifying JSONParserConfiguration.java to enable strictMode..."
+
+ def filePath = project.file('src/main/java/org/json/JSONParserConfiguration.java')
+
+ if (!filePath.exists()) {
+ throw new GradleException("Could not find file: ${filePath.absolutePath}")
+ }
+
+ // Create a backup of the original file
+ def backupFile = new File(filePath.absolutePath + '.bak')
+ Files.copy(filePath.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
+
+ // Read and modify the file content
+ def content = filePath.text
+ def modifiedContent = content.replace('// this.strictMode = true;', 'this.strictMode = true;')
+
+ // Write the modified content back to the file
+ filePath.text = modifiedContent
+
+ println "File modified successfully at: ${filePath.absolutePath}"
+ }
+}
+
+// Add a task to restore the original file
+task restoreStrictMode {
+ doLast {
+ println "Restoring original JSONParserConfiguration.java..."
+
+ def filePath = project.file('src/main/java/org/json/JSONParserConfiguration.java')
+ def backupFile = new File(filePath.absolutePath + '.bak')
+
+ if (backupFile.exists()) {
+ Files.copy(backupFile.toPath(), filePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ backupFile.delete()
+ println "Original file restored successfully at: ${filePath.absolutePath}"
+ } else {
+ println "Backup file not found at: ${backupFile.absolutePath}. No restoration performed."
+ }
+ }
+}
+
+// Create a task to run the workflow
+task testWithStrictMode {
+ dependsOn modifyStrictMode
+ finalizedBy restoreStrictMode
+
+ doLast {
+ // This will trigger a clean build and run tests with strictMode enabled
+ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
+ exec {
+ executable 'cmd'
+ args '/c', 'gradlew.bat', 'clean', 'build'
+ }
+ } else {
+ exec {
+ executable './gradlew'
+ args 'clean', 'build'
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/RELEASES.md b/docs/RELEASES.md
index 30b8af2bc..7513766ff 100644
--- a/docs/RELEASES.md
+++ b/docs/RELEASES.md
@@ -5,6 +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)
~~~
+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.
20240205 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 7b102433b..3f15d6896 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
org.jsonjson
- 20240303
+ 20260522bundleJSON in Java
@@ -70,7 +70,7 @@
com.jayway.jsonpathjson-path
- 2.4.0
+ 2.9.0test
@@ -198,6 +198,66 @@
maven-jar-plugin3.3.0
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.9.0
+ true
+
+ central
+
+
+
+
+ test-strict-mode
+
+
+
+ com.google.code.maven-replacer-plugin
+ replacer
+ 1.5.3
+
+
+
+ enable-strict-mode
+ process-sources
+
+ replace
+
+
+ src/main/java/org/json/JSONParserConfiguration.java
+
+
+ // this.strictMode = true;
+ this.strictMode = true;
+
+
+
+
+
+
+ restore-original
+ test
+
+ replace
+
+
+ src/main/java/org/json/JSONParserConfiguration.java
+
+
+ this.strictMode = true;
+ // this.strictMode = true;
+
+
+
+
+
+
+
+
+
+
+
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 382359858..d1dcf5c44 100644
--- a/src/main/java/org/json/JSONArray.java
+++ b/src/main/java/org/json/JSONArray.java
@@ -75,20 +75,19 @@ public JSONArray() {
}
/**
- * Constructs a JSONArray from a JSONTokener.
- *
- * This constructor reads the JSONTokener to parse a JSON array. It uses the default JSONParserConfiguration.
+ * Construct a JSONArray from a JSONTokener.
*
- * @param x A JSONTokener
- * @throws JSONException If there is a syntax error.
+ * @param x
+ * A JSONTokener
+ * @throws JSONException
+ * If there is a syntax error.
*/
public JSONArray(JSONTokener x) throws JSONException {
- this(x, new JSONParserConfiguration());
+ this(x, x.getJsonParserConfiguration());
}
/**
* Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration.
- * JSONParserConfiguration contains strictMode turned off (false) by default.
*
* @param x A JSONTokener instance from which the JSONArray is constructed.
* @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
@@ -96,84 +95,82 @@ public JSONArray(JSONTokener x) throws JSONException {
*/
public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
this();
- char nextChar = x.nextClean();
- // check first character, if not '[' throw JSONException
- if (nextChar != '[') {
+ boolean isInitial = x.getPrevious() == 0;
+ if (x.nextClean() != '[') {
throw x.syntaxError("A JSONArray text must start with '['");
}
- parseTokener(x, jsonParserConfiguration); // runs recursively
-
- }
-
- private void parseTokener(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) {
- boolean strictMode = jsonParserConfiguration.isStrictMode();
-
- char cursor = x.nextClean();
-
- switch (cursor) {
- case 0:
- throwErrorIfEoF(x);
- break;
- case ',':
- cursor = x.nextClean();
-
- throwErrorIfEoF(x);
-
- if(strictMode && cursor == ']'){
- throw x.syntaxError(getInvalidCharErrorMsg(cursor));
- }
-
- if (cursor == ']') {
- break;
- }
-
- x.back();
-
- parseTokener(x, jsonParserConfiguration);
- break;
- case ']':
- if (strictMode) {
- cursor = x.nextClean();
- boolean isEoF = x.end();
-
- if (isEoF) {
- break;
- }
-
- if (x.getArrayLevel() == 0) {
- throw x.syntaxError(getInvalidCharErrorMsg(cursor));
- }
-
+ char nextChar = x.nextClean();
+ 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();
+ for (;;) {
+ if (x.nextClean() == ',') {
x.back();
+ this.myArrayList.add(JSONObject.NULL);
+ } else {
+ x.back();
+ this.myArrayList.add(x.nextValue());
}
- break;
- default:
- x.back();
- boolean currentCharIsQuote = x.getPrevious() == '"';
- boolean quoteIsNotNextToValidChar = x.getPreviousChar() != ',' && x.getPreviousChar() != '[';
-
- if (strictMode && currentCharIsQuote && quoteIsNotNextToValidChar) {
- throw x.syntaxError(getInvalidCharErrorMsg(cursor));
- }
-
- this.myArrayList.add(x.nextValue(jsonParserConfiguration));
- parseTokener(x, jsonParserConfiguration);
+ if (checkForSyntaxError(x, jsonParserConfiguration, isInitial)) return;
+ }
+ } else {
+ if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) {
+ throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text");
+ }
}
}
- /**
- * Throws JSONException if JSONTokener has reached end of file, usually when array is unclosed. No ']' found,
- * instead EoF.
- *
- * @param x the JSONTokener being evaluated.
- * @throws JSONException if JSONTokener has reached end of file.
- */
- private void throwErrorIfEoF(JSONTokener x) {
- if (x.end()) {
- throw x.syntaxError(String.format("Expected a ',' or ']' but instead found '%s'", x.getPrevious()));
+ /** 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;
}
/**
@@ -187,19 +184,22 @@ private void throwErrorIfEoF(JSONTokener x) {
* If there is a syntax error.
*/
public JSONArray(String source) throws JSONException {
- this(new JSONTokener(source), new JSONParserConfiguration());
+ this(source, new JSONParserConfiguration());
}
/**
- * Constructs a JSONArray from a source JSON text and a JSONParserConfiguration.
+ * Construct a JSONArray from a source JSON text.
*
- * @param source A string that begins with [ (left bracket) and
- * ends with ] (right bracket).
- * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
- * @throws JSONException If there is a syntax error.
+ * @param source
+ * A string that begins with [ (left
+ * bracket) and ends with ]
+ * (right bracket).
+ * @param jsonParserConfiguration the parser config object
+ * @throws JSONException
+ * If there is a syntax error.
*/
public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
- this(new JSONTokener(source), jsonParserConfiguration);
+ this(new JSONTokener(source, jsonParserConfiguration), jsonParserConfiguration);
}
/**
@@ -348,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);
@@ -428,7 +426,7 @@ public Number getNumber(int index) throws JSONException {
/**
* Get the enum value associated with an index.
- *
+ *
* @param
* Enum Type
* @param clazz
@@ -616,7 +614,7 @@ public String join(String separator) throws JSONException {
if (len == 0) {
return "";
}
-
+
StringBuilder sb = new StringBuilder(
JSONObject.valueToString(this.myArrayList.get(0)));
@@ -749,11 +747,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();
}
/**
@@ -785,11 +779,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();
}
/**
@@ -821,11 +811,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();
}
/**
@@ -857,11 +843,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();
}
/**
@@ -930,7 +912,7 @@ public Integer optIntegerObject(int index, Integer defaultValue) {
/**
* Get the enum value associated with a key.
- *
+ *
* @param
* Enum Type
* @param clazz
@@ -945,7 +927,7 @@ public > E optEnum(Class clazz, int index) {
/**
* Get the enum value associated with a key.
- *
+ *
* @param
* Enum Type
* @param clazz
@@ -978,8 +960,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
@@ -994,8 +976,8 @@ public BigInteger optBigInteger(int index, BigInteger defaultValue) {
}
/**
- * Get the optional BigDecimal value associated with an index. The
- * defaultValue is returned if there is no value for the index, or if the
+ * Get the optional BigDecimal 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. If the value
* is float or double, the {@link BigDecimal#BigDecimal(double)}
* constructor will be used. See notes on the constructor for conversion
@@ -1164,7 +1146,7 @@ public Number optNumber(int index, Number defaultValue) {
if (val instanceof Number){
return (Number) val;
}
-
+
if (val instanceof String) {
try {
return JSONObject.stringToNumber((String) val);
@@ -1241,7 +1223,7 @@ public JSONArray put(Collection> value) {
public JSONArray put(double value) throws JSONException {
return this.put(Double.valueOf(value));
}
-
+
/**
* Append a float value. This increases the array's length by one.
*
@@ -1496,19 +1478,19 @@ public JSONArray put(int index, Object value) throws JSONException {
*
* @param collection
* A Collection.
- * @return this.
+ * @return this.
*/
public JSONArray putAll(Collection> collection) {
this.addAll(collection, false);
return this;
}
-
+
/**
* Put an Iterable's elements in to the JSONArray.
*
* @param iter
* An Iterable.
- * @return this.
+ * @return this.
*/
public JSONArray putAll(Iterable> iter) {
this.addAll(iter, false);
@@ -1520,7 +1502,7 @@ public JSONArray putAll(Iterable> iter) {
*
* @param array
* A JSONArray.
- * @return this.
+ * @return this.
*/
public JSONArray putAll(JSONArray array) {
// directly copy the elements from the source array to this one
@@ -1535,7 +1517,7 @@ public JSONArray putAll(JSONArray array) {
* @param array
* Array. If the parameter passed is null, or not an array or Iterable, an
* exception will be thrown.
- * @return this.
+ * @return this.
*
* @throws JSONException
* If not an array, JSONArray, Iterable or if an value is non-finite number.
@@ -1546,9 +1528,9 @@ public JSONArray putAll(Object array) throws JSONException {
this.addAll(array, false);
return this;
}
-
+
/**
- * Creates a JSONPointer using an initialization string and tries to
+ * Creates a JSONPointer using an initialization string and tries to
* match it to an item within this JSONArray. For example, given a
* JSONArray initialized with this document:
*
- * and this JSONPointer string:
+ * and this JSONPointer string:
*
* "/0/b"
*
@@ -1569,9 +1551,9 @@ public JSONArray putAll(Object array) throws JSONException {
public Object query(String jsonPointer) {
return query(new JSONPointer(jsonPointer));
}
-
+
/**
- * Uses a user initialized JSONPointer and tries to
+ * Uses a user initialized JSONPointer and tries to
* match it to an item within this JSONArray. For example, given a
* JSONArray initialized with this document:
*
* Warning: This method assumes that the data structure is acyclical.
*
- *
+ *
* @param indentFactor
* The number of spaces to add to each level of indentation.
* @return a printable, displayable, transmittable representation of the
@@ -1778,11 +1775,11 @@ public Writer write(Writer writer) throws JSONException {
/**
* Write the contents of the JSONArray as JSON text to a writer.
- *
+ *
*
If
{@code indentFactor > 0}
and the {@link JSONArray} has only
* one element, then the array will be output on a single line:
*
{@code [1]}
- *
+ *
*
If an array has 2 or more elements, then it will be output across
* multiple lines:
{@code
* [
@@ -1813,12 +1810,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;
@@ -1830,12 +1822,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) {
@@ -1850,6 +1837,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
@@ -1962,7 +1969,6 @@ private void addAll(Object array, boolean wrap) throws JSONException {
private void addAll(Object array, boolean wrap, int recursionDepth) {
addAll(array, wrap, recursionDepth, new JSONParserConfiguration());
}
-
/**
* Add an array's elements to the JSONArray.
*`
@@ -2001,7 +2007,7 @@ private void addAll(Object array, boolean wrap, int recursionDepth, JSONParserCo
// JSONArray
this.myArrayList.addAll(((JSONArray)array).myArrayList);
} else if (array instanceof Collection) {
- this.addAll((Collection>)array, wrap, recursionDepth);
+ this.addAll((Collection>)array, wrap, recursionDepth, jsonParserConfiguration);
} else if (array instanceof Iterable) {
this.addAll((Iterable>)array, wrap);
} else {
@@ -2009,6 +2015,7 @@ private void addAll(Object array, boolean wrap, int recursionDepth, JSONParserCo
"JSONArray initial value should be a string or collection or array.");
}
}
+
/**
* Create a new JSONException in a common format for incorrect conversions.
* @param idx index of the item
@@ -2037,7 +2044,4 @@ private static JSONException wrongValueFormatException(
, cause);
}
- private static String getInvalidCharErrorMsg(char cursor) {
- return String.format("invalid character '%s' found after end of array", cursor);
- }
}
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 642e96703..a4f1e7c85 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 extends Map> 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
}
}
}
@@ -195,7 +199,7 @@ public JSONObject(JSONObject jo, String ... names) {
* duplicated key.
*/
public JSONObject(JSONTokener x) throws JSONException {
- this(x, new JSONParserConfiguration());
+ this(x, x.getJsonParserConfiguration());
}
/**
@@ -211,63 +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) {
- case 0:
- throw x.syntaxError("A JSONObject text must end with '}'");
- case '}':
- return;
- default:
- key = x.nextSimpleValue(c, jsonParserConfiguration).toString();
+ if (parseJSONObject(x, jsonParserConfiguration, isInitial)) {
+ return;
}
+ }
+ }
- // The key is followed by ':'.
+ /**
+ * 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();
- c = x.nextClean();
- if (c != ':') {
- throw x.syntaxError("Expected a ':' after a key");
- }
+ switch (c) {
+ case 0:
+ throw jsonTokener.syntaxError("A JSONObject text must end with '}'");
+ case '}':
+ if (isInitial && jsonParserConfiguration.isStrictMode() && jsonTokener.nextClean() != 0) {
+ throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text");
+ }
+ return true;
+ default:
+ obj = jsonTokener.nextSimpleValue(c);
+ key = obj.toString();
+ }
- // Use syntaxError(..) to include error location
+ checkKeyForStrictMode(jsonTokener, jsonParserConfiguration, obj);
- if (key != null) {
- // Check if key exists
- boolean keyExists = this.opt(key) != null;
- if (keyExists && !jsonParserConfiguration.isOverwriteDuplicateKey()) {
- throw x.syntaxError("Duplicate key \"" + key + "\"");
- }
+ // The key is followed by ':'.
+ c = jsonTokener.nextClean();
+ if (c != ':') {
+ throw jsonTokener.syntaxError("Expected a ':' after a key");
+ }
- Object value = x.nextValue(jsonParserConfiguration);
- // Only add value if non-null
- if (value != null) {
- this.put(key, value);
- }
+ // 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 + "\"");
}
- // Pairs are separated by ','.
+ Object value = jsonTokener.nextValue();
+ // Only add value if non-null
+ if (value != null) {
+ this.put(key, value);
+ }
+ }
- switch (x.nextClean()) {
+ // Pairs are separated by ','.
+ if (parseEndOfKeyValuePair(jsonTokener, jsonParserConfiguration, isInitial)) {
+ doneParsing = true;
+ }
+
+ 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 jsonTokener.syntaxError("Strict mode error: Invalid character ';' found");
+ }
+ break;
case ',':
- if (x.nextClean() == '}') {
- return;
+ if (jsonTokener.nextClean() == '}') {
+ // trailing commas are not allowed in strict mode
+ if (jsonParserConfiguration.isStrictMode()) {
+ throw jsonTokener.syntaxError("Strict mode error: Expected another object element");
+ }
+ // 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 '}':
- return;
+ if (isInitial && jsonParserConfiguration.isStrictMode() && jsonTokener.nextClean() != 0) {
+ throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text");
+ }
+ // 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()));
}
}
}
@@ -316,7 +391,7 @@ private JSONObject(Map, ?> m, int recursionDepth, JSONParserConfiguration json
throw new NullPointerException("Null key.");
}
final Object value = e.getValue();
- if (value != null) {
+ if (value != null || jsonParserConfiguration.isUseNativeNulls()) {
testValidity(value);
this.map.put(String.valueOf(e.getKey()), wrap(value, recursionDepth + 1, jsonParserConfiguration));
}
@@ -385,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