package org.test4j.hamcrest.matcher.property.report; import static ext.test4j.apache.commons.lang.ClassUtils.getShortClassName; import static java.lang.reflect.Modifier.isStatic; import static java.lang.reflect.Modifier.isTransient; import static org.test4j.hamcrest.matcher.property.reflection.HibernateUtil.getUnproxiedValue; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.Map; /** * A class for generating a string representation of any object, array or * primitive value. * <p/> * Non-primitive objects are processed recursively so that a string * representation of inner objects is also generated. Too avoid too much output, * this recursion is limited with a given maximum depth. * */ @SuppressWarnings({ "rawtypes" }) public class ObjectFormatter { /** * The maximum recursion depth */ protected int maxDepth; /** * Creates a formatter with a maximum recursion depth of 5. */ public ObjectFormatter() { this(5); } /** * Creates a formatter with the given maximum recursion depth. * <p/> * NOTE: there is no cycle detection. A large max depth value can cause lots * of output in case of a cycle. * * @param maxDepth * The max depth > 0 */ public ObjectFormatter(int maxDepth) { this.maxDepth = maxDepth; } /** * Gets the string representation of the given object. * * @param object * The instance * @return The string representation, not null */ public String format(Object object) { StringBuilder result = new StringBuilder(); formatImpl(object, 0, result); return result.toString(); } /** * Actual implementation of the formatting. * * @param object * The instance * @param currentDepth * The current recursion depth * @param result * The builder to append the result to, not null */ protected void formatImpl(Object object, int currentDepth, StringBuilder result) { // get the actual value if the value is wrapped by a Hibernate proxy object = getUnproxiedValue(object); if (object == null) { result.append(String.valueOf(object)); return; } if (object instanceof String) { result.append('"'); result.append(object); result.append('"'); return; } if (object instanceof Number || object instanceof Date) { result.append(String.valueOf(object)); return; } if (object instanceof Character) { result.append('\''); result.append(String.valueOf(object)); result.append('\''); return; } Class type = object.getClass(); if (type.isPrimitive() || type.isEnum()) { result.append(String.valueOf(object)); return; } if (formatProxy(object, result)) { return; } if (type.getName().startsWith("java.lang")) { result.append(String.valueOf(object)); return; } if (type.isArray()) { formatArray(object, currentDepth, result); return; } if (object instanceof Collection) { formatCollection((Collection<?>) object, currentDepth, result); return; } if (object instanceof Map) { formatMap((Map<?, ?>) object, currentDepth, result); return; } if (currentDepth >= maxDepth) { result.append(getShortClassName(type)); result.append("<...>"); return; } formatObject(object, currentDepth, result); } /** * Formats the given array. * * @param array * The array, not null * @param currentDepth * The current recursion depth * @param result * The builder to append the result to, not null */ protected void formatArray(Object array, int currentDepth, StringBuilder result) { if (array instanceof byte[]) { result.append(Arrays.toString((byte[]) array)); return; } if (array instanceof short[]) { result.append(Arrays.toString((short[]) array)); return; } if (array instanceof int[]) { result.append(Arrays.toString((int[]) array)); return; } if (array instanceof long[]) { result.append(Arrays.toString((long[]) array)); return; } if (array instanceof char[]) { result.append(Arrays.toString((char[]) array)); return; } if (array instanceof float[]) { result.append(Arrays.toString((float[]) array)); return; } if (array instanceof double[]) { result.append(Arrays.toString((double[]) array)); return; } if (array instanceof boolean[]) { result.append(Arrays.toString((boolean[]) array)); return; } // format an object array result.append("["); boolean notFirst = false; for (Object element : (Object[]) array) { if (notFirst) { result.append(", "); } else { notFirst = true; } formatImpl(element, currentDepth + 1, result); } result.append("]"); } /** * Formats the given collection. * * @param collection * The collection, not null * @param currentDepth * The current recursion depth * @param result * The builder to append the result to, not null */ protected void formatCollection(Collection<?> collection, int currentDepth, StringBuilder result) { result.append("["); boolean notFirst = false; for (Object element : collection) { if (notFirst) { result.append(", "); } else { notFirst = true; } formatImpl(element, currentDepth + 1, result); } result.append("]"); } /** * Formats the given map. * * @param map * The map, not null * @param currentDepth * The current recursion depth * @param result * The builder to append the result to, not null */ protected void formatMap(Map<?, ?> map, int currentDepth, StringBuilder result) { result.append("{"); boolean notFirst = false; for (Map.Entry<?, ?> element : map.entrySet()) { if (notFirst) { result.append(", "); } else { notFirst = true; } formatImpl(element.getKey(), currentDepth, result); result.append("="); formatImpl(element.getValue(), currentDepth + 1, result); } result.append("}"); } /** * Formats the given object by formatting the inner fields. * * @param object * The object, not null * @param currentDepth * The current recursion depth * @param result * The builder to append the result to, not null */ protected void formatObject(Object object, int currentDepth, StringBuilder result) { Class type = object.getClass(); result.append(getShortClassName(type)); result.append("<"); formatFields(object, type, currentDepth, result); result.append(">"); } /** * Formats the field values of the given object. * * @param object * The object, not null * @param clazz * The class for which to format the fields, not null * @param currentDepth * The current recursion depth * @param result * The builder to append the result to, not null */ protected void formatFields(Object object, Class clazz, int currentDepth, StringBuilder result) { Field[] fields = clazz.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); for (int i = 0; i < fields.length; i++) { // skip transient and static fields Field field = fields[i]; if (isTransient(field.getModifiers()) || isStatic(field.getModifiers()) || field.isSynthetic()) { continue; } try { if (i > 0) { result.append(", "); } result.append(field.getName()); result.append("="); formatImpl(field.get(object), currentDepth + 1, result); } catch (IllegalAccessException e) { // this can't happen. Would get a Security exception instead // throw a runtime exception in case the impossible happens. throw new InternalError("Unexpected IllegalAccessException"); } } // format fields declared in superclass Class superclazz = clazz.getSuperclass(); while (superclazz != null && !superclazz.getName().startsWith("java.lang")) { formatFields(object, superclazz, currentDepth, result); superclazz = superclazz.getSuperclass(); } } protected boolean formatProxy(Object object, StringBuilder result) { String className = getShortClassName(object.getClass()); int index = className.indexOf("..EnhancerByCGLIB.."); if (index > 0) { result.append("Proxy<"); result.append(className.substring(0, index)); result.append(">"); return true; } return false; } }