Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/main/java/org/json/JSONArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private static boolean checkForSyntaxError(JSONTokener x, JSONParserConfiguratio
* If there is a syntax error.
*/
public JSONArray(String source) throws JSONException {
this(source, new JSONParserConfiguration());
this(source, JSONParserConfiguration.getInstance());
}

/**
Expand All @@ -209,7 +209,7 @@ public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration)
* A Collection.
*/
public JSONArray(Collection<?> collection) {
this(collection, 0, new JSONParserConfiguration());
this(collection, 0, JSONParserConfiguration.getInstance());
}

/**
Expand Down Expand Up @@ -1410,7 +1410,7 @@ public JSONArray put(int index, long value) throws JSONException {
* If a key in the map is <code>null</code>
*/
public JSONArray put(int index, Map<?, ?> value) throws JSONException {
this.put(index, new JSONObject(value, new JSONParserConfiguration()));
this.put(index, new JSONObject(value, JSONParserConfiguration.getInstance()));
return this;
}

Expand Down Expand Up @@ -1967,7 +1967,7 @@ private void addAll(Object array, boolean wrap) throws JSONException {
* Variable for tracking the count of nested object creations.
*/
private void addAll(Object array, boolean wrap, int recursionDepth) {
addAll(array, wrap, recursionDepth, new JSONParserConfiguration());
addAll(array, wrap, recursionDepth, JSONParserConfiguration.getInstance());
}
/**
* Add an array's elements to the JSONArray.
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ private static void checkKeyForStrictMode(JSONTokener jsonTokener, JSONParserCon
* If a key in the map is <code>null</code>
*/
public JSONObject(Map<?, ?> m) {
this(m, 0, new JSONParserConfiguration());
this(m, 0, JSONParserConfiguration.getInstance());
}

/**
Expand Down Expand Up @@ -460,7 +460,7 @@ private JSONObject(Map<?, ?> m, int recursionDepth, JSONParserConfiguration json
*/
public JSONObject(Object bean) {
this();
this.populateMap(bean, new JSONParserConfiguration());
this.populateMap(bean, JSONParserConfiguration.getInstance());
}

public JSONObject(Object bean, JSONParserConfiguration jsonParserConfiguration) {
Expand All @@ -470,7 +470,7 @@ public JSONObject(Object bean, JSONParserConfiguration jsonParserConfiguration)

private JSONObject(Object bean, Set<Object> objectsRecord) {
this();
this.populateMap(bean, objectsRecord, new JSONParserConfiguration());
this.populateMap(bean, objectsRecord, JSONParserConfiguration.getInstance());
}

/**
Expand Down Expand Up @@ -513,7 +513,7 @@ public JSONObject(Object object, String ... names) {
* duplicated key.
*/
public JSONObject(String source) throws JSONException {
this(source, new JSONParserConfiguration());
this(source, JSONParserConfiguration.getInstance());
}

/**
Expand Down Expand Up @@ -2960,7 +2960,7 @@ static Object wrap(Object object, int recursionDepth, JSONParserConfiguration js
}

private static Object wrap(Object object, Set<Object> objectsRecord) {
return wrap(object, objectsRecord, 0, new JSONParserConfiguration());
return wrap(object, objectsRecord, 0, JSONParserConfiguration.getInstance());
}

private static Object wrap(Object object, Set<Object> objectsRecord, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) {
Expand Down
41 changes: 40 additions & 1 deletion src/main/java/org/json/JSONParserConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@
* Configuration object for the JSON parser. The configuration is immutable.
*/
public class JSONParserConfiguration extends ParserConfiguration {

/**
* system-wide default configuration.
*/
private static JSONParserConfiguration globalConfig = null;

/**
* Sets the system-wide global JSONParserConfiguration.
* This can only be called once during the application's lifecycle.
*
* @param configuration The configuration to set globally.
* @throws IllegalStateException if the configuration is already set.
* @throws IllegalArgumentException if the provided config is null.
*/
public static void setGlobalConfiguration(JSONParserConfiguration configuration){
if (configuration == null) {
throw new IllegalArgumentException("Global JSONParserConfiguration cannot be null.");
}
if(globalConfig == null){
globalConfig = configuration;
} else {
throw new IllegalStateException("Global JSONParserConfiguration has already been set. It cannot be modified.");
}
}

/**
* Retrieves the system-wide global JSONParserConfiguration.
* If one hasn't been explicitly set, it returns a fresh instance with standard defaults.
*
* @return The active global JSONParserConfiguration.
*/
static JSONParserConfiguration getInstance() {
JSONParserConfiguration config = globalConfig;
if (config == null) {
return new JSONParserConfiguration();
}
return config;
}

/**
* Used to indicate whether to overwrite duplicate key or not.
*/
Expand Down Expand Up @@ -73,7 +112,7 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite

return clone;
}

/**
* Controls the parser's behavior when meeting Java null values while converting maps.
* If set to true, the parser will put a JSONObject.NULL into the resulting JSONObject.
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/json/JSONTokener.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class JSONTokener {
* @param reader the source.
*/
public JSONTokener(Reader reader) {
this(reader, new JSONParserConfiguration());
this(reader, JSONParserConfiguration.getInstance());
}

/**
Expand Down Expand Up @@ -70,7 +70,7 @@ public JSONTokener(Reader reader, JSONParserConfiguration jsonParserConfiguratio
* @param inputStream The source.
*/
public JSONTokener(InputStream inputStream) {
this(inputStream, new JSONParserConfiguration());
this(inputStream, JSONParserConfiguration.getInstance());
}

/**
Expand Down
143 changes: 139 additions & 4 deletions src/test/java/org/json/junit/JSONParserConfigurationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,156 @@
import org.junit.Test;

import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.After;
import org.junit.Before;

import static org.junit.Assert.*;

public class JSONParserConfigurationTest {
private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\"}";

@Before
public void resetGlobalConfigurationBeforeTest() {
resetGlobalConfiguration();
}

@After
public void resetGlobalConfigurationAfterTest() {
resetGlobalConfiguration();
}

@Test
public void globalConfigurationShouldRejectNull() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> JSONParserConfiguration.setGlobalConfiguration(null));

assertEquals("Global JSONParserConfiguration cannot be null.", exception.getMessage());
}

@Test
public void globalConfigurationShouldOnlyBeSetOnce() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration());

IllegalStateException exception = assertThrows(IllegalStateException.class,

Check warning on line 52 in src/test/java/org/json/junit/JSONParserConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code of the lambda to have only one invocation possibly throwing a runtime exception.

See more on https://sonarcloud.io/project/issues?id=stleary_JSON-java&issues=AZ2-kCJK85-Qd9mO63Cg&open=AZ2-kCJK85-Qd9mO63Cg&pullRequest=1050
() -> JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withStrictMode(true)));

assertEquals("Global JSONParserConfiguration has already been set. It cannot be modified.",
exception.getMessage());
}

@Test
public void globalOverwriteDuplicateKeyShouldAffectDefaultJSONObjectStringConstructor() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withOverwriteDuplicateKey(true));

JSONObject jsonObject = new JSONObject(TEST_SOURCE);

assertEquals("value2", jsonObject.getString("key"));
}

@Test
public void globalStrictModeShouldAffectDefaultJSONTokenerReaderConstructor() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withStrictMode(true));

JSONException exception = assertThrows(JSONException.class,

Check warning on line 72 in src/test/java/org/json/junit/JSONParserConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code of the lambda to have only one invocation possibly throwing a runtime exception.

See more on https://sonarcloud.io/project/issues?id=stleary_JSON-java&issues=AZ2-kCJK85-Qd9mO63Ch&open=AZ2-kCJK85-Qd9mO63Ch&pullRequest=1050
() -> new JSONObject(new JSONTokener(new StringReader("{\"key\":\"value\"} invalid trailing text"))));

assertEquals("Strict mode error: Unparsed characters found at end of input text at 17 [character 18 line 1]",
exception.getMessage());
}

@Test
public void globalUseNativeNullsShouldAffectDefaultJSONObjectMapConstructor() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withUseNativeNulls(true));
Map<String, Object> nullMap = Collections.singletonMap("nullKey", null);

JSONObject jsonObject = new JSONObject(nullMap);

assertTrue(jsonObject.has("nullKey"));
assertEquals(JSONObject.NULL, jsonObject.get("nullKey"));
}

@Test
public void globalUseNativeNullsShouldAffectDefaultJSONArrayArrayWrapping() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withUseNativeNulls(true));
Map<String, Object> nullMap = Collections.singletonMap("nullKey", null);

JSONArray jsonArray = new JSONArray(new Object[] { nullMap });
JSONObject nestedObject = jsonArray.getJSONObject(0);

assertTrue(nestedObject.has("nullKey"));
assertEquals(JSONObject.NULL, nestedObject.get("nullKey"));
}

@Test
public void whenNoGlobalConfigurationSet_shouldUseDefaultConfiguration() {
// No global configuration set, should use defaults
// Default: overwriteDuplicateKey=false, so duplicate keys throw exception
JSONException exception = assertThrows(JSONException.class,
() -> new JSONObject(TEST_SOURCE));

assertTrue(exception.getMessage().contains("Duplicate key"));
}

@Test
public void globalStrictModeShouldAffectDefaultJSONArrayStringConstructor() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withStrictMode(true));

JSONException exception = assertThrows(JSONException.class,
() -> new JSONArray("[\"value\"] invalid trailing text"));

assertEquals("Strict mode error: Unparsed characters found at end of input text at 11 [character 12 line 1]",
exception.getMessage());
}

@Test
public void globalUseNativeNullsShouldAffectDefaultJSONArrayCollectionConstructor() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withUseNativeNulls(true));
Map<String, Object> nullMap = Collections.singletonMap("nullKey", null);
List<Map<String, Object>> collection = Collections.singletonList(nullMap);

JSONArray jsonArray = new JSONArray(collection);
JSONObject nestedObject = jsonArray.getJSONObject(0);

assertTrue(nestedObject.has("nullKey"));
assertEquals(JSONObject.NULL, nestedObject.get("nullKey"));
}

@Test
public void globalMaxNestingDepthShouldAffectDefaultJSONObjectMapConstructor() {
JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withMaxNestingDepth(1));
// Create a nested map that exceeds max depth 1 (depth 0 -> depth 1 -> depth 2 would exceed)
Map<String, Object> innerMostMap = Collections.singletonMap("innermost", "value");
Map<String, Object> innerMap = Collections.singletonMap("inner", innerMostMap);
Map<String, Object> outerMap = Collections.singletonMap("outer", innerMap);

JSONException exception = assertThrows(JSONException.class,
() -> new JSONObject(outerMap));

assertTrue(exception.getMessage().contains("depth"));
}


private void resetGlobalConfiguration() {
try {
Field field = JSONParserConfiguration.class.getDeclaredField("globalConfig");
field.setAccessible(true);
field.set(null, null);
} catch (ReflectiveOperationException e) {
throw new AssertionError("Unable to reset global JSONParserConfiguration for test isolation.", e);
}
}

@Test(expected = JSONException.class)
public void testThrowException() {
new JSONObject(TEST_SOURCE);
Expand Down
Loading