Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSONObject parsing for self-referencing maps #809

Closed
Closed
110 changes: 83 additions & 27 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public String toString() {
* The map where the JSONObject's properties are kept.
*/
private final Map<String, Object> map;
private final JSONParserConfiguration configuration;

public Class<? extends Map> getMapType() {
return map.getClass();
Expand All @@ -168,6 +169,7 @@ public JSONObject() {
// retrieval based on associative access.
// Therefore, an implementation mustn't rely on the order of the item.
this.map = new HashMap<String, Object>();
configuration = new JSONParserConfiguration();
}

/**
Expand Down Expand Up @@ -273,18 +275,51 @@ public JSONObject(JSONTokener x) throws JSONException {
* If a key in the map is <code>null</code>
*/
public JSONObject(Map<?, ?> m) {
this(m, getCircularDependencySet(), new JSONParserConfiguration());
}

/**
* The same functionality as {@link JSONObject#JSONObject(Map)} but with the possibility
* of passing a {@link JSONParserConfiguration} to overwrite the default.
*
* @param m
* A map object that can be used to initialize the contents of
* the JSONObject.
* @param jsonParserConfiguration the configuration to overwrite the default functionality
*/
public JSONObject(Map<?, ?> m, JSONParserConfiguration jsonParserConfiguration) {
this(m, Collections.newSetFromMap(new IdentityHashMap<>()), jsonParserConfiguration);
}

private JSONObject(Map<?, ?> m, Set<Object> objectsRecord, JSONParserConfiguration jsonParserConfiguration) {
configuration = jsonParserConfiguration;

if (m == null) {
this.map = new HashMap<String, Object>();
} else {
this.map = new HashMap<String, Object>(m.size());
for (final Entry<?, ?> e : m.entrySet()) {
if(e.getKey() == null) {
throw new NullPointerException("Null key.");
}
final Object value = e.getValue();
if (value != null) {
testValidity(value);
this.map.put(String.valueOf(e.getKey()), wrap(value));
this.map = new HashMap<>();
return;
}

map = new HashMap<>(m.size());

final boolean circularReferenceValidated = configuration.isCircularReferenceValidated();

for (final Entry<?, ?> e : m.entrySet()) {
if(e.getKey() == null) {
throw new NullPointerException("Null key.");
}
final Object value = e.getValue();
if (value != null) {
testValidity(value);
if (circularReferenceValidated) {
if (objectsRecord.contains(value)) {
throw new JSONException("Found circular reference.");
}

objectsRecord.add(value);
this.map.put(String.valueOf(e.getKey()), wrap(value, objectsRecord, configuration));
objectsRecord.remove(value);
} else {
this.map.put(String.valueOf(e.getKey()), wrap(value, objectsRecord, configuration));
}
}
}
Expand Down Expand Up @@ -351,13 +386,17 @@ public JSONObject(Map<?, ?> m) {
* If a getter returned a non-finite number.
*/
public JSONObject(Object bean) {
this(bean, new JSONParserConfiguration());
}

public JSONObject(Object bean, JSONParserConfiguration jsonParserConfiguration) {
this();
this.populateMap(bean);
this.populateMap(bean, getCircularDependencySet(), jsonParserConfiguration);
}

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

/**
Expand Down Expand Up @@ -454,6 +493,7 @@ public JSONObject(String baseName, Locale locale) throws JSONException {
*/
protected JSONObject(int initialCapacity){
this.map = new HashMap<String, Object>(initialCapacity);
configuration = new JSONParserConfiguration();
}

/**
Expand Down Expand Up @@ -1694,14 +1734,14 @@ public String optString(String key, String defaultValue) {
*
* @param bean
* the bean
* @param objectsRecord
* the set to track for circular dependencies
* @param jsonParserConfiguration
* the configuration for the JSON parser
* @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, Set<Object> objectsRecord) {
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.
Expand Down Expand Up @@ -1732,7 +1772,7 @@ && isValidMethodName(method.getName())) {
objectsRecord.add(result);

testValidity(result);
this.map.put(key, wrap(result, objectsRecord));
this.map.put(key, wrap(result, objectsRecord, jsonParserConfiguration));

objectsRecord.remove(result);

Expand Down Expand Up @@ -2657,10 +2697,24 @@ public static String valueToString(Object value) throws JSONException {
* @return The wrapped value
*/
public static Object wrap(Object object) {
return wrap(object, null);
return wrap(object, new JSONParserConfiguration());
}

private static Object wrap(Object object, Set<Object> objectsRecord) {
/**
* The same functionality as the {@link JSONObject#wrap(Object)} but with the possibility to pass a
* {@link JSONParserConfiguration} to overwrite the default functionality.
*
* @param object
* the object to wrap
* @param jsonParserConfiguration
* the configuration for JSON parser
* @return the wrapped object
*/
public static Object wrap(Object object, JSONParserConfiguration jsonParserConfiguration) {
return wrap(object, getCircularDependencySet(), jsonParserConfiguration);
}

private static Object wrap(Object object, Set<Object> objectsRecord, JSONParserConfiguration jsonParserConfiguration) {
try {
if (NULL.equals(object)) {
return NULL;
Expand All @@ -2685,7 +2739,7 @@ private static Object wrap(Object object, Set<Object> objectsRecord) {
}
if (object instanceof Map) {
Map<?, ?> map = (Map<?, ?>) object;
return new JSONObject(map);
return new JSONObject(map, objectsRecord, jsonParserConfiguration);
}
Package objectPackage = object.getClass().getPackage();
String objectPackageName = objectPackage != null ? objectPackage
Expand All @@ -2695,10 +2749,8 @@ private static Object wrap(Object object, Set<Object> objectsRecord) {
|| object.getClass().getClassLoader() == null) {
return object.toString();
}
if (objectsRecord != null) {
return new JSONObject(object, objectsRecord);
}
return new JSONObject(object);

return new JSONObject(object, objectsRecord, jsonParserConfiguration);
}
catch (JSONException exception) {
throw exception;
Expand All @@ -2707,6 +2759,10 @@ private static Object wrap(Object object, Set<Object> objectsRecord) {
}
}

private static Set<Object> getCircularDependencySet() {
return Collections.newSetFromMap(new IdentityHashMap<>());
}

/**
* Write the contents of the JSONObject as JSON text to a writer. For
* compactness, no whitespace is added.
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/org/json/JSONParserConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.json;

/**
* Configuration class for JSON parsers.
*/
public class JSONParserConfiguration {

private boolean circularReferenceValidated;

/**
* Configuration with the default values.
*/
public JSONParserConfiguration() {
circularReferenceValidated = true;
}

private JSONParserConfiguration(boolean circularReferenceValidated) {
this.circularReferenceValidated = circularReferenceValidated;
}

/**
* Retrieves the configuration about the circular dependency check.
*
* @return if true enables the circular reference check, false otherwise
*/
public boolean isCircularReferenceValidated() {
return circularReferenceValidated;
}

/**
* Sets the flag that controls the underline functionality to check or not about circular reference.
*
* @param circularReferenceValidation if true enables the circular reference check, false otherwise.
* Default is true
* @return a new instance of the configuration with the given value being set
*/
public JSONParserConfiguration withCircularReferenceValidation(boolean circularReferenceValidation) {
JSONParserConfiguration configuration = (JSONParserConfiguration) clone();
configuration.circularReferenceValidated = circularReferenceValidation;
return configuration;
}

@Override
protected Object clone() {
return new JSONParserConfiguration(circularReferenceValidated);
}
}
43 changes: 43 additions & 0 deletions src/test/java/org/json/junit/JSONObjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.json.JSONString;
import org.json.JSONTokener;
import org.json.XML;
import org.json.JSONParserConfiguration;
import org.json.junit.data.BrokenToString;
import org.json.junit.data.ExceptionalBean;
import org.json.junit.data.Fraction;
Expand Down Expand Up @@ -3713,4 +3714,46 @@ public void issue713BeanConstructorWithNonFiniteNumbers() {
assertThrows(JSONException.class, () -> new JSONObject(bean));
}
}

@Test(expected = JSONException.class)
public void testCircleReferenceFirstLevel() {
Map<Object, Object> jsonObject = new HashMap<>();

jsonObject.put("test", jsonObject);

new JSONObject(jsonObject, new JSONParserConfiguration());
}

@Test(expected = StackOverflowError.class)
public void testCircleReferenceMultiplyLevel_notConfigured_expectedStackOverflow() {
Map<Object, Object> inside = new HashMap<>();

Map<Object, Object> jsonObject = new HashMap<>();
inside.put("test", jsonObject);
jsonObject.put("test", inside);

new JSONObject(jsonObject, new JSONParserConfiguration().withCircularReferenceValidation(false));
}

@Test(expected = JSONException.class)
public void testCircleReferenceMultiplyLevel_configured_expectedJSONException() {
Map<Object, Object> inside = new HashMap<>();

Map<Object, Object> jsonObject = new HashMap<>();
inside.put("test", jsonObject);
jsonObject.put("test", inside);

new JSONObject(jsonObject, new JSONParserConfiguration());
}

@Test
public void testDifferentKeySameInstanceNotACircleReference() {
Map<Object, Object> map1 = new HashMap<>();
Map<Object, Object> map2 = new HashMap<>();

map1.put("test1", map2);
map1.put("test2", map2);

new JSONObject(map1);
}
}