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

Handle circular references in Java beans #645

Merged
merged 7 commits into from Nov 21, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
);
}
}
56 changes: 56 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,61 @@ public void testPutNullObject() {
jsonObject.put(null, new Object());
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
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(expected=JSONException.class)
public void testRepeatObjectRecursive() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should not throw an exception. There is no recursion here.

Copy link
Contributor Author

@Zetmas Zetmas Nov 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your reply @johnjaylward ! Could you please explain a little more about why this test case is not recursive? I assumed this test case was recursive because it starts with C and ends with C in both children branches, creating loops of C-B-A-D-C... I also ran the test and it seems there would be an Exception thrown. Thanks for your patience!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you are correct! My eyes glazed over the first C for some reason.

As additional tests, can you make these:

Self recursion, should throw exception:

A -> A
B -> A -> A

Complex graph, duplicate object(s), but no recursion (no exception thrown):

E -> B -> A -> D -> C
       -> D -> C

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional test cases added

// 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 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;
}
}