Compare commits

...

34 Commits

Author SHA1 Message Date
Sean Leary
78137d389d
Merge pull request #1001 from marilynel/master
addressing sonarqube concerns in JSONObject
2025-07-31 20:54:08 -05:00
marilynel
38c3a0bb3f more sonarcube issues 2025-07-27 11:45:07 -08:00
marilynel
ebd9a17a3b addressing minor sonarqube concerns 2025-07-27 11:26:50 -08:00
Sean Leary
82432f0245
Merge pull request #1000 from marilynel/master
fixing sonarcube issues
2025-07-23 20:46:33 -05:00
marilynel
e762629bcc oops one more sonarcube issue lol 2025-07-20 12:04:51 -08:00
marilynel
7fc41a6c0e addressing cognitive complextity 2025-07-20 11:58:30 -08:00
marilynel
d5d82cdb87 fixing sonarcube issues 2025-07-20 11:31:29 -08:00
Sean Leary
0a9364e920
Merge pull request #999 from marilynel/master
fixed some strict mode issues
2025-07-16 20:12:57 -05:00
marilynel
c91b728386 oops forgot null 2025-07-13 12:52:42 -08:00
marilynel
fdaeb486ed fixed some strict mode issues 980 2025-07-13 12:41:17 -08:00
Sean Leary
f0a78aff61
Merge pull request #995 from marilynel/master
Fix regression XML parsing null with keepStrings
2025-07-09 20:18:48 -05:00
Sean Leary
a79e8a15e5
Merge pull request #994 from stleary/tech-debt-20250701
tech-debt-25250701
2025-07-08 11:04:29 -05:00
marilynel
7bb3df8ebf added test details 2025-07-06 12:41:44 -08:00
marilynel
3dce55794f fixed keeping null as string 2025-07-06 12:37:05 -08:00
Sean Leary
d7593fb808
Merge pull request #992 from surajdm123/add-tests
Added JUnit test cases for HTTPTokener
2025-07-06 08:27:59 -05:00
Sean Leary
1eed44a59e
Merge pull request #993 from surajdm123/add-tests-2
Added JUnit tests for XMLTokenerTest
2025-07-06 08:27:20 -05:00
Sean Leary
7eccadefcd
Merge pull request #991 from Simulant87/update-codeql-v3
update CodeQL to v3
2025-07-04 16:39:18 -05:00
Sean Leary
7b0d1942b4 tech-debt-25250701 add jacoco to gradle build, refactor JSONObject to restore performance 2025-07-03 20:39:13 -05:00
surajdm123
a729c2077a Added JUnit tests for XMLTokenerTest 2025-07-03 01:23:46 -07:00
surajdm123
7ac773be72 Added JUnit test cases for HTTPTokener 2025-07-03 00:58:15 -07:00
Simulant
7da120e631 update CodeQL to v3 2025-07-01 22:57:36 +02:00
Sean Leary
197afddbfb
Merge pull request #990 from Simulant87/984-refactor-cognitive-complexity-populateMap
Refactor JSONObject populateMap() per SonarQube
2025-07-01 07:13:18 -05:00
Sean Leary
1bdaacc8b0
Merge pull request #989 from AlexCai2019/master
Minor refactoring
2025-06-27 20:17:17 -05:00
Alex Cai
c882783d58
Format line 2755 in JSONObject.java 2025-06-27 01:44:27 +08:00
Simulant
5063d314a5 #984 extract method for annotation value check 2025-06-25 23:08:01 +02:00
Simulant
916fba5d39 #984 extract methods reducing cognitive complexity
for JSONObject#populateMap
2025-06-25 23:00:07 +02:00
AlexCai2019
aac376f305 Remove a redundant condition and an empty string
Remove "NULL.equals(object)" on line 2756 of JSONObject.java since line 2752 has already tested it.
Remove the empty string on line 249 of JSONPointer.java.
2025-06-23 01:31:51 +08:00
Sean Leary
32e56da786
Merge pull request #988 from stleary/remove-unused-code-jsonobject
removed unused method from jsonobject
2025-06-16 11:34:35 -05:00
Sean Leary
50330430ce remove-unused-code-jsonobject removed unused method from jsonobject 2025-06-07 16:15:43 -05:00
Sean Leary
f1935f5254
Merge pull request #987 from AlexCai2019/master
Use constant.equals()
2025-06-07 09:59:48 -05:00
AlexCai2019
e800cc349f Use constant.equals()
There are some equals() that are not constant.equals(variable), but variable.equals(constant)
2025-06-05 02:15:49 +08:00
Sean Leary
72a1a48173
Merge pull request #983 from harshith8854/master
Use JSONParserConfiguration to decide on serializing Null fields into JSONObject #982
2025-05-31 09:58:46 -05:00
hboggavarapu
a381060f81 Add testcase to assert Null fields serialization without JSONParserConfiguration 2025-05-24 21:54:12 +05:30
hboggavarapu
dadc3e59dc Use JSONParserConfiguration to decide on serializing null fields into JSONObject #982 2025-05-23 17:57:08 +05:30
12 changed files with 544 additions and 155 deletions

View File

@ -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

View File

@ -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'

View File

@ -334,13 +334,11 @@ public class JSONArray implements Iterable<Object> {
*/
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);

View File

@ -111,7 +111,7 @@ public class JSONML {
}
} 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());
}

View File

@ -79,17 +79,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.
*
@ -180,7 +169,7 @@ public class JSONObject {
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,45 +200,62 @@ public class JSONObject {
*/
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();
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();
obj = jsonTokener.nextSimpleValue(c);
key = obj.toString();
}
checkKeyForStrictMode(jsonTokener, jsonParserConfiguration, obj);
// The key is followed by ':'.
c = x.nextClean();
c = jsonTokener.nextClean();
if (c != ':') {
throw x.syntaxError("Expected a ':' after a key");
throw jsonTokener.syntaxError("Expected a ':' after a 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 x.syntaxError("Duplicate key \"" + key + "\"");
throw jsonTokener.syntaxError("Duplicate key \"" + key + "\"");
}
Object value = x.nextValue();
Object value = jsonTokener.nextValue();
// Only add value if non-null
if (value != null) {
this.put(key, value);
@ -257,33 +263,71 @@ public class JSONObject {
}
// 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 +445,17 @@ public class JSONObject {
*/
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<Object> objectsRecord) {
this();
this.populateMap(bean, objectsRecord);
this.populateMap(bean, objectsRecord, new JSONParserConfiguration());
}
/**
@ -674,13 +723,11 @@ public class JSONObject {
*/
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);
@ -1764,31 +1811,45 @@ public class JSONObject {
* @throws JSONException
* If a getter returned a non-finite number.
*/
private void populateMap(Object bean) {
populateMap(bean, Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()));
private void populateMap(Object bean, JSONParserConfiguration jsonParserConfiguration) {
populateMap(bean, Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()), jsonParserConfiguration);
}
private void populateMap(Object bean, Set<Object> 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<Object> objectsRecord, JSONParserConfiguration jsonParserConfiguration) {
Class<?> klass = bean.getClass();
// If klass is a System class then set includeSuperClass to false.
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())) {
if (isValidMethod(method)) {
final String key = getKeyNameFromMethod(method);
if (key != null && !key.isEmpty()) {
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<Object> objectsRecord, JSONParserConfiguration jsonParserConfiguration,
Method method, String key) {
try {
final Object result = method.invoke(bean);
if (result != null) {
if (result != null || jsonParserConfiguration.isUseNativeNulls()) {
// check cyclic dependency and throw error if needed
// the wrap and populateMap combination method is
// itself DFS recursive
@ -1803,23 +1864,26 @@ public class JSONObject {
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) {
}
}
closeClosable(result);
}
} catch (IllegalAccessException ignore) {
// ignore exception
} catch (IllegalArgumentException ignore) {
// ignore exception
} catch (InvocationTargetException ignore) {
// ignore exception
}
}
}
}
/**
* 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) {
@ -1837,7 +1901,7 @@ public class JSONObject {
}
}
JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class);
if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) {
if (annotationValueNotEmpty(annotation)) {
return annotation.value();
}
String key;
@ -1863,6 +1927,46 @@ public class JSONObject {
return key;
}
/**
* 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) {
}
}
}
/**
* Searches the class hierarchy to see if the method or it's super
* implementations and interfaces has the annotation.
@ -1906,7 +2010,7 @@ public class JSONObject {
}
//If the superclass is Object, no annotations will be found any more
if (c.getSuperclass().equals(Object.class))
if (Object.class.equals(c.getSuperclass()))
return null;
try {
@ -1964,7 +2068,7 @@ public class JSONObject {
}
//If the superclass is Object, no annotations will be found any more
if (c.getSuperclass().equals(Object.class))
if (Object.class.equals(c.getSuperclass()))
return -1;
try {
@ -2750,13 +2854,13 @@ public class JSONObject {
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;
}
@ -3010,24 +3114,4 @@ public class JSONObject {
"JavaBean object contains recursively defined member variable of key " + quote(key)
);
}
/**
* For a prospective number, remove the leading zeros
* @param value prospective number
* @return number without leading zeros
*/
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);
}
++counter;
}
if (negativeFirstChar) {return "-0";}
return "0";
}
}

View File

@ -127,7 +127,7 @@ public class JSONPointer {
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 @@ public class JSONPointer {
*/
@Override
public String toString() {
StringBuilder rval = new StringBuilder("");
StringBuilder rval = new StringBuilder();
for (String token: this.refTokens) {
rval.append('/').append(escape(token));
}

View File

@ -511,12 +511,22 @@ public class JSONTokener {
throw this.syntaxError("Missing value");
}
Object obj = JSONObject.stringToValue(string);
// Strict mode only allows strings with explicit double quotes
// if obj is a boolean, look at string
if (jsonParserConfiguration != null &&
jsonParserConfiguration.isStrictMode() &&
obj instanceof String) {
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;
}

View File

@ -428,6 +428,9 @@ public class XML {
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));
}

View File

@ -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);
}
}

View File

@ -3997,6 +3997,56 @@ public class JSONObjectTest {
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
*
@ -4012,4 +4062,36 @@ public class JSONObjectTest {
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.
*
* <p>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.</p>
*/
@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}.
*
* <p>This test ensures that uninitialized fields in the bean are not serialized
* into the resulting JSON object, and the object remains empty.</p>
*/
@Test
public void jsonObjectParseNullFieldsWithoutParserConfiguration() {
RecursiveBean bean = new RecursiveBean(null);
JSONObject jsonObject = new JSONObject(bean);
assertTrue("JSONObject should be empty", jsonObject.isEmpty());
}
}

View File

@ -775,8 +775,8 @@ public class XMLConfigurationTest {
*/
@Test
public void testToJSONArray_jsonOutput_withKeepNumberAsString() {
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\"],\"title\":true}}");
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><id>null</id><item id=\"01\"/><title>True</title></root>";
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);
@ -787,13 +787,25 @@ public class XMLConfigurationTest {
*/
@Test
public void testToJSONArray_jsonOutput_withKeepBooleanAsString() {
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",1,\"00\",0],\"title\":\"True\"}}");
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><id>null</id><item id=\"01\"/><title>True</title></root>";
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 = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>null</title></root>";
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
*/

View File

@ -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 <![CDATA[ some <CDATA> content ]]> after";
XMLTokener tokener = new XMLTokener(new StringReader(xml));
tokener.skipPast("<![CDATA[");
String cdata = tokener.nextCDATA();
assertEquals(" some <CDATA> content ", cdata);
}
/**
* Tests that nextContent() returns plain text content before a tag.
*/
@Test
public void testNextContentWithText() {
String xml = "Some content<nextTag>";
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 = "<tag>";
XMLTokener tokener = new XMLTokener(xml);
Object content = tokener.nextContent();
assertEquals('<', content);
}
/**
* Tests that nextEntity() resolves a known entity like &amp; 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"
}
}