Compare commits

..

19 Commits

Author SHA1 Message Date
Sean Leary
ff264ef647 Enhance README with license clarification
Added license clarification
2026-02-18 14:50:17 -06:00
Sean Leary
a37aa69480 Merge pull request #1039 from pratiktiwari13/bugfix/empty-force-list
Fixes the issue of losing the array if an empty forceList element or a tag is in the middle or the end
2026-02-03 11:15:58 -06:00
Pratik Tiwari
510a03ac36 Fixes #1040, Aligns non-forceList behaviour with forceList 2026-01-31 10:34:24 +05:30
Sean Leary
538afc3d78 Merge pull request #1038 from OwenSanzas/fix-xmltokener-unescapeentity
Fix input validation in XMLTokener.unescapeEntity()
2026-01-30 08:13:34 -06:00
Sean Leary
d092d0903c Merge pull request #1037 from OwenSanzas/fix-jsonml-classcast
Fix ClassCastException in JSONML.toJSONArray and toJSONObject
2026-01-30 08:12:16 -06:00
Pratik Tiwari
7a8da886e7 Remove unnecessary conditions 2026-01-30 19:29:46 +05:30
OwenSanzas
0737e04f8a Add unit tests for JSONML ClassCastException fix
Added comprehensive test coverage for safe type casting:

Exception cases (should throw JSONException, not ClassCastException):
- Malformed XML causing type mismatch in toJSONArray()
- Type mismatch in toJSONObject()

Valid cases (should continue to work):
- Valid XML to JSONArray conversion
- Valid XML to JSONObject conversion

These tests verify the fix for issue #1034 where ClassCastException
was thrown when parse() returned unexpected types.
2026-01-28 10:07:34 +00:00
OwenSanzas
592e7828d9 Add unit tests for XMLTokener.unescapeEntity() input validation
Added comprehensive test coverage for numeric character reference parsing:

Exception cases (should throw JSONException):
- Empty numeric entity: &#;
- Invalid decimal entity: &#txx;
- Empty hex entity: &#x;
- Invalid hex characters: &#xGGG;

Valid cases (should parse correctly):
- Decimal entity: A -> 'A'
- Lowercase hex entity: A -> 'A'
- Uppercase hex entity: A -> 'A'

These tests verify the fixes for issues #1035 and #1036.
2026-01-28 09:58:35 +00:00
OwenSanzas
6c1bfbc7a5 Refactor XMLTokener.unescapeEntity() to reduce complexity
Extracted hex and decimal parsing logic into separate methods to
address SonarQube complexity warning:
- parseHexEntity(): handles ઼ format
- parseDecimalEntity(): handles { format

This reduces cyclomatic complexity while maintaining identical
functionality and all validation checks.
2026-01-28 09:52:25 +00:00
OwenSanzas
534ce3c4d1 Fix input validation in XMLTokener.unescapeEntity()
Fix StringIndexOutOfBoundsException and NumberFormatException in
XMLTokener.unescapeEntity() when parsing malformed XML numeric
character references.

Issues:
- &#; (empty numeric reference) caused StringIndexOutOfBoundsException
- &#txx; (invalid decimal) caused NumberFormatException
- &#xGGG; (invalid hex) caused NumberFormatException

Changes:
- Add length validation before accessing character positions
- Add isValidHex() and isValidDecimal() helper methods
- Throw proper JSONException with descriptive messages

Fixes #1035, Fixes #1036
2026-01-27 11:40:18 +00:00
OwenSanzas
9d14246bee Fix ClassCastException in JSONML.toJSONArray and toJSONObject
Add type checking before casting parse() results to JSONArray/JSONObject.
When parse() returns an unexpected type (e.g., String for malformed input),
the code now throws a descriptive JSONException instead of ClassCastException.

This prevents unchecked exceptions from propagating to callers who only
expect JSONException from these methods.

Fixes #1034
2026-01-27 11:36:46 +00:00
Pratik Tiwari
995fb840f7 Fixes the issue of losing the array if an empty forceList element or a tag is in the middle or the end 2026-01-02 21:20:53 +05:30
Sean Leary
e635f40238 Merge pull request #1027 from Simulant87/1023-set-default-locale
Save/restore default locale in test
2025-12-29 19:42:57 -06:00
Sean Leary
d5e744ca90 Merge pull request #1028 from Simulant87/fix-sonarqube-reliability-issues
Refactoring: Fix sonarqube reliability issues
2025-12-29 19:42:02 -06:00
Sean Leary
e0c4086168 Merge pull request #1029 from Simulant87/external-javadoc-badge
add badge to external hosted javadoc
2025-12-29 19:41:31 -06:00
Sean Leary
cf653682be Merge pull request #1030 from stleary/pre-release-20251224
pre-release-20251224 Prep for next release
2025-12-24 09:16:47 -06:00
Simulant
96353de304 add badge to external hosted javadoc 2025-12-21 23:16:01 +01:00
Simulant
8cbb4d5bb3 Fix sonarqube reliability issues 2025-12-20 22:57:24 +01:00
Simulant
421abfdc1f save and restore the current default locale, to avoid any side effects on other executions in the same JVM 2025-12-20 22:27:45 +01:00
10 changed files with 487 additions and 58 deletions

3
.gitignore vendored
View File

@@ -16,3 +16,6 @@ build
/gradlew
/gradlew.bat
.gitmodules
# ignore compiled class files
*.class

View File

@@ -9,6 +9,7 @@ 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/20251224/json-20251224.jar)**
@@ -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
@@ -28,8 +31,16 @@ Project goals include:
* Maintain backward compatibility
* 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
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.
The files in this package implement JSON encoders and decoders. The package can also convert between JSON and XML, HTTP headers, Cookies, and CDL.
# If you would like to contribute to this project

View File

@@ -22,6 +22,33 @@ public class JSONML {
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.
@@ -276,7 +303,7 @@ public class JSONML {
* @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));
}
@@ -298,7 +325,7 @@ public class JSONML {
* @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));
}
@@ -323,7 +350,7 @@ public class JSONML {
* @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));
}
@@ -347,7 +374,7 @@ public class JSONML {
* @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));
}
@@ -369,7 +396,7 @@ public class JSONML {
* @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));
}
@@ -386,7 +413,7 @@ public class JSONML {
* @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));
}
@@ -404,7 +431,7 @@ public class JSONML {
* @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));
}
@@ -424,7 +451,7 @@ public class JSONML {
* @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));
}
@@ -446,7 +473,7 @@ public class JSONML {
* @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));
}
@@ -464,7 +491,7 @@ public class JSONML {
* @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));
}
@@ -484,7 +511,7 @@ public class JSONML {
* @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));
}
@@ -506,7 +533,7 @@ public class JSONML {
* @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));
}

View File

@@ -9,6 +9,7 @@ import java.io.StringReader;
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 @@ public class XML {
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private int nextIndex = 0;
private int length = string.length();
private final int length = string.length();
@Override
public boolean hasNext() {
@@ -89,6 +90,9 @@ public class XML {
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
int result = string.codePointAt(this.nextIndex);
this.nextIndex += Character.charCount(result);
return result;
@@ -387,8 +391,13 @@ public class XML {
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) {
@@ -447,7 +456,11 @@ public class XML {
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()));

View File

@@ -151,33 +151,108 @@ public class XMLTokener extends JSONTokener {
/**
* 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: &#;");
}
return new String(new int[] {cp},0,1);
int cp = (e.charAt(1) == 'x' || e.charAt(1) == 'X')
? parseHexEntity(e)
: parseDecimalEntity(e);
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., "&#xABC;").
* @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., "&#123;").
* @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;
}
/**
* <pre>{@code

View File

@@ -986,4 +986,70 @@ public class JSONMLTest {
}
}
/**
* 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 = "<root><item>value</item></root>";
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 = "<root attr=\"value\"><item>content</item></root>";
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());
}
}
}

View File

@@ -36,25 +36,31 @@ public class JSONObjectLocaleTest {
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"));
// 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"));
/**
* 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);
}
}
}

View File

@@ -3117,12 +3117,13 @@ public class JSONObjectTest {
// test a more complex object
writer = new StringWriter();
try {
new JSONObject()
JSONObject object = new JSONObject()
.put("somethingElse", "a value")
.put("someKey", new JSONArray()
.put(new JSONObject().put("key1", new BrokenToString())))
.write(writer).toString();
.put(new JSONObject().put("key1", new BrokenToString())));
try {
object.write(writer).toString();
fail("Expected an exception, got a String value");
} catch (JSONException e) {
assertEquals("Unable to write JSONObject value for key: someKey", e.getMessage());
@@ -3136,14 +3137,15 @@ public class JSONObjectTest {
// test a more slightly complex object
writer = new StringWriter();
try {
new JSONObject()
object = new JSONObject()
.put("somethingElse", "a value")
.put("someKey", new JSONArray()
.put(new JSONObject().put("key1", new BrokenToString()))
.put(12345)
)
.write(writer).toString();
);
try {
object.write(writer).toString();
fail("Expected an exception, got a String value");
} catch (JSONException e) {
assertEquals("Unable to write JSONObject value for key: someKey", e.getMessage());

View File

@@ -1092,7 +1092,7 @@ public class XMLConfigurationTest {
"<addresses></addresses>";
String expectedStr =
"{\"addresses\":[]}";
"{\"addresses\":[\"\"]}";
Set<String> forceList = new HashSet<String>();
forceList.add("addresses");
@@ -1130,7 +1130,7 @@ public class XMLConfigurationTest {
"<addresses />";
String expectedStr =
"{\"addresses\":[]}";
"{\"addresses\":[\"\"]}";
Set<String> forceList = new HashSet<String>();
forceList.add("addresses");
@@ -1144,6 +1144,157 @@ public class XMLConfigurationTest {
Util.compareActualVsExpectedJsonObjects(jsonObject, expetedJsonObject);
}
@Test
public void testForceListWithLastElementAsEmptyTag(){
final String originalXml = "<root><id>1</id><id/></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}";
HashSet<String> 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 = "<root><id/><id>1</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}";
HashSet<String> 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 = "<root><id>1</id><id/><id>2</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}";
HashSet<String> 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 = "<root><id>1</id><id></id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}";
HashSet<String> 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 = "<root><id></id><id>1</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}";
HashSet<String> 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 = "<root><id>1</id><id></id><id>2</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}";
HashSet<String> 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 = "<root><id></id><id/><id>1</id><id/><id></id><id>2</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\",\"\",1,\"\",\"\",2]}}";
HashSet<String> 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 = "<root><id>0</id><id>1</id><id/><id></id></root>";
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<String> 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 = "<root><id></id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}";
HashSet<String> 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 = "<root><id/></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}";
HashSet<String> 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;

View File

@@ -1426,6 +1426,81 @@ public class XMLTest {
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 = "<a>&#;</a>";
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 = "<a>&#txx;</a>";
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 = "<a>&#x;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that invalid hex entity &#xGGG; throws JSONException.
* Validates hex digit validation.
*/
@Test(expected = JSONException.class)
public void testInvalidHexEntityThrowsJSONException() {
String xmlStr = "<a>&#xGGG;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that valid decimal numeric entity &#65; works correctly.
* Should decode to character 'A'.
*/
@Test
public void testValidDecimalEntity() {
String xmlStr = "<a>&#65;</a>";
JSONObject jsonObject = XML.toJSONObject(xmlStr);
assertEquals("A", jsonObject.getString("a"));
}
/**
* Tests that valid hex numeric entity &#x41; works correctly.
* Should decode to character 'A'.
*/
@Test
public void testValidHexEntity() {
String xmlStr = "<a>&#x41;</a>";
JSONObject jsonObject = XML.toJSONObject(xmlStr);
assertEquals("A", jsonObject.getString("a"));
}
/**
* Tests that valid uppercase hex entity &#X41; works correctly.
* Should decode to character 'A'.
*/
@Test
public void testValidUppercaseHexEntity() {
String xmlStr = "<a>&#X41;</a>";
JSONObject jsonObject = XML.toJSONObject(xmlStr);
assertEquals("A", jsonObject.getString("a"));
}
}