/* * Copyright 2011 Red Hat Inc. * * 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.drools.persistence.marshalling.util; import static org.junit.Assert.fail; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.util.AbstractCollection; import java.util.AbstractSet; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import org.drools.core.base.ClassFieldAccessorCache; import org.drools.core.impl.StatefulKnowledgeSessionImpl; import org.drools.core.util.AbstractHashTable; import org.drools.core.impl.KnowledgeBaseImpl; import org.drools.core.process.instance.impl.WorkItemImpl; import org.drools.core.time.impl.JDKTimerService; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CompareViaReflectionUtil { private static Logger logger = LoggerFactory.getLogger(CompareViaReflectionUtil.class); public static IdentityHashMap<Object, Object> seenObjects = null; private static Class<?> OBJECT_ARRAY_CLASS = (new Object[0]).getClass(); private static int TO_ARRAY = 0; private static int ENTRY_SET = 1; private static HashSet<Package> javaPackages = new HashSet<Package>(); static { // primitives ("package null") javaPackages.add(long.class.getPackage()); // java.math javaPackages.add(BigDecimal.class.getPackage()); // java.util javaPackages.add(AbstractCollection.class.getPackage()); // java.util.concurrent javaPackages.add(BlockingQueue.class.getPackage()); // java.util.concurrent.atomic javaPackages.add(AtomicInteger.class.getPackage()); // java.util.concurrent.atomic // javaPackages.add(ReentrantLock.class.getPackage()); javaPackages.add(Long.class.getPackage()); } private static HashMap<Class<?>, Integer> arrClassMap = new HashMap<Class<?>, Integer>(); { arrClassMap.put((new byte[0]).getClass(), BYTE); arrClassMap.put((new short[0]).getClass(), SHORT); arrClassMap.put((new int[0]).getClass(), INT); arrClassMap.put((new long[0]).getClass(), LONG); arrClassMap.put((new float[0]).getClass(), FLOAT); arrClassMap.put((new double[0]).getClass(), DOUBLE); arrClassMap.put((new boolean[0]).getClass(), BOOLEAN); arrClassMap.put((new char[0]).getClass(), CHAR); arrClassMap.put((new Object[0]).getClass(), OBJECT); } private static HashSet<Field> doNotCompareFieldsMap = new HashSet<Field>(); static { try { doNotCompareFieldsMap.add(StatefulKnowledgeSessionImpl.class.getDeclaredField("id")); doNotCompareFieldsMap.add(KnowledgeBaseImpl.class.getDeclaredField("mappedKnowledgeBaseListeners")); doNotCompareFieldsMap.add(KnowledgeBaseImpl.class.getDeclaredField("id")); doNotCompareFieldsMap.add(KnowledgeBaseImpl.class.getDeclaredField("workingMemoryCounter")); doNotCompareFieldsMap.add(WorkItemImpl.class.getDeclaredField("id")); doNotCompareFieldsMap.add(WorkItemImpl.class.getDeclaredField("processInstanceId")); doNotCompareFieldsMap.add(ClassFieldAccessorCache.class.getDeclaredField("classLoader")); doNotCompareFieldsMap.add(StatefulKnowledgeSessionImpl.class.getDeclaredField("globalResolver")); doNotCompareFieldsMap.add(JDKTimerService.class.getDeclaredField("scheduler")); doNotCompareFieldsMap.add(KnowledgeBaseImpl.class.getDeclaredField("classFieldAccessorCache")); } catch (Exception e) { logger.error(e.getClass().getSimpleName() + ": " + e.getMessage()); // do nothing } } private final static int BYTE = 0; private final static int SHORT = 1; private final static int INT = 2; private final static int LONG = 3; private final static int FLOAT = 4; private final static int DOUBLE = 5; private final static int BOOLEAN = 6; private final static int CHAR = 7; private final static int OBJECT = 8; private final static int NULL = 9; @SuppressWarnings("rawtypes") private static HashSet<Class> atomicPrimitiveClasses = new HashSet<Class>(); static { atomicPrimitiveClasses.add(AtomicBoolean.class); atomicPrimitiveClasses.add(AtomicInteger.class); atomicPrimitiveClasses.add(AtomicLong.class); atomicPrimitiveClasses.add(AtomicReference.class); } @SuppressWarnings("rawtypes") private static HashSet<Class> atomicArrayClasses = new HashSet<Class>(); static { atomicArrayClasses.add(AtomicIntegerArray.class); atomicArrayClasses.add(AtomicLongArray.class); atomicArrayClasses.add(AtomicReferenceArray.class); } /** * This method compares two objects recursively. * @see #compareInstances(DebugContext, Object, Object) * @param objA The object to be compared to objB. * @param objB The object to be compared to objA. * @return Whether or not the two objects are equal. */ public static boolean compareInstances(Object objA, Object objB ) { seenObjects = new IdentityHashMap<Object, Object>(); return compareInstances(null, objA, objB); } /** * A Java object basically consists (or can consist) of 3 things: <ol> * <li>Methods, if present</li> * <li>Fields (attributes) which contain other Java objects</li> * <li>Fields which contain primitives or primitive based objects (int, Integer, String, etc.)</li> * <li>Fields which contain arrays, Java collection objects (byte [], Set, HashMap, LinkedBlockingQueue, etc..)</li> * </ol> * This means, that when we compare two instantiations of the same class, we can do it as follows:<ul> * <li>Methods: * <ul><li>We don't have to compare methods, since those don't have any "state".</li></ul> * </li><li>Objects: * <ul><li>Comparing fields containing other Java objects (that are don't belong to a java.* package) is simply a recursive operation.</li></ul> * </li><li>Primitive based: * <ul><li>Comparing primitive or primitive based objects can be done using <pre>objA.equals(objB)</pre></li></ul> * </li><li>Arrays: * <ul><li>We iterate through the array and compare elements to each other recursively.</li></ul> * </li><li>Collections: * <ul> * <li>Fields containing collection objects end up having one of two methods:<ul> * <li>toArray(), which returns an Object<?> []</li> * <li>entrySet(), which returns a Set<Map.Entry<K, V>> object</li> * </ul></li> * </ul> * </ul> * And that is exactly what we do in this method! We recursively step through the object tree which * has, as its root node, the class defined by the initial objects given to this method. * </p> * Lastly, you might see the following output if the TRACE level is set for logging out of this object: * <pre> * 0 : objA and objB are both null * X : objA and objB are unequal * = : objA and objB are equal * == : objA and objB are the <i>same</i> instance (of the same object) * (=) : objA and objB are class objects and thus both equal and the same * % : objA and objB are not being compared * ! : objA has already been compared and will not be compared again * in order to avoid cycles in the object tree</pre> * <b>Note</b>: This method has a few weaknesses:<ul> * <li>If the object tree contains objects that <i>extend</i> collection objects (HashMap, List, etc.), this method might not compare them * correctly or efficiently. The logic to do this fairly trivial but just hasn't been added yet.</li> * <li>If the object tree contains objects that are "home made" collection objects, this method might * also not compare these correctly or efficiently. The logic to do this is non-trivial and to some degree * dependent on how the objects are implemented and what type of access is given to the data in the object.</li> * </ul> * One last thing to remember here is the following issue:<ul> * <li>Due to how some objects are created, it's possible that the object tree of objA and object tree of objB * contain the <i>same</i> object instantiation. This will mostly be the result of a static object being used * during the the initialization of the objects. But this is nonetheless wierd, because I don't expect this. * </li><li>When this happens, we say that objA and objB are the same</li> * </ul> * </p> * @param context This contains logging information (recursive level, position in object tree of objA/objB) * @param objA The first instantiation to be compared. * @param objB The first instantiation to be compared. * @return Whether or not objA and objB are equal, given the criteria above. */ private static boolean compareInstances(DebugContext context, Object objA, Object objB ) { if( context == null ) { context = new DebugContext(0, "", true); } boolean same = false; if( objA == null && objB == null ) { context.name += "0"; same = true; } else if( objA == null || objB == null ) { context.name += "X"; same = false; } else if( objA == objB ) { context.name += objA.getClass().getSimpleName() + " "; String stateSymbol = "=="; if( objA instanceof Class<?> | objA instanceof Class) { stateSymbol = "(=)"; } context.name += stateSymbol; same = true; } else if( objA.getClass().getName().equals(objB.getClass().getName()) ) { Class<?> objClass = objA.getClass(); context.name += "|" + context.level + "| " + objClass.getSimpleName(); boolean primitiveBasedObjectOrCollection = false; if( javaPackages.contains(objClass.getPackage()) ) { primitiveBasedObjectOrCollection = true; same = comparePrimitiveBasedOrCollectionInstances(context, objA, objB); } else if( objA instanceof AbstractHashTable ) { primitiveBasedObjectOrCollection = true; same = compareDroolsSets(context, objA, objB); } else if( objA.getClass().isArray() ) { primitiveBasedObjectOrCollection = true; same = compareArrays(context, objA, objB); } else { // Check if it's an enum Class<?> superClass = objClass.getSuperclass(); while( superClass != null ) { if( superClass.equals(Enum.class) ) { same = objA.equals(objB); primitiveBasedObjectOrCollection = true; break; } superClass = superClass.getSuperclass(); } } if( ! primitiveBasedObjectOrCollection ) { if( seenObjects.put(objA, objA) == null ) { same = compareInstancesOfSameClass(context, objA, objB); if( !same ) { seenObjects.remove(objA); } } else { context.name += ": !"; same = true; } } } else { context.name += ": X"; } if( context.print ) { logger.trace( context.name ); } return same; } public static boolean compareArrays(DebugContext context, Object objA, Object objB) { // Determine array type class Object classTypeValue = arrClassMap.get(objA.getClass()); int classType = -1; if( classTypeValue != null) { classType = (Integer) classTypeValue; } else if( objA.getClass().isArray() ) { classType = OBJECT; } else if( classTypeValue == null ) { classType = NULL; } // Check Sameness boolean same = false; switch(classType) { case BYTE: same = Arrays.equals((byte []) objB,(byte [])objA); break; case SHORT: same = Arrays.equals((short []) objB,(short [])objA); break; case INT: same = Arrays.equals((int []) objB,(int [])objA); break; case LONG: same = Arrays.equals((long []) objB,(long [])objA); break; case FLOAT: same = Arrays.equals((float []) objB,(float [])objA); break; case DOUBLE: same = Arrays.equals((double []) objB,(double [])objA); break; case BOOLEAN: same = Arrays.equals((boolean []) objB,(boolean [])objA); break; case CHAR: same = Arrays.equals((char []) objB,(char [])objA); break; case OBJECT: int lengthA = Array.getLength(objA); int lengthB = Array.getLength(objB); if( lengthA != lengthB ) { same = false; } else if( lengthA == 0 ) { same = true; } else { same = true; for( int i = 0; same && i < lengthA; ++i ) { DebugContext subContext = context.clone(); subContext.level = context.level + 1; subContext.name = context.name + ": (" + i + ") "; same = compareInstances(subContext, Array.get(objA, i), Array.get(objB, i)); } } break; case NULL: same = objA == objB; break; default: fail( "Unable to determine class of array [" + classType + "]"); } return same; } /** * This method should be called when we've been able to ascertain (all) of the following:<ul> * <li>Neither of the objects are null</li> * <li>Both of the objects belong to the same class</li> * </ul> * In this method, we go through <i>all</i> fields of the object and compare them (recursively). * <br/> * @param context This contains logging information (recursive level, position in object tree of objA/objB) * @param objA The first instantiation to be compared. * @param objB The first instantiation to be compared. * @return Whether or not objA and objB are equal. */ private static boolean compareInstancesOfSameClass(DebugContext context, Object objA, Object objB) { boolean same = false; try { Class<?> objClass = objA.getClass(); do { Field [] fields = objClass.getDeclaredFields(); if( fields.length == 0 ) { same = true; } else { same = true; for( int i = 0; same && i < fields.length; ++i ) { DebugContext subContext = context.clone(); subContext.level = context.level + 1; subContext.name = context.name + ": " + fields[i].getName() + " > "; if( Modifier.isTransient(fields[i].getModifiers()) || doNotCompareFieldsMap.contains(fields[i]) ) { if( context.print ) { logger.trace( context.name + ": " + fields[i].getName() + " %" ); } continue; } fields[i].setAccessible(true); Object subObjA = fields[i].get(objA); Object subObjB = fields[i].get(objB); same = compareInstances(subContext, subObjA, subObjB); } } objClass = objClass.getSuperclass(); } while( objClass != null && same); context.name += ": " + (same == true ? "=" : "X"); } catch( Exception e ) { same = false; e.printStackTrace(); Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } return same; } private static boolean compareDroolsSets(DebugContext context, Object objA, Object objB) { boolean same = true; int length = 0; try { Method sizeMethod = AbstractHashTable.class.getDeclaredMethod("size", (Class []) null); Integer sizeA = (Integer) sizeMethod.invoke(objA, (Object []) null); Integer sizeB = (Integer) sizeMethod.invoke(objB, (Object []) null); if( ! sizeA.equals(sizeB) ) { return false; } length = sizeA.intValue(); } catch( Exception e ) { same = false; Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } if( length == 0 ) { return true; } Method toArrayMethod = null; try { toArrayMethod = AbstractHashTable.class.getDeclaredMethod("toArray", (Class []) null); } catch( Exception e ) { same = false; Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } if( toArrayMethod == null ) { fail("Could not retrieve toArray() method for " + objA.getClass().getName()); } Object [] arrayA = null; Object [] arrayB = null; try { arrayA = (Object []) toArrayMethod.invoke(objA, (Object []) null); arrayB = (Object []) toArrayMethod.invoke(objB, (Object []) null); } catch (Exception e) { same = false; fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } if( arrayA == null && arrayB == null ) { return true; } else if( arrayA == null || arrayB == null ) { return false; } for( int a = 0; a < length; ++a ) { boolean elementIsSame = false; for( int b = 0; b < length; ++b ) { Object subObjA = arrayA[a]; Object subObjB = arrayA[b]; try { Method getValueMethod = subObjA.getClass().getMethod("getValue", (Class []) null); subObjA = getValueMethod.invoke(subObjA, (Object [])null); subObjB = getValueMethod.invoke(subObjB, (Object [])null); } catch( Exception e ) { e.printStackTrace(); fail("Could not retrieve getValue() method for " + subObjA.getClass().getName()); } DebugContext entryContext = context.clone(); String name = context.name + ": "; entryContext.level = context.level + 1; entryContext.name = name + "<entry> "; entryContext.print = false; if( compareInstances(entryContext, subObjA, subObjB) ) { logger.trace(entryContext.name); elementIsSame = true; break; } } if( ! elementIsSame ) { // a matching element was not found in arrayB same = false; break; } } return same; } /** * Compare two objects which are all of the following: <ul> * <li>A primitive or primitive based object</li> * <li>A collection object</li> * </ul> * @param context This contains logging information (recursive level, position in object tree of objA/objB) * @param objA The first instantiation to be compared. * @param objB The first instantiation to be compared. * @return Whether or not objA and objB are equal. */ private static boolean comparePrimitiveBasedOrCollectionInstances(DebugContext context, Object objA, Object objB) { boolean same = false; Class<?> objClass = objA.getClass(); Method [] methods = getMethodToRetrieveCollection(objClass); try { if( methods[TO_ARRAY] != null ) { same = compareArrayBasedObjects(context, methods[TO_ARRAY], objA, objB); } else if( methods[ENTRY_SET] != null) { same = compareEntrySetBasedObjects(context, methods[ENTRY_SET], objA, objB); } else if( objClass.isArray() ) { same = compareArrays(context, objA, objB); } else if( atomicPrimitiveClasses.contains(objClass) ) { same = compareAtomicPrimitives(objA, objB); } else if( atomicArrayClasses.contains(objClass) ) { same = compareAtomicArrays(context, objA, objB); } else { same = objA.equals(objB); } context.name += ": " + (same == true ? "=" : "X"); } catch( Exception e ) { e.printStackTrace(); same = false; Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } return same; } protected static boolean compareAtomicPrimitives(Object objA, Object objB) { boolean same = false; try { Method getMethod = objA.getClass().getMethod("get", new Class[0]); Object valA = getMethod.invoke(objA, (Object []) null); Object valB = getMethod.invoke(objB, (Object []) null); if( valA.equals(valB) ) { same = true; } } catch( Exception e ) { e.printStackTrace(); same = false; Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } return same; } protected static boolean compareAtomicArrays(DebugContext context, Object objA, Object objB) { boolean same = false; int length = 0; try { Method lengthMethod = objA.getClass().getMethod("length", new Class[0]); Object valA = lengthMethod.invoke(objA, (Object []) null); Object valB = lengthMethod.invoke(objB, (Object []) null); if( valA.equals(valB) ) { same = true; length = (Integer) valA; } else { return false; } } catch( Exception e ) { same = false; Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } try { Method getMethod = objA.getClass().getMethod("get", new Class[] { int.class } ); for( int i = 0; i < length && same; ++i ) { Object subObjA = getMethod.invoke(objA, i); Object subObjB = getMethod.invoke(objB, i); if( subObjA == null && subObjB == null ) { continue; } DebugContext subContext = context.clone(); subContext.level = context.level + 1; subContext.name = context.name + ": (" + i + ") "; same = compareInstances(subContext, subObjA, subObjB); } } catch (Exception e) { same = false; Assert.fail(e.getClass().getSimpleName() + ": " + e.getMessage() ); } return same; } /** * Collection based objects (Array based or Set based), should basically always implement * one of two methods:<ul> * <li>.toArray()</li> * <li>.entrySet()</li> * </ul> * <p/> * This method retrieves the appropriate method for the given object (so that we can later retrieve * the list/array/set that this Object is based on). */ private static Method [] getMethodToRetrieveCollection(Class<?> objClass) { Method [] methods = new Method[2]; do { Method [] objMethods = objClass.getDeclaredMethods(); for( int m = 0; m < objMethods.length; ++m ) { if(objMethods[m].getName().equals("toArray") && objMethods[m].getParameterTypes().length == 0 && objMethods[m].getReturnType().equals(OBJECT_ARRAY_CLASS) ) { methods[TO_ARRAY] = objMethods[m]; methods[TO_ARRAY].setAccessible(true); break; } else if(objMethods[m].getName().equals("entrySet") && objMethods[m].getParameterTypes().length == 0 && objMethods[m].getReturnType().equals(Set.class) ) { methods[ENTRY_SET] = objMethods[m]; methods[ENTRY_SET].setAccessible(true); break; } } objClass = objClass.getSuperclass(); } while( objClass != null && methods[TO_ARRAY] == null && methods[ENTRY_SET] == null ); return methods; } private static boolean compareArrayBasedObjects(DebugContext context, Method toArrayMethod, Object objA, Object objB) throws Exception { boolean same = true; Object [] arrayA = (Object []) toArrayMethod.invoke(objA, (Object []) null); Object [] arrayB = (Object []) toArrayMethod.invoke(objB, (Object []) null); // Simple checks if( arrayA == null && arrayB == null ) { return true; } else if( arrayA == null || arrayB == null ) { return false; } else { if( arrayA.length != arrayB.length ) { return false; } if( arrayA.length == 0 ) { return true; } // Check whether order matters Class<?> superClass = objA.getClass().getSuperclass(); boolean isSet = false; while( superClass != null ) { if( superClass.equals(AbstractSet.class) ) { isSet = true; } superClass = superClass.getSuperclass(); } // Check content for( int a = 0; same && a < arrayA.length; ++a ) { Object subObjA = arrayA[a]; if( ! isSet ) { // order matters, compare element a from both arrays Object subObjB = arrayB[a]; DebugContext subContext = context.clone(); subContext.level = context.level + 1; subContext.name = context.name + ": " + "<elem> "; same = compareInstances(subContext, subObjA, subObjB); } else { // order doesn't matter, check if a matching element exists in arrayB boolean elementIsSame = false; for( int b = 0; ! elementIsSame && b < arrayB.length; ++b ) { Object subObjB = arrayB[b]; DebugContext subContext = context.clone(); subContext.level = context.level + 1; subContext.name = context.name + ": " + "<elem> "; elementIsSame = compareInstances(subContext, subObjA, subObjB); } if( elementIsSame == false ) { same = false; } } } } return same; } private static boolean compareEntrySetBasedObjects(DebugContext context, Method entrySetMethod, Object objA, Object objB) throws Exception { boolean same = true; @SuppressWarnings("unchecked") Set<Map.Entry<?,?>> entrySetA = (Set<Map.Entry<?,?>>) entrySetMethod.invoke(objA, (Object []) null); @SuppressWarnings("unchecked") Set<Map.Entry<?,?>> entrySetB = (Set<Map.Entry<?,?>>) entrySetMethod.invoke(objB, (Object []) null); // Simple checks if( entrySetA == null && entrySetB == null ) { return true; } else if( entrySetA == null || entrySetB == null ) { return false; } if( entrySetA.size() != entrySetB.size() ) { return false; } if( entrySetA.size() == 0 ) { return true; } // Check content for( Entry<?, ?> entryA : entrySetA ) { boolean elementIsSame = false; Object keyA = entryA.getKey(); for( Entry<?, ?> entryB : entrySetB ) { Object keyB = entryB.getKey(); DebugContext entryContext = context.clone(); String name = context.name + ": "; entryContext.level = context.level + 1; entryContext.name = name + "<key> "; entryContext.print = false; if( compareInstances(entryContext, keyA, keyB) ) { logger.trace( entryContext.name ); entryContext.name = name + "<entry> "; entryContext.print = true; elementIsSame = compareInstances(entryContext, entryA.getValue(), entryB.getValue()); break; } } if( ! elementIsSame ) { // a matching element was not found in arrayB same = false; break; } } return same; } private static class DebugContext { public int level; public String name; public boolean print; public DebugContext(int level, String name, boolean print) { this.level = level; this.name = name; this.print = print; } public DebugContext clone() { DebugContext newDebugContext = new DebugContext(this.level, this.name, this.print); return newDebugContext; } } }