/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.safeinvoker; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Hibernate; import org.hibernate.proxy.HibernateProxy; /** * This is a single static utility that is used to process any object just prior to sending it over the wire * to remote clients (like GWT clients or remote web service clients). * * Essentially this utility scrubs the object of all Hibernate proxies, cleaning it such that it * can be serialized over the wire successfully. * * @author Greg Hinkle * @author Jay Shaughnessy * @author John Mazzitelli */ @SuppressWarnings("unchecked") public class HibernateDetachUtility { private static final Log LOG = LogFactory.getLog(HibernateDetachUtility.class); public static enum SerializationType { SERIALIZATION, JAXB } /* * This is the type of object that will be used to generate identity * hashcodes for objects that are being scanned during the detach. * During production runtime, the hashCodeGenerator will use the * java.lang.System mechanism, but since this is package scoped, * tests can override this since. (See BZ 732089). */ static interface HashCodeGenerator { Integer getHashCode(Object value); } static class SystemHashCodeGenerator implements HashCodeGenerator { @Override public Integer getHashCode(Object value) { return System.identityHashCode(value); } } static HashCodeGenerator hashCodeGenerator = new SystemHashCodeGenerator(); // be able to configure the deepest recursion this utility will be allowed to go (see BZ 702109 that precipitated this need) private static final String DEPTH_ALLOWED_SYSPROP = "rhq.server.hibernate-detach-utility.depth-allowed"; private static final String THROW_EXCEPTION_ON_DEPTH_LIMIT_SYSPROP = "rhq.server.hibernate-detach-utility.throw-exception-on-depth-limit"; private static final int depthAllowed; private static final boolean throwExceptionOnDepthLimit; private static final String DUMP_STACK_THRESHOLDS_SYSPROP = "rhq.server.hibernate-detach-utility.dump-stack-thresholds"; // e.g. "5000:10000" (millis:num-objects) private static final boolean dumpStackOnThresholdLimit; private static final long millisThresholdLimit; private static final int sizeThresholdLimit; static { int value; try { String str = System.getProperty(DEPTH_ALLOWED_SYSPROP, "100"); value = Integer.parseInt(str); } catch (Throwable t) { value = 100; } depthAllowed = value; boolean booleanValue; try { String str = System.getProperty(THROW_EXCEPTION_ON_DEPTH_LIMIT_SYSPROP, "true"); booleanValue = Boolean.parseBoolean(str); } catch (Throwable t) { booleanValue = true; } throwExceptionOnDepthLimit = booleanValue; boolean tmp_dumpStackOnThresholdLimit; long tmp_millisThresholdLimit; int tmp_sizeThresholdLimit; String prop = null; try { prop = System.getProperty(DUMP_STACK_THRESHOLDS_SYSPROP); if (prop != null) { String[] nums = prop.split(":"); tmp_dumpStackOnThresholdLimit = true; tmp_millisThresholdLimit = Long.parseLong(nums[0]); tmp_sizeThresholdLimit = Integer.parseInt(nums[1]); } else { tmp_dumpStackOnThresholdLimit = false; tmp_millisThresholdLimit = Long.MAX_VALUE; tmp_sizeThresholdLimit = Integer.MAX_VALUE; } } catch (Throwable t) { LOG.warn("Bad value for [" + DUMP_STACK_THRESHOLDS_SYSPROP + "]=[" + prop + "]: " + t.toString()); tmp_dumpStackOnThresholdLimit = true; // they wanted to set it to something, so give them some defaults tmp_millisThresholdLimit = 5000L; // 5 seconds tmp_sizeThresholdLimit = 10000; // 10K objects } dumpStackOnThresholdLimit = tmp_dumpStackOnThresholdLimit; millisThresholdLimit = tmp_millisThresholdLimit; sizeThresholdLimit = tmp_sizeThresholdLimit; } public static void nullOutUninitializedFields(Object value, SerializationType serializationType) throws Exception { long start = System.currentTimeMillis(); Map<Integer, Object> checkedObjectMap = new HashMap<Integer, Object>(); Map<Integer, List<Object>> checkedObjectCollisionMap = new HashMap<Integer, List<Object>>(); nullOutUninitializedFields(value, checkedObjectMap, checkedObjectCollisionMap, 0, serializationType); long duration = System.currentTimeMillis() - start; if (dumpStackOnThresholdLimit) { int numObjectsProcessed = checkedObjectMap.size(); if (duration > millisThresholdLimit || numObjectsProcessed > sizeThresholdLimit) { String rootObjectString = (value != null) ? value.getClass().toString() : "null"; LOG.warn("Detached [" + numObjectsProcessed + "] objects in [" + duration + "]ms from root object [" + rootObjectString + "]", new Throwable("HIBERNATE DETACH UTILITY STACK TRACE")); } } else { // 10s is really long, log SOMETHING if (duration > 10000L && LOG.isDebugEnabled()) { LOG.debug("Detached [" + checkedObjectMap.size() + "] objects in [" + duration + "]ms"); } } // help the garbage collector be clearing these before we leave checkedObjectMap.clear(); checkedObjectCollisionMap.clear(); } /** * @param value the object needing to be detached/scrubbed. * @param checkedObjectMap This maps identityHashCodes to Objects we've already detached. In that way we can * quickly determine if we've already done the work for the incoming value and avoid taversing it again. This * works well almost all of the time, but it is possible that two different objects can have the same identity hash * (conflicts are always possible with a hash). In that case we utilize the checkedObjectCollisionMap (see below). * @param checkedObjectCollisionMap checkedObjectMap maps the identityhash to the *first* object with that hash. In * most cases there will only be mapping for one hash, but it is possible to encounter the same hash for multiple * objects, especially on 32bit or IBM JVMs. It is important to know if an object has already been detached * because if it is somehow self-referencing, we have to stop the recursion. This map holds the 2nd..Nth mapping * for a single hash and is used to ensure we never try to detach an object already processed. * @param depth used to stop infinite recursion, defaults to a depth we don't expectto see, but it is configurable. * @param serializationType * @throws Exception if a problem occurs * @throws IllegalStateException if the recursion depth limit is reached */ private static void nullOutUninitializedFields(Object value, Map<Integer, Object> checkedObjectMap, Map<Integer, List<Object>> checkedObjectCollisionMap, int depth, SerializationType serializationType) throws Exception { if (depth > depthAllowed) { String warningMessage = "Recursed too deep [" + depth + " > " + depthAllowed + "], will not attempt to detach object of type [" + ((value != null) ? value.getClass().getName() : "N/A") + "]. This may cause serialization errors later. " + "You can try to work around this by setting the system property [" + DEPTH_ALLOWED_SYSPROP + "] to a value higher than [" + depth + "] or you can set the system property [" + THROW_EXCEPTION_ON_DEPTH_LIMIT_SYSPROP + "] to 'false'"; LOG.warn(warningMessage); if (throwExceptionOnDepthLimit) { throw new IllegalStateException(warningMessage); } return; } if (null == value) { return; } // System.identityHashCode is a hash code, and therefore not guaranteed to be unique. And we've seen this // be the case. So, we use it to try and avoid duplicating work, but handle the case when two objects may // have an identity crisis. Integer valueIdentity = hashCodeGenerator.getHashCode(value); Object checkedObject = checkedObjectMap.get(valueIdentity); if (null == checkedObject) { // if we have not yet encountered an object with this hash, store it in our map and start scrubbing checkedObjectMap.put(valueIdentity, value); } else if (value == checkedObject) { // if we have scrubbed this already, no more work to be done return; } else { // we have a situation where multiple objects have the same identity hashcode, work with our // collision map to decide whether it needs to be scrubbed and add if necessary. // Note that this code block is infrequently hit, it is by design that we've pushed the extra // work, map, etc, involved for this infrequent case into its own block. The standard cases must // be as fast and lean as possible. boolean alreadyDetached = false; List<Object> collisionObjects = checkedObjectCollisionMap.get(valueIdentity); if (null == collisionObjects) { // if this is the 2nd occurrence for this hash, create a new map entry collisionObjects = new ArrayList<Object>(1); checkedObjectCollisionMap.put(valueIdentity, collisionObjects); } else { // if we have scrubbed this already, no more work to be done for (Object collisionObject : collisionObjects) { if (value == collisionObject) { alreadyDetached = true; break; } } } if (LOG.isDebugEnabled()) { StringBuilder message = new StringBuilder("\n\tIDENTITY HASHCODE COLLISION [hash="); message.append(valueIdentity); message.append(", alreadyDetached="); message.append(alreadyDetached); message.append("]"); message.append("\n\tCurrent : "); message.append(value.getClass().getName()); message.append("\n\t "); message.append(value); message.append("\n\tPrevious : "); message.append(checkedObject.getClass().getName()); message.append("\n\t "); message.append(checkedObject); for (Object collisionObject : collisionObjects) { message.append("\n\tPrevious : "); message.append(collisionObject.getClass().getName()); message.append("\n\t "); message.append(collisionObject); } LOG.debug(message); } // now that we've done our logging, if already detached we're done. Otherwise add to the list of collision // objects for this hash, and start scrubbing if (alreadyDetached) { return; } collisionObjects.add(value); } // Perform the detaching if (value instanceof Object[]) { Object[] objArray = (Object[]) value; for (int i = 0; i < objArray.length; i++) { Object listEntry = objArray[i]; Object replaceEntry = replaceObject(listEntry); if (replaceEntry != null) { objArray[i] = replaceEntry; } nullOutUninitializedFields(objArray[i], checkedObjectMap, checkedObjectCollisionMap, depth + 1, serializationType); } } else if (value instanceof List) { // Null out any entries in initialized collections ListIterator i = ((List) value).listIterator(); while (i.hasNext()) { Object val = i.next(); Object replace = replaceObject(val); if (replace != null) { val = replace; i.set(replace); } nullOutUninitializedFields(val, checkedObjectMap, checkedObjectCollisionMap, depth + 1, serializationType); } } else if (value instanceof Collection) { Collection collection = (Collection) value; Collection itemsToBeReplaced = new ArrayList(); Collection replacementItems = new ArrayList(); for (Object item : collection) { Object replacementItem = replaceObject(item); if (replacementItem != null) { itemsToBeReplaced.add(item); replacementItems.add(replacementItem); item = replacementItem; } nullOutUninitializedFields(item, checkedObjectMap, checkedObjectCollisionMap, depth + 1, serializationType); } collection.removeAll(itemsToBeReplaced); collection.addAll(replacementItems); // watch out! if this collection is a Set, HashMap$MapSet doesn't support addAll. See BZ 688000 } else if (value instanceof Map) { Map originalMap = (Map) value; HashMap<Object, Object> replaceMap = new HashMap<Object, Object>(); for (Iterator i = originalMap.keySet().iterator(); i.hasNext();) { // get original key and value - these might be hibernate proxies Object originalKey = i.next(); Object originalKeyValue = originalMap.get(originalKey); // replace with non-hibernate classes, if appropriate (will be null otherwise) Object replaceKey = replaceObject(originalKey); Object replaceValue = replaceObject(originalKeyValue); // if either original key or original value was a hibernate proxy object, we have to // remove it from the original map, and remember the replacement objects for later if (replaceKey != null || replaceValue != null) { Object newKey = (replaceKey != null) ? replaceKey : originalKey; Object newValue = (replaceValue != null) ? replaceValue : originalKeyValue; replaceMap.put(newKey, newValue); i.remove(); } } // all hibernate proxies have been removed, we need to replace them with their // non-proxy object representations that we got from replaceObject() calls originalMap.putAll(replaceMap); // now go through each item in the map and null out their internal fields for (Object key : originalMap.keySet()) { nullOutUninitializedFields(originalMap.get(key), checkedObjectMap, checkedObjectCollisionMap, depth + 1, serializationType); nullOutUninitializedFields(key, checkedObjectMap, checkedObjectCollisionMap, depth + 1, serializationType); } } else if (value instanceof Enum) { // don't need to detach enums, treat them as special objects return; } if (serializationType == SerializationType.JAXB) { XmlAccessorType at = value.getClass().getAnnotation(XmlAccessorType.class); if (at != null && at.value() == XmlAccessType.FIELD) { nullOutFieldsByFieldAccess(value, checkedObjectMap, checkedObjectCollisionMap, depth, serializationType); } else { nullOutFieldsByAccessors(value, checkedObjectMap, checkedObjectCollisionMap, depth, serializationType); } } else if (serializationType == SerializationType.SERIALIZATION) { nullOutFieldsByFieldAccess(value, checkedObjectMap, checkedObjectCollisionMap, depth, serializationType); } } private static void nullOutFieldsByFieldAccess(Object object, Map<Integer, Object> checkedObjects, Map<Integer, List<Object>> checkedObjectCollisionMap, int depth, SerializationType serializationType) throws Exception { Class tmpClass = object.getClass(); List<Field> fieldsToClean = new ArrayList<Field>(); while (tmpClass != null && tmpClass != Object.class) { Field[] declaredFields = tmpClass.getDeclaredFields(); for (Field declaredField : declaredFields) { // do not process static final or transient fields since they won't be serialized anyway int modifiers = declaredField.getModifiers(); if (!((Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers)) || Modifier.isTransient(modifiers))) { fieldsToClean.add(declaredField); } } tmpClass = tmpClass.getSuperclass(); } nullOutFieldsByFieldAccess(object, fieldsToClean, checkedObjects, checkedObjectCollisionMap, depth, serializationType); } private static void nullOutFieldsByFieldAccess(Object object, List<Field> classFields, Map<Integer, Object> checkedObjects, Map<Integer, List<Object>> checkedObjectCollisionMap, int depth, SerializationType serializationType) throws Exception { boolean accessModifierFlag = false; for (Field field : classFields) { accessModifierFlag = false; if (!field.isAccessible()) { field.setAccessible(true); accessModifierFlag = true; } Object fieldValue = field.get(object); if (fieldValue instanceof HibernateProxy) { Object replacement = null; String assistClassName = fieldValue.getClass().getName(); if (assistClassName.contains("jvst") || assistClassName.contains("EnhancerByCGLIB")) { Class assistClass = fieldValue.getClass(); try { Method m = assistClass.getMethod("writeReplace"); replacement = m.invoke(fieldValue); String assistNameDelimiter = assistClassName.contains("jvst") ? "_$$_" : "$$"; assistClassName = assistClassName.substring(0, assistClassName.indexOf(assistNameDelimiter)); if (replacement != null && !replacement.getClass().getName().contains("hibernate")) { nullOutUninitializedFields(replacement, checkedObjects, checkedObjectCollisionMap, depth + 1, serializationType); field.set(object, replacement); } else { replacement = null; } } catch (Exception e) { LOG.error("Unable to write replace object " + fieldValue.getClass(), e); } } if (replacement == null) { String className = ((HibernateProxy) fieldValue).getHibernateLazyInitializer().getEntityName(); //see if there is a context classloader we should use instead of the current one. ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); Class clazz = contextClassLoader == null ? Class.forName(className) : Class.forName(className, true, contextClassLoader); Class[] constArgs = { Integer.class }; Constructor construct = null; try { construct = clazz.getConstructor(constArgs); replacement = construct.newInstance((Integer) ((HibernateProxy) fieldValue) .getHibernateLazyInitializer().getIdentifier()); field.set(object, replacement); } catch (NoSuchMethodException nsme) { try { Field idField = clazz.getDeclaredField("id"); Constructor ct = clazz.getDeclaredConstructor(); ct.setAccessible(true); replacement = ct.newInstance(); if (!idField.isAccessible()) { idField.setAccessible(true); } idField.set(replacement, ((HibernateProxy) fieldValue) .getHibernateLazyInitializer().getIdentifier()); } catch (Exception e) { e.printStackTrace(); LOG.error("No id constructor and unable to set field id for base bean " + className, e); } field.set(object, replacement); } } } else { if (fieldValue instanceof org.hibernate.collection.spi.PersistentCollection) { // Replace hibernate specific collection types if (!((org.hibernate.collection.spi.PersistentCollection) fieldValue).wasInitialized()) { field.set(object, null); } else { Object replacement = null; boolean needToNullOutFields = true; // needed for BZ 688000 if (fieldValue instanceof Map) { replacement = new HashMap((Map) fieldValue); } else if (fieldValue instanceof List) { replacement = new ArrayList((List) fieldValue); } else if (fieldValue instanceof Set) { ArrayList l = new ArrayList((Set) fieldValue); // cannot recurse Sets, see BZ 688000 nullOutUninitializedFields(l, checkedObjects, checkedObjectCollisionMap, depth + 1, serializationType); replacement = new HashSet(l); // convert it back to a Set since that's the type of the real collection, see BZ 688000 needToNullOutFields = false; } else if (fieldValue instanceof Collection) { replacement = new ArrayList((Collection) fieldValue); } setField(object, field.getName(), replacement); if (needToNullOutFields) { nullOutUninitializedFields(replacement, checkedObjects, checkedObjectCollisionMap, depth + 1, serializationType); } } } else { if (fieldValue != null && (fieldValue.getClass().getName().contains("org.rhq") || fieldValue instanceof Collection || fieldValue instanceof Object[] || fieldValue instanceof Map)) nullOutUninitializedFields((fieldValue), checkedObjects, checkedObjectCollisionMap, depth + 1, serializationType); } } if (accessModifierFlag) { field.setAccessible(false); } } } private static Object replaceObject(Object object) { Object replacement = null; if (object instanceof HibernateProxy) { if (object.getClass().getName().contains("jvst")) { Class assistClass = object.getClass(); try { Method m = assistClass.getMethod("writeReplace"); replacement = m.invoke(object); } catch (Exception e) { LOG.error("Unable to write replace object " + object.getClass(), e); } } } return replacement; } private static void nullOutFieldsByAccessors(Object value, Map<Integer, Object> checkedObjects, Map<Integer, List<Object>> checkedObjectCollisionMap, int depth, SerializationType serializationType) throws Exception { // Null out any collections that aren't loaded BeanInfo bi = Introspector.getBeanInfo(value.getClass(), Object.class); PropertyDescriptor[] pds = bi.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { Object propertyValue = null; try { propertyValue = pd.getReadMethod().invoke(value); } catch (Throwable lie) { if (LOG.isDebugEnabled()) { LOG.debug("Couldn't load: " + pd.getName() + " off of " + value.getClass().getSimpleName(), lie); } } if (!Hibernate.isInitialized(propertyValue)) { try { if (LOG.isDebugEnabled()) { LOG.debug("Nulling out: " + pd.getName() + " off of " + value.getClass().getSimpleName()); } Method writeMethod = pd.getWriteMethod(); if ((writeMethod != null) && (writeMethod.getAnnotation(XmlTransient.class) == null)) { pd.getWriteMethod().invoke(value, new Object[] { null }); } else { nullOutField(value, pd.getName()); } } catch (Exception lie) { LOG.debug("Couldn't null out: " + pd.getName() + " off of " + value.getClass().getSimpleName() + " trying field access", lie); nullOutField(value, pd.getName()); } } else { if ((propertyValue instanceof Collection) || ((propertyValue != null) && propertyValue.getClass().getName().startsWith("org.rhq.core.domain"))) { nullOutUninitializedFields(propertyValue, checkedObjects, checkedObjectCollisionMap, depth + 1, serializationType); } } } } private static void setField(Object object, String fieldName, Object newValue) { try { Field f = object.getClass().getDeclaredField(fieldName); if (f != null) { // try to set the field this way f.setAccessible(true); f.set(object, newValue); } } catch (NoSuchFieldException e) { // ignore this } catch (IllegalAccessException e) { // ignore this } } private static void nullOutField(Object value, String fieldName) { try { Field f = value.getClass().getDeclaredField(fieldName); if (f != null) { // try to set the field this way f.setAccessible(true); f.set(value, null); } } catch (NoSuchFieldException e) { // ignore this } catch (IllegalAccessException e) { // ignore this } } }