/* * * * Copyright 2010, Unitils.org * * * * 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 org.unitils.core.util; import java.io.File; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.util.Collection; import java.util.Date; import java.util.Map; import static java.lang.reflect.Modifier.isStatic; import static java.lang.reflect.Modifier.isTransient; import static org.apache.commons.lang.ClassUtils.getShortClassName; import static org.unitils.reflectionassert.util.HibernateUtil.getUnproxiedValue; /** * 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. * * @author Tim Ducheyne * @author Filip Neven */ public class ObjectFormatter { public static final String MOCK_NAME_CHAIN_SEPARATOR = "##chained##"; /* The maximum recursion depth */ protected int maxDepth; /* The maximum nr of elements for arrays and collections to display */ protected int maxNrArrayOrCollectionElements; protected ArrayAndCollectionFormatter arrayAndCollectionFormatter; /** * Creates a formatter with a maximum recursion depth of 3. */ public ObjectFormatter() { this(3, 15); } /** * 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 * @param maxNrArrayOrCollectionElements The maximum nr of elements for arrays and collections to display > 0 */ public ObjectFormatter(int maxDepth, int maxNrArrayOrCollectionElements) { this.maxDepth = maxDepth; this.maxNrArrayOrCollectionElements = maxNrArrayOrCollectionElements; this.arrayAndCollectionFormatter = new ArrayAndCollectionFormatter(maxNrArrayOrCollectionElements, this); } /** * 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 (formatString(object, result)) { return; } if (formatNumberOrDate(object, result)) { return; } Class<?> type = object.getClass(); if (formatCharacter(object, type, result)) { return; } if (formatPrimitiveOrEnum(object, type, result)) { return; } if (formatMock(object, result)) { return; } if (formatProxy(object, type, result)) { return; } if (formatJavaLang(object, result, type)) { return; } if (type.isArray()) { arrayAndCollectionFormatter.formatArray(object, currentDepth, result); return; } if (object instanceof Collection) { arrayAndCollectionFormatter.formatCollection((Collection<?>) object, currentDepth, result); return; } if (object instanceof Map) { arrayAndCollectionFormatter.formatMap((Map<?, ?>) object, currentDepth, result); return; } if (currentDepth >= maxDepth) { result.append(getShortClassName(type)); result.append("<...>"); return; } if (formatFile(object, result)) { return; } formatObject(object, currentDepth, result); } protected boolean formatJavaLang(Object object, StringBuilder result, Class<?> type) { if (type.getName().startsWith("java.lang")) { result.append(String.valueOf(object)); return true; } return false; } protected boolean formatPrimitiveOrEnum(Object object, Class<?> type, StringBuilder result) { if (type.isPrimitive() || type.isEnum()) { result.append(String.valueOf(object)); return true; } return false; } protected boolean formatCharacter(Object object, Class<?> type, StringBuilder result) { if (object instanceof Character || Character.TYPE.equals(type)) { result.append('\''); result.append(String.valueOf(object)); result.append('\''); return true; } return false; } protected boolean formatNumberOrDate(Object object, StringBuilder result) { if (object instanceof Number || object instanceof Date) { result.append(String.valueOf(object)); return true; } return false; } protected boolean formatString(Object object, StringBuilder result) { if (object instanceof String) { result.append('"'); result.append(object); result.append('"'); return true; } return false; } /** * 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 formatMock(Object object, StringBuilder result) { try { Class<?> proxyUtilsClass = getProxyUtilsClass(); if (proxyUtilsClass == null) { return false; } String mockName = (String) proxyUtilsClass.getMethod("getMockName", Object.class).invoke(null, object); if (mockName == null) { return false; } mockName = mockName.replaceAll(MOCK_NAME_CHAIN_SEPARATOR, "."); if (isDummy(object)) { result.append("Dummy<"); } else { result.append("Mock<"); } result.append(mockName); result.append(">"); return true; } catch (Exception e) { return false; } } protected boolean isDummy(Object object) { Class<?> clazz = object.getClass(); Class<?> dummyObjectClass = getDummyObjectClass(); return dummyObjectClass != null && dummyObjectClass.isAssignableFrom(clazz); } protected boolean formatProxy(Object object, Class<?> type, StringBuilder result) { if (Proxy.isProxyClass(type)) { result.append("Proxy<?>"); return true; } 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; } protected boolean formatFile(Object object, StringBuilder result) { if (object instanceof File) { result.append("File<"); result.append(((File) object).getPath()); result.append(">"); return true; } return false; } /** * @return The interface that represents a dummy object. If the DummyObject interface is not in the * classpath, null is returned. */ protected Class<?> getDummyObjectClass() { try { return Class.forName("org.unitils.mock.dummy.DummyObject"); } catch (ClassNotFoundException e) { return null; } } /** * @return The proxy utils. null if not in classpath */ protected Class<?> getProxyUtilsClass() { try { return Class.forName("org.unitils.mock.core.proxy.ProxyUtils"); } catch (ClassNotFoundException e) { return null; } } }