Skip to content

Commit

Permalink
Merge pull request #645 from Zetmas/feature/issue#632
Browse files Browse the repository at this point in the history
Detect and handle circular references when parsing Java beans
  • Loading branch information
stleary committed Nov 21, 2021
2 parents d6227c8 + fca7e17 commit bc623e3
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 3 deletions.
51 changes: 48 additions & 3 deletions src/main/java/org/json/JSONObject.java
Expand Up @@ -39,6 +39,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -365,6 +366,11 @@ public JSONObject(Object bean) {
this.populateMap(bean);
}

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

/**
* Construct a JSONObject from an Object, using reflection to find the
* public members. The resulting JSONObject's keys will be the strings from
Expand Down Expand Up @@ -1520,6 +1526,10 @@ public String optString(String key, String defaultValue) {
* the bean
*/
private void populateMap(Object bean) {
populateMap(bean, new HashSet<Object>());
}

private void populateMap(Object bean, Set<Object> objectsRecord) {
Class<?> klass = bean.getClass();

// If klass is a System class then set includeSuperClass to false.
Expand All @@ -1540,10 +1550,22 @@ && isValidMethodName(method.getName())) {
try {
final Object result = method.invoke(bean);
if (result != null) {
this.map.put(key, wrap(result));
// check cyclic dependency and throw error if needed
// the wrap and populateMap combination method is
// itself DFS recursive
if (objectsRecord.contains(result)) {
throw recursivelyDefinedObjectException(key);
}

objectsRecord.add(result);

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

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
// after calling toString
if (result instanceof Closeable) {
try {
((Closeable) result).close();
Expand Down Expand Up @@ -2431,6 +2453,10 @@ public static String valueToString(Object value) throws JSONException {
* @return The wrapped value
*/
public static Object wrap(Object object) {
return wrap(object, null);
}

private static Object wrap(Object object, Set<Object> objectsRecord) {
try {
if (NULL.equals(object)) {
return NULL;
Expand Down Expand Up @@ -2465,7 +2491,15 @@ public static Object wrap(Object object) {
|| object.getClass().getClassLoader() == null) {
return object.toString();
}
return new JSONObject(object);
if (objectsRecord != null) {
return new JSONObject(object, objectsRecord);
}
else {
return new JSONObject(object);
}
}
catch (JSONException exception) {
throw exception;
} catch (Exception exception) {
return null;
}
Expand Down Expand Up @@ -2676,4 +2710,15 @@ private static JSONException wrongValueFormatException(
"JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value + ")."
, cause);
}

/**
* Create a new JSONException in a common format for recursive object definition.
* @param key name of the key
* @return JSONException that can be thrown.
*/
private static JSONException recursivelyDefinedObjectException(String key) {
return new JSONException(
"JavaBean object contains recursively defined member variable of key " + quote(key)
);
}
}
94 changes: 94 additions & 0 deletions src/test/java/org/json/junit/JSONObjectTest.java
Expand Up @@ -73,6 +73,7 @@ of this software and associated documentation files (the "Software"), to deal
import org.json.junit.data.MyNumber;
import org.json.junit.data.MyNumberContainer;
import org.json.junit.data.MyPublicClass;
import org.json.junit.data.RecursiveBean;
import org.json.junit.data.Singleton;
import org.json.junit.data.SingletonEnum;
import org.json.junit.data.WeirdList;
Expand Down Expand Up @@ -3218,6 +3219,99 @@ public void testPutNullObject() {
jsonObject.put(null, new Object());
fail("Expected an exception");
}
@Test(expected=JSONException.class)
public void testSelfRecursiveObject() {
// A -> A ...
RecursiveBean ObjA = new RecursiveBean("ObjA");
ObjA.setRef(ObjA);
new JSONObject(ObjA);
fail("Expected an exception");
}
@Test(expected=JSONException.class)
public void testLongSelfRecursiveObject() {
// B -> A -> A ...
RecursiveBean ObjA = new RecursiveBean("ObjA");
RecursiveBean ObjB = new RecursiveBean("ObjB");
ObjB.setRef(ObjA);
ObjA.setRef(ObjA);
new JSONObject(ObjB);
fail("Expected an exception");
}
@Test(expected=JSONException.class)
public void testSimpleRecursiveObject() {
// B -> A -> B ...
RecursiveBean ObjA = new RecursiveBean("ObjA");
RecursiveBean ObjB = new RecursiveBean("ObjB");
ObjB.setRef(ObjA);
ObjA.setRef(ObjB);
new JSONObject(ObjA);
fail("Expected an exception");
}
@Test(expected=JSONException.class)
public void testLongRecursiveObject() {
// D -> C -> B -> A -> D ...
RecursiveBean ObjA = new RecursiveBean("ObjA");
RecursiveBean ObjB = new RecursiveBean("ObjB");
RecursiveBean ObjC = new RecursiveBean("ObjC");
RecursiveBean ObjD = new RecursiveBean("ObjD");
ObjC.setRef(ObjB);
ObjB.setRef(ObjA);
ObjD.setRef(ObjC);
ObjA.setRef(ObjD);
new JSONObject(ObjB);
fail("Expected an exception");
}
@Test(expected=JSONException.class)
public void testRepeatObjectRecursive() {
// C -> B -> A -> D -> C ...
// -> D -> C ...
RecursiveBean ObjA = new RecursiveBean("ObjA");
RecursiveBean ObjB = new RecursiveBean("ObjB");
RecursiveBean ObjC = new RecursiveBean("ObjC");
RecursiveBean ObjD = new RecursiveBean("ObjD");
ObjC.setRef(ObjB);
ObjB.setRef(ObjA);
ObjB.setRef2(ObjD);
ObjA.setRef(ObjD);
ObjD.setRef(ObjC);
new JSONObject(ObjC);
fail("Expected an exception");
}
@Test
public void testRepeatObjectNotRecursive() {
// C -> B -> A
// -> A
RecursiveBean ObjA = new RecursiveBean("ObjA");
RecursiveBean ObjB = new RecursiveBean("ObjB");
RecursiveBean ObjC = new RecursiveBean("ObjC");
ObjC.setRef(ObjA);
ObjB.setRef(ObjA);
ObjB.setRef2(ObjA);
new JSONObject(ObjC);
new JSONObject(ObjB);
new JSONObject(ObjA);
}
@Test
public void testLongRepeatObjectNotRecursive() {
// C -> B -> A -> D -> E
// -> D -> E
RecursiveBean ObjA = new RecursiveBean("ObjA");
RecursiveBean ObjB = new RecursiveBean("ObjB");
RecursiveBean ObjC = new RecursiveBean("ObjC");
RecursiveBean ObjD = new RecursiveBean("ObjD");
RecursiveBean ObjE = new RecursiveBean("ObjE");
ObjC.setRef(ObjB);
ObjB.setRef(ObjA);
ObjB.setRef2(ObjD);
ObjA.setRef(ObjD);
ObjD.setRef(ObjE);
new JSONObject(ObjC);
new JSONObject(ObjB);
new JSONObject(ObjA);
new JSONObject(ObjD);
new JSONObject(ObjE);
}


@Test
public void testIssue548ObjectWithEmptyJsonArray() {
Expand Down
23 changes: 23 additions & 0 deletions src/test/java/org/json/junit/data/RecursiveBean.java
@@ -0,0 +1,23 @@
package org.json.junit.data;

/**
* test class for verifying if recursively defined bean can be correctly identified
* @author Zetmas
*
*/
public class RecursiveBean {
private String name;
private Object reference;
private Object reference2;
public String getName() { return name; }
public Object getRef() {return reference;}
public Object getRef2() {return reference2;}
public void setRef(Object refObj) {reference = refObj;}
public void setRef2(Object refObj) {reference2 = refObj;}

public RecursiveBean(String name) {
this.name = name;
reference = null;
reference2 = null;
}
}

0 comments on commit bc623e3

Please sign in to comment.