diff --git a/gson/src/main/java/com/google/gson/CircularReferenceException.java b/gson/src/main/java/com/google/gson/CircularReferenceException.java new file mode 100644 index 00000000..52840a2b --- /dev/null +++ b/gson/src/main/java/com/google/gson/CircularReferenceException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson; + +import java.lang.reflect.Field; + +/** + * Exception class to indicate a circular reference error. + * This class is not part of the public API and hence is not public. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +final class CircularReferenceException extends RuntimeException { + private static final long serialVersionUID = 7444343294106513081L; + private final Object offendingNode; + + CircularReferenceException(Object offendingNode) { + super("circular reference error"); + this.offendingNode = offendingNode; + } + + public IllegalStateException createDetailedException(Field offendingField) { + StringBuilder msg = new StringBuilder(getMessage()); + if (offendingField != null) { + msg.append("\n ").append("Offending field: ").append(offendingField.getName() + "\n"); + } + if (offendingNode != null) { + msg.append("\n ").append("Offending object: ").append(offendingNode); + } + return new IllegalStateException(msg.toString(), this); + } +} diff --git a/gson/src/main/java/com/google/gson/JsonPrintFormatter.java b/gson/src/main/java/com/google/gson/JsonPrintFormatter.java index a5aaaa7d..39961582 100644 --- a/gson/src/main/java/com/google/gson/JsonPrintFormatter.java +++ b/gson/src/main/java/com/google/gson/JsonPrintFormatter.java @@ -17,8 +17,6 @@ package com.google.gson; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import java.util.Stack; /** diff --git a/gson/src/main/java/com/google/gson/JsonSerializationVisitor.java b/gson/src/main/java/com/google/gson/JsonSerializationVisitor.java index 2d3183c0..16d22eba 100644 --- a/gson/src/main/java/com/google/gson/JsonSerializationVisitor.java +++ b/gson/src/main/java/com/google/gson/JsonSerializationVisitor.java @@ -54,7 +54,7 @@ final class JsonSerializationVisitor implements ObjectNavigator.Visitor { return; } if (ancestors.contains(node)) { - throw new IllegalStateException("Circular reference found: " + node); + throw new CircularReferenceException(node); } ancestors.push(node); } @@ -85,27 +85,35 @@ final class JsonSerializationVisitor implements ObjectNavigator.Visitor { } public void visitArrayField(Field f, Type typeOfF, Object obj) { - if (isFieldNull(f, obj)) { - if (serializeNulls) { - addChildAsElement(f, JsonNull.createJsonNull()); + try { + if (isFieldNull(f, obj)) { + if (serializeNulls) { + addChildAsElement(f, JsonNull.createJsonNull()); + } + } else { + Object array = getFieldValue(f, obj); + addAsChildOfObject(f, typeOfF, array); } - } else { - Object array = getFieldValue(f, obj); - addAsChildOfObject(f, typeOfF, array); + } catch (CircularReferenceException e) { + throw e.createDetailedException(f); } } public void visitObjectField(Field f, Type typeOfF, Object obj) { - if (isFieldNull(f, obj)) { - if (serializeNulls) { - addChildAsElement(f, JsonNull.createJsonNull()); + try { + if (isFieldNull(f, obj)) { + if (serializeNulls) { + addChildAsElement(f, JsonNull.createJsonNull()); + } + } else { + Object fieldValue = getFieldValue(f, obj); + if (fieldValue != null) { + typeOfF = getActualTypeIfMoreSpecific(typeOfF, fieldValue.getClass()); + } + addAsChildOfObject(f, typeOfF, fieldValue); } - } else { - Object fieldValue = getFieldValue(f, obj); - if (fieldValue != null) { - typeOfF = getActualTypeIfMoreSpecific(typeOfF, fieldValue.getClass()); - } - addAsChildOfObject(f, typeOfF, fieldValue); + } catch (CircularReferenceException e) { + throw e.createDetailedException(f); } } @@ -160,20 +168,34 @@ final class JsonSerializationVisitor implements ObjectNavigator.Visitor { @SuppressWarnings("unchecked") public boolean visitUsingCustomHandler(Object obj, Type objType) { - JsonSerializer serializer = serializers.getHandlerFor(objType); - if (serializer == null && obj != null) { - serializer = serializers.getHandlerFor(obj.getClass()); - } - - if (serializer != null) { - if (obj == null) { - assignToRoot(JsonNull.createJsonNull()); - } else { - assignToRoot(serializer.serialize(obj, objType, context)); + try { + JsonSerializer serializer = serializers.getHandlerFor(objType); + if (serializer == null && obj != null) { + serializer = serializers.getHandlerFor(obj.getClass()); } - return true; + + if (serializer != null) { + if (obj == null) { + assignToRoot(JsonNull.createJsonNull()); + } else { + assignToRoot(invokeCustomHandler(obj, objType, serializer)); + } + return true; + } + return false; + } catch (CircularReferenceException e) { + throw e.createDetailedException(null); + } + } + + @SuppressWarnings("unchecked") + private JsonElement invokeCustomHandler(Object obj, Type objType, JsonSerializer serializer) { + start(obj); + try { + return serializer.serialize(obj, objType, context); + } finally { + end(obj); } - return false; } @SuppressWarnings("unchecked") @@ -189,13 +211,15 @@ final class JsonSerializationVisitor implements ObjectNavigator.Visitor { } JsonSerializer serializer = serializers.getHandlerFor(actualTypeOfField); if (serializer != null) { - JsonElement child = serializer.serialize(obj, actualTypeOfField, context); + JsonElement child = invokeCustomHandler(obj, actualTypeOfField, serializer); addChildAsElement(f, child); return true; } return false; } catch (IllegalAccessException e) { throw new RuntimeException(); + } catch (CircularReferenceException e) { + throw e.createDetailedException(f); } } diff --git a/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java b/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java new file mode 100644 index 00000000..f4a04e42 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.functional; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.common.TestTypes.ClassOverridingEquals; + +/** + * Functional tests related to circular reference detection and error reporting. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class CircularReferenceTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testCircularSerialization() throws Exception { + ContainsReferenceToSelfType a = new ContainsReferenceToSelfType(); + ContainsReferenceToSelfType b = new ContainsReferenceToSelfType(); + a.children.add(b); + b.children.add(a); + try { + gson.toJson(a); + fail("Circular types should not get printed!"); + } catch (IllegalStateException expected) { + assertTrue(expected.getMessage().contains("children")); + } + } + + public void testSelfReferenceSerialization() throws Exception { + ClassOverridingEquals objA = new ClassOverridingEquals(); + objA.ref = objA; + + try { + gson.toJson(objA); + fail("Circular reference to self can not be serialized!"); + } catch (IllegalStateException expected) { } + } + + public void testSelfReferenceArrayFieldSerialization() throws Exception { + ClassWithSelfReferenceArray objA = new ClassWithSelfReferenceArray(); + objA.children = new ClassWithSelfReferenceArray[]{objA}; + + try { + gson.toJson(objA); + fail("Circular reference to self can not be serialized!"); + } catch (IllegalStateException expected) { + assertTrue(expected.getMessage().contains("children")); + } + } + + public void testSelfReferenceCustomHandlerSerialization() throws Exception { + ClassWithSelfReference obj = new ClassWithSelfReference(); + obj.child = obj; + Gson gson = new GsonBuilder().registerTypeAdapter(ClassWithSelfReference.class, new JsonSerializer() { + public JsonElement serialize(ClassWithSelfReference src, Type typeOfSrc, + JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("property", "value"); + obj.add("child", context.serialize(src.child)); + return obj; + } + }).create(); + try { + gson.toJson(obj); + fail("Circular reference to self can not be serialized!"); + } catch (IllegalStateException expected) { + assertTrue(expected.getMessage().contains("Offending")); + } + } + + public void testDirectedAcyclicGraphSerialization() throws Exception { + ContainsReferenceToSelfType a = new ContainsReferenceToSelfType(); + ContainsReferenceToSelfType b = new ContainsReferenceToSelfType(); + ContainsReferenceToSelfType c = new ContainsReferenceToSelfType(); + a.children.add(b); + a.children.add(c); + b.children.add(c); + assertNotNull(gson.toJson(a)); + } + + public void testDirectedAcyclicGraphDeserialization() throws Exception { + String json = "{\"children\":[{\"children\":[{\"children\":[]}]},{\"children\":[]}]}"; + ContainsReferenceToSelfType target = gson.fromJson(json, ContainsReferenceToSelfType.class); + assertNotNull(target); + assertEquals(2, target.children.size()); + } + + private static class ContainsReferenceToSelfType { + Collection children = new ArrayList(); + } + + private static class ClassWithSelfReference { + ClassWithSelfReference child; + } + + private static class ClassWithSelfReferenceArray { + @SuppressWarnings("unused") + ClassWithSelfReferenceArray[] children; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index c97ed94d..86fe2c64 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -28,7 +28,6 @@ import com.google.gson.InstanceCreator; import com.google.gson.common.TestTypes.ArrayOfObjects; import com.google.gson.common.TestTypes.BagOfPrimitiveWrappers; import com.google.gson.common.TestTypes.BagOfPrimitives; -import com.google.gson.common.TestTypes.ClassOverridingEquals; import com.google.gson.common.TestTypes.ClassWithArray; import com.google.gson.common.TestTypes.ClassWithNoFields; import com.google.gson.common.TestTypes.ClassWithObjects; @@ -91,23 +90,6 @@ public class ObjectTest extends TestCase { assertEquals(jsonString, target.getExpectedJson()); } - public void testDirectedAcyclicGraphSerialization() throws Exception { - ContainsReferenceToSelfType a = new ContainsReferenceToSelfType(); - ContainsReferenceToSelfType b = new ContainsReferenceToSelfType(); - ContainsReferenceToSelfType c = new ContainsReferenceToSelfType(); - a.children.add(b); - a.children.add(c); - b.children.add(c); - assertNotNull(gson.toJson(a)); - } - - public void testDirectedAcyclicGraphDeserialization() throws Exception { - String json = "{\"children\":[{\"children\":[{\"children\":[]}]},{\"children\":[]}]}"; - ContainsReferenceToSelfType target = gson.fromJson(json, ContainsReferenceToSelfType.class); - assertNotNull(target); - assertEquals(2, target.children.size()); - } - public void testClassWithTransientFieldsSerialization() throws Exception { ClassWithTransientFields target = new ClassWithTransientFields(1L); assertEquals(target.getExpectedJson(), gson.toJson(target)); @@ -231,11 +213,15 @@ public class ObjectTest extends TestCase { public void testEmptyCollectionInAnObjectDeserialization() throws Exception { String json = "{\"children\":[]}"; - ContainsReferenceToSelfType target = gson.fromJson(json, ContainsReferenceToSelfType.class); + ClassWithCollectionField target = gson.fromJson(json, ClassWithCollectionField.class); assertNotNull(target); assertTrue(target.children.isEmpty()); } + private static class ClassWithCollectionField { + Collection children = new ArrayList(); + } + public void testPrimitiveArrayInAnObjectDeserialization() throws Exception { String json = "{\"longArray\":[0,1,2,3,4,5,6,7,8,9]}"; PrimitiveArray target = gson.fromJson(json, PrimitiveArray.class); @@ -252,31 +238,10 @@ public class ObjectTest extends TestCase { } public void testEmptyCollectionInAnObjectSerialization() throws Exception { - ContainsReferenceToSelfType target = new ContainsReferenceToSelfType(); + ClassWithCollectionField target = new ClassWithCollectionField(); assertEquals("{\"children\":[]}", gson.toJson(target)); } - public void testCircularSerialization() throws Exception { - ContainsReferenceToSelfType a = new ContainsReferenceToSelfType(); - ContainsReferenceToSelfType b = new ContainsReferenceToSelfType(); - a.children.add(b); - b.children.add(a); - try { - gson.toJson(a); - fail("Circular types should not get printed!"); - } catch (IllegalStateException expected) { } - } - - public void testSelfReferenceSerialization() throws Exception { - ClassOverridingEquals objA = new ClassOverridingEquals(); - objA.ref = objA; - - try { - gson.toJson(objA); - fail("Circular reference to self can not be serialized!"); - } catch (IllegalStateException expected) { } - } - public void testPrivateNoArgConstructorDeserialization() throws Exception { ClassWithPrivateNoArgsConstructor target = gson.fromJson("{\"a\":20}", ClassWithPrivateNoArgsConstructor.class); @@ -339,11 +304,6 @@ public class ObjectTest extends TestCase { } } - private static class ContainsReferenceToSelfType { - public Collection children = - new ArrayList(); - } - private static class ArrayOfArrays { private final BagOfPrimitives[][] elements; public ArrayOfArrays() {