Fix bug 16676: StackOverflow due to ToStringBuilder

(http://issues.apache.org/bugzilla/show_bug.cgi?id=16676)


git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/lang/trunk@137276 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Gary D. Gregory 2003-03-27 08:55:22 +00:00
parent ab7745f98e
commit edb0e8d284
3 changed files with 376 additions and 64 deletions

View File

@ -54,8 +54,9 @@
package org.apache.commons.lang.builder;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
/**
* <p><code>ToString</code> generation routine.</p>
@ -115,10 +116,21 @@
* @author Stephen Colebourne
* @author Gary Gregory
* @since 1.0
* @version $Id: ToStringBuilder.java,v 1.16 2003/03/23 17:54:16 scolebourne Exp $
* @version $Id: ToStringBuilder.java,v 1.17 2003/03/27 08:54:31 ggregory Exp $
*/
public class ToStringBuilder {
/**
* A registry of objects used by <code>reflectionToString</code> methods to detect cyclical object references
* and avoid infinite loops.
*/
private static ThreadLocal reflectionRegistry = new ThreadLocal() {
protected synchronized Object initialValue() {
// The HashSet implementation is not synchronized, which is just what we need here.
return new HashSet();
}
};
/**
* The default style of output to use
*/
@ -136,6 +148,40 @@ public class ToStringBuilder {
*/
private final Object object;
/**
* Returns the registry of objects being traversed by the
* <code>reflectionToString</code> methods in the current thread.
* @return Set the registry of objects being traversed
*/
static Set getReflectionRegistry() {
return (Set) reflectionRegistry.get();
}
/**
* Returns <code>true</code> if the registry contains the given object.
* Used by the reflection methods to avoid infinite loops.
* @return boolean <code>true</code> if the registry contains the given object.
*/
static boolean isRegistered(Object value) {
return getReflectionRegistry().contains(value);
}
/**
* Registers the given object.
* Used by the reflection methods to avoid infinite loops.
*/
static void register(Object value) {
getReflectionRegistry().add(value);
}
/**
* Unregisters the given object.
* Used by the reflection methods to avoid infinite loops.
*/
static void unregister(Object value) {
getReflectionRegistry().remove(value);
}
/**
* <p>Constructor for <code>ToStringBuilder</code>.</p>
*
@ -351,9 +397,6 @@ public static String reflectionToString(Object object, ToStringStyle style, bool
if (object == null) {
return style.getNullText();
}
if (style == null) {
style = getDefaultStyle();
}
ToStringBuilder builder = new ToStringBuilder(object, style);
Class clazz = object.getClass();
reflectionAppend(object, clazz, builder, outputTransients);
@ -366,7 +409,9 @@ public static String reflectionToString(Object object, ToStringStyle style, bool
/**
* Appends the fields and values defined by the given object of the
* given Class.
* given Class. If a cycle is detected as an objects is "toString()'ed",
* such an object is rendered as if <code>Object.toString()</code>
* had been called and not implemented by the object.
*
* @param object the object to append details of
* @param clazz the class of object parameter
@ -374,61 +419,54 @@ public static String reflectionToString(Object object, ToStringStyle style, bool
* @param useTransients whether to output transient fields
*/
private static void reflectionAppend(Object object, Class clazz, ToStringBuilder builder, boolean useTransients) {
if (clazz.isArray()) {
reflectionAppendArray(object, clazz, builder);
if (isRegistered(object)) {
// The object has already been appended, therefore we have an object cycle.
// Append a simple Object.toString style string. The field name is already appended at this point.
builder.appendAsObjectToString(object);
return;
}
Field[] fields = clazz.getDeclaredFields();
Field.setAccessible(fields, true);
for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
if ((f.getName().indexOf('$') == -1)
&& (useTransients || !Modifier.isTransient(f.getModifiers()))
&& (!Modifier.isStatic(f.getModifiers()))) {
try {
builder.append(f.getName(), f.get(object));
} catch (IllegalAccessException ex) {
//this can't happen. Would get a Security exception instead
//throw a runtime exception in case the impossible happens.
throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage());
try {
register(object);
if (clazz.isArray()) {
builder.reflectionAppendArray(object);
return;
}
Field[] fields = clazz.getDeclaredFields();
Field.setAccessible(fields, true);
for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
String fieldName = f.getName();
if ((fieldName.indexOf('$') == -1)
&& (useTransients || !Modifier.isTransient(f.getModifiers()))
&& (!Modifier.isStatic(f.getModifiers()))) {
try {
// Warning: Field.get(Object) creates wrappers objects for primitive types.
Object fieldValue = f.get(object);
if (isRegistered(fieldValue)
&& !f.getType().isPrimitive()) {
// A known field value has already been appended, therefore we have an object cycle,
// append a simple Object.toString style string.
builder.getStyle().appendFieldStart(builder.getStringBuffer(), fieldName);
builder.appendAsObjectToString(fieldValue);
// The recursion out of "builder.append(fieldName, fieldValue);" below will append the field
// end marker.
} else {
try {
register(object);
builder.append(fieldName, fieldValue);
} finally {
unregister(object);
}
}
} catch (IllegalAccessException ex) {
//this can't happen. Would get a Security exception instead
//throw a runtime exception in case the impossible happens.
throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage());
}
}
}
}
}
/**
* Appends the array elements in the given <code>Object</code> of the
* given <code>Class</code> to a <code>ToStringBuilder</code>.
*
* @param object the array object to append details of
* @param clazz the array class of the object parameter
* @param builder the builder to append to
*/
private static void reflectionAppendArray(Object object, Class clazz, ToStringBuilder builder) {
try {
// A multi-dimension array invokes the append(Object) method.
// A single-dimension array of primitive type pt invokes the append(pt[]) method.
builder.getClass().getDeclaredMethod("append", new Class[] { clazz.getComponentType().isArray() ? Object.class : clazz }).invoke(
builder,
new Object[] { object });
} catch (SecurityException e) {
// "This cannot happen"
throw new InternalError("Unexpected SecurityException: " + e.getMessage());
} catch (NoSuchMethodException e) {
// "This cannot happen"
throw new InternalError("Unexpected NoSuchMethodException: " + e.getMessage());
} catch (IllegalArgumentException e) {
// Method.invoke exception
// "This cannot happen"
throw new InternalError("Unexpected IllegalArgumentException: " + e.getMessage());
} catch (IllegalAccessException e) {
// Method.invoke exception
// "This cannot happen"
throw new InternalError("Unexpected IllegalAccessException: " + e.getMessage());
} catch (InvocationTargetException e) {
// Method.invoke exception
// "This cannot happen"
throw new InternalError("Unexpected InvocationTargetException: " + e.getMessage());
} finally {
unregister(object);
}
}
@ -485,6 +523,18 @@ public ToStringBuilder appendToString(String toString) {
return this;
}
/**
* <p>Appends with the same format as the default <code>Object toString()
* </code> method. Appends the class name followed by
* {@link System#identityHashCode(java.lang.Object)}.</p>
*
* @param object the <code>Object</code> whose class name and id to output
*/
public ToStringBuilder appendAsObjectToString(Object object) {
this.getStyle().appendAsObjectToString(this.getStringBuffer(), object);
return this;
}
//----------------------------------------------------------------------------
/**
@ -757,6 +807,18 @@ public ToStringBuilder append(Object[] array) {
return this;
}
/**
* <p>Append to the <code>toString</code> an <code>Object</code>
* array.</p>
*
* @param array the array to add to the <code>toString</code>
* @return this
*/
public ToStringBuilder reflectionAppendArray(Object array) {
style.reflectionAppendArrayDetail(buffer, null, array);
return this;
}
/**
* <p>Append to the <code>toString</code> an <code>Object</code>
* array.</p>

View File

@ -54,6 +54,7 @@
package org.apache.commons.lang.builder;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Map;
@ -81,7 +82,7 @@
*
* @author Stephen Colebourne
* @since 1.0
* @version $Id: ToStringStyle.java,v 1.10 2003/03/23 17:54:16 scolebourne Exp $
* @version $Id: ToStringStyle.java,v 1.11 2003/03/27 08:54:31 ggregory Exp $
*/
public abstract class ToStringStyle implements Serializable {
@ -319,6 +320,8 @@ public void append(StringBuffer buffer, String fieldName, Object value, Boolean
*
* <p>Either detail or summary views can be specified.</p>
*
* <p>If a cycle is detected, an object will be appended with the Object.toString() format.</p>
*
* @param buffer the <code>StringBuffer</code> to populate
* @param fieldName the field name, typically not used as already appended
* @param value the value to add to the <code>toString</code>,
@ -326,7 +329,12 @@ public void append(StringBuffer buffer, String fieldName, Object value, Boolean
* @param detail output detail or not
*/
protected void appendInternal(StringBuffer buffer, String fieldName, Object value, boolean detail) {
if (value instanceof Collection) {
if (ToStringBuilder.isRegistered(value)
&& !(value instanceof Number || value instanceof Boolean || value instanceof Character)) {
appendAsObjectToString(buffer, value);
}
else if (value instanceof Collection) {
if (detail) {
appendDetail(buffer, fieldName, (Collection) value);
} else {
@ -742,6 +750,32 @@ protected void appendDetail(StringBuffer buffer, String fieldName, Object[] arra
buffer.append(arrayEnd);
}
/**
* <p>Append to the <code>toString</code> the detail of an any array type.</p>
*
* @param buffer the <code>StringBuffer</code> to populate
* @param fieldName the field name, typically not used as already appended
* @param array the array to add to the <code>toString</code>,
* not <code>null</code>
*/
protected void reflectionAppendArrayDetail(StringBuffer buffer, String fieldName, Object array) {
buffer.append(arrayStart);
int length = Array.getLength(array);
for (int i = 0; i < length; i++) {
Object item = Array.get(array, i);
if (i > 0) {
buffer.append(arraySeparator);
}
if (item == null) {
appendNullText(buffer, fieldName);
} else {
appendInternal(buffer, fieldName, item, arrayContentDetail);
}
}
buffer.append(arrayEnd);
}
/**
* <p>Append to the <code>toString</code> a summary of an
* <code>Object</code> array.</p>
@ -1274,6 +1308,19 @@ protected void appendIdentityHashCode(StringBuffer buffer, Object object) {
}
}
/**
* <p>Appends with the same format as the default <code>Object toString()
* </code> method. Appends the class name followed by
* {@link System#identityHashCode(java.lang.Object)}.</p>
*
* @param buffer the <code>StringBuffer</code> to populate
* @param object the <code>Object</code> whose class name and id to output
*/
protected void appendAsObjectToString(StringBuffer buffer, Object object) {
this.appendClassName(buffer, object);
this.appendIdentityHashCode(buffer, object);
}
/**
* <p>Append the content start to the buffer.</p>
*

View File

@ -63,10 +63,12 @@
import junit.framework.TestSuite;
import junit.textui.TestRunner;
/**
* Unit tests {@link org.apache.commons.lang.ToStringBuilder}.
* Unit tests for {@link org.apache.commons.lang.ToStringBuilder}.
*
* @author <a href="mailto:scolebourne@joda.org">Stephen Colebourne</a>
* @version $Id: ToStringBuilderTest.java,v 1.6 2003/03/23 17:35:51 scolebourne Exp $
* @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
* @author <a href="mailto:alex@apache.org">Alex Chaffee</a>
* @version $Id: ToStringBuilderTest.java,v 1.7 2003/03/27 08:55:22 ggregory Exp $
*/
public class ToStringBuilderTest extends TestCase {
@ -164,10 +166,37 @@ public void testBlank() {
assertEquals(baseStr + "[]", new ToStringBuilder(base).toString());
}
public void testReflection() {
assertEquals(baseStr + "[value=5]", ToStringBuilder.reflectionToString(base));
}
/**
* Test wrapper for int primitive.
*/
public void testReflectionInteger() {
assertEquals(baseStr + "[value=5]", ToStringBuilder.reflectionToString(base));
}
/**
* Test wrapper for char primitive.
*/
public void testReflectionCharacter() {
Character c = new Character('A');
assertEquals(this.toBaseString(c) + "[value=A]", ToStringBuilder.reflectionToString(c));
}
/**
* Test wrapper for char boolean.
*/
public void testReflectionBoolean() {
Boolean b;
b = Boolean.TRUE;
assertEquals(this.toBaseString(b) + "[value=true]", ToStringBuilder.reflectionToString(b));
b = Boolean.FALSE;
assertEquals(this.toBaseString(b) + "[value=false]", ToStringBuilder.reflectionToString(b));
}
/**
* Create the same toString() as Object.toString().
* @param o the object to create the string for.
* @return a String in the Object.toString format.
*/
private String toBaseString(Object o) {
return o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o));
}
@ -200,6 +229,7 @@ public void testReflectionObjectArray() {
assertEquals(baseStr + "[{<null>,5,{3,6}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionLongArray() {
@ -208,6 +238,7 @@ public void testReflectionLongArray() {
assertEquals(baseStr + "[{1,2,-3,4}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionIntArray() {
@ -216,6 +247,7 @@ public void testReflectionIntArray() {
assertEquals(baseStr + "[{1,2,-3,4}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionShortArray() {
@ -224,6 +256,7 @@ public void testReflectionShortArray() {
assertEquals(baseStr + "[{1,2,-3,4}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionyteArray() {
@ -232,6 +265,7 @@ public void testReflectionyteArray() {
assertEquals(baseStr + "[{1,2,-3,4}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionCharArray() {
@ -240,6 +274,7 @@ public void testReflectionCharArray() {
assertEquals(baseStr + "[{A,2,_,D}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionDoubleArray() {
@ -248,6 +283,7 @@ public void testReflectionDoubleArray() {
assertEquals(baseStr + "[{1.0,2.9876,-3.00001,4.3}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionFloatArray() {
@ -256,6 +292,7 @@ public void testReflectionFloatArray() {
assertEquals(baseStr + "[{1.0,2.9876,-3.00001,4.3}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionBooleanArray() {
@ -264,6 +301,7 @@ public void testReflectionBooleanArray() {
assertEquals(baseStr + "[{true,false,false}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
// Reflection Array Array tests
@ -274,6 +312,7 @@ public void testReflectionFloatArrayArray() {
assertEquals(baseStr + "[{{1.0,2.29686},<null>,{NaN}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
@ -283,6 +322,7 @@ public void testReflectionLongArrayArray() {
assertEquals(baseStr + "[{{1,2},<null>,{5}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionIntArrayArray() {
@ -291,6 +331,7 @@ public void testReflectionIntArrayArray() {
assertEquals(baseStr + "[{{1,2},<null>,{5}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionhortArrayArray() {
@ -299,6 +340,7 @@ public void testReflectionhortArrayArray() {
assertEquals(baseStr + "[{{1,2},<null>,{5}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionByteArrayArray() {
@ -307,6 +349,7 @@ public void testReflectionByteArrayArray() {
assertEquals(baseStr + "[{{1,2},<null>,{5}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionCharArrayArray() {
@ -315,6 +358,7 @@ public void testReflectionCharArrayArray() {
assertEquals(baseStr + "[{{A,B},<null>,{p}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionDoubleArrayArray() {
@ -323,6 +367,7 @@ public void testReflectionDoubleArrayArray() {
assertEquals(baseStr + "[{{1.0,2.29686},<null>,{NaN}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
public void testReflectionBooleanArrayArray() {
@ -332,6 +377,7 @@ public void testReflectionBooleanArrayArray() {
assertEquals(baseStr + "[{{true,false},<null>,{false}}]", ToStringBuilder.reflectionToString(array));
array = null;
assertReflectionArray("<null>", array);
this.validateEmptyReflectionRegistry();
}
// Reflection hierarchy tests
@ -341,6 +387,7 @@ public void testReflectionHierarchyArrayList() {
String baseStr = this.toBaseString(base);
assertEquals(baseStr + "[elementData={<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>},size=0,modCount=0]", ToStringBuilder.reflectionToString(base, null, true));
assertEquals(baseStr + "[size=0]", ToStringBuilder.reflectionToString(base, null, false));
this.validateEmptyReflectionRegistry();
}
public void testReflectionHierarchy() {
@ -367,6 +414,7 @@ public void testReflectionHierarchy() {
assertEquals(baseStr + "[b=b,a=a]", ToStringBuilder.reflectionToString(baseB, null, false, List.class));
assertEquals(baseStr + "[b=b,a=a]", ToStringBuilder.reflectionToString(baseB, null, false, ReflectionTestFixtureA.class));
assertEquals(baseStr + "[b=b]", ToStringBuilder.reflectionToString(baseB, null, false, ReflectionTestFixtureB.class));
this.validateEmptyReflectionRegistry();
}
static class ReflectionTestFixtureA {
@ -396,6 +444,161 @@ public String toString() {
}
}
// Reflection cycle tests
/**
* Test an array element pointing to its container.
*/
public void testReflectionArrayCycle() throws Exception {
Object[] objects = new Object[1];
objects[0] = objects;
assertEquals(
this.toBaseString(objects) + "[{" + this.toBaseString(objects) + "}]",
ToStringBuilder.reflectionToString(objects));
this.validateEmptyReflectionRegistry();
}
/**
* Test an array element pointing to its container.
*/
public void testReflectionArrayCycleLevel2() throws Exception {
Object[] objects = new Object[1];
Object[] objectsLevel2 = new Object[1];
objects[0] = objectsLevel2;
objectsLevel2[0] = (Object) objects;
assertEquals(
this.toBaseString(objects) + "[{{" + this.toBaseString(objects) + "}}]",
ToStringBuilder.reflectionToString(objects));
assertEquals(
this.toBaseString(objectsLevel2) + "[{{" + this.toBaseString(objectsLevel2) + "}}]",
ToStringBuilder.reflectionToString(objectsLevel2));
this.validateEmptyReflectionRegistry();
}
public void testReflectionArrayArrayCycle() throws Exception {
Object[][] objects = new Object[2][2];
objects[0][0] = objects;
objects[0][1] = objects;
objects[1][0] = objects;
objects[1][1] = objects;
String basicToString = this.toBaseString(objects);
assertEquals(
basicToString
+ "[{{"
+ basicToString
+ ","
+ basicToString
+ "},{"
+ basicToString
+ ","
+ basicToString
+ "}}]",
ToStringBuilder.reflectionToString(objects));
this.validateEmptyReflectionRegistry();
}
/**
* A reflection test fixture.
*/
static class ReflectionTestCycleA {
ReflectionTestCycleB b;
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
/**
* A reflection test fixture.
*/
static class ReflectionTestCycleB {
ReflectionTestCycleA a;
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
/**
* A reflection test fixture.
*/
static class SimpleReflectionTestFixture {
Object o;
public SimpleReflectionTestFixture() {
}
public SimpleReflectionTestFixture(Object o) {
this.o = o;
}
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
/**
* Test an Object pointing to itself, the simplest test.
*
* @throws Exception
*/
public void testSimpleReflectionObjectCycle() throws Exception {
SimpleReflectionTestFixture simple = new SimpleReflectionTestFixture();
simple.o = simple;
assertTrue(ToStringBuilder.getReflectionRegistry().isEmpty());
assertEquals(this.toBaseString(simple) + "[o=" + this.toBaseString(simple) + "]", simple.toString());
this.validateEmptyReflectionRegistry();
}
/**
* Test Objects pointing to each other.
*
* @throws Exception
*/
public void testReflectionObjectCycle() throws Exception {
ReflectionTestCycleA a = new ReflectionTestCycleA();
ReflectionTestCycleB b = new ReflectionTestCycleB();
a.b = b;
b.a = a;
assertEquals(
this.toBaseString(a) + "[b=" + this.toBaseString(b) + "[a=" + this.toBaseString(a) + "]]",
a.toString());
this.validateEmptyReflectionRegistry();
}
/**
* Test a nasty combination of arrays and Objects pointing to each other.
* objects[0] -> SimpleReflectionTestFixture[ o -> objects ]
*
* @throws Exception
*/
public void testReflectionArrayAndObjectCycle() throws Exception {
Object[] objects = new Object[1];
SimpleReflectionTestFixture simple = new SimpleReflectionTestFixture(objects);
objects[0] = (Object) simple;
assertEquals(
this.toBaseString(objects)
+ "[{"
+ this.toBaseString(simple)
+ "[o="
+ this.toBaseString(objects)
+ "]"
+ "}]",
ToStringBuilder.reflectionToString(objects));
assertEquals(
this.toBaseString(simple)
+ "[o={"
+ this.toBaseString(simple)
+ "}]",
ToStringBuilder.reflectionToString(simple));
this.validateEmptyReflectionRegistry();
}
void validateEmptyReflectionRegistry() {
assertTrue(ToStringBuilder.getReflectionRegistry().isEmpty());
}
// End: Reflection cycle tests
public void testAppendSuper() {
assertEquals(baseStr + "[]", new ToStringBuilder(base).appendSuper("Integer@8888[]").toString());
assertEquals(baseStr + "[<null>]", new ToStringBuilder(base).appendSuper("Integer@8888[<null>]").toString());