ggregory 2003/03/27 00:54:32 Modified: lang/src/java/org/apache/commons/lang/builder ToStringStyle.java ToStringBuilder.java Log: Fix bug 16676: StackOverflow due to ToStringBuilder (http://issues.apache.org/bugzilla/show_bug.cgi?id=16676) Revision Changes Path 1.11 +49 -2 jakarta-commons/lang/src/java/org/apache/commons/lang/builder/ToStringStyle.java Index: ToStringStyle.java =================================================================== RCS file: /home/cvs/jakarta-commons/lang/src/java/org/apache/commons/lang/builder/ToStringStyle.java,v retrieving revision 1.10 retrieving revision 1.11 diff -u -r1.10 -r1.11 --- ToStringStyle.java 23 Mar 2003 17:54:16 -0000 1.10 +++ ToStringStyle.java 27 Mar 2003 08:54:31 -0000 1.11 @@ -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; @@ -319,6 +320,8 @@ * * <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 @@ * @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 { @@ -743,6 +751,32 @@ } /** + * <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> * @@ -1272,6 +1306,19 @@ buffer.append('@'); buffer.append(Integer.toHexString(System.identityHashCode(object))); } + } + + /** + * <p>Appends with the same format as the default <code>Object toString() + * </code> method. Appends the class name followed by + * [EMAIL PROTECTED] 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); } /** 1.17 +119 -57 jakarta-commons/lang/src/java/org/apache/commons/lang/builder/ToStringBuilder.java Index: ToStringBuilder.java =================================================================== RCS file: /home/cvs/jakarta-commons/lang/src/java/org/apache/commons/lang/builder/ToStringBuilder.java,v retrieving revision 1.16 retrieving revision 1.17 diff -u -r1.16 -r1.17 --- ToStringBuilder.java 23 Mar 2003 17:54:16 -0000 1.16 +++ ToStringBuilder.java 27 Mar 2003 08:54:31 -0000 1.17 @@ -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> @@ -120,6 +121,17 @@ 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 */ private static ToStringStyle defaultStyle = ToStringStyle.DEFAULT_STYLE; @@ -137,6 +149,40 @@ 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> * * <p>This constructor outputs using the default style set with @@ -351,9 +397,6 @@ 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 @@ /** * 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 @@ * @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 @@ return this; } + /** + * <p>Appends with the same format as the default <code>Object toString() + * </code> method. Appends the class name followed by + * [EMAIL PROTECTED] 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; + } + //---------------------------------------------------------------------------- /** @@ -754,6 +804,18 @@ */ public ToStringBuilder append(Object[] array) { style.append(buffer, null, array, null); + 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; }
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]