/*
* Copyright [2010] John DeRegnaucourt (jdereg@gmail.com)
* 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.
*
* Multiple changes for customizing the comparison behavior by Roman K.
*
*/
package org.dcm4chee.archive.conf.defaults.test;
import org.dcm4che3.conf.api.AttributeCoercions;
import org.dcm4che3.imageio.codec.CompressionRules;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Test two objects for equivalence with a 'deep' comparison. This will traverse
* the Object graph and perform either a field-by-field comparison on each
* object (if no .equals() method has been overridden from Object), or it
* will call the customized .equals() method if it exists. This method will
* allow object graphs loaded at different times (with different object ids)
* to be reliably compared. Object.equals() / Object.hashCode() rely on the
* object's identity, which would not consider two equivalent objects necessarily
* equals. This allows graphs containing instances of Classes that did not
* overide .equals() / .hashCode() to be compared. For example, testing for
* existence in a cache. Relying on an object's identity will not locate an
* equivalent object in a cache.<br/><br/>
* <p/>
* This method will handle cycles correctly, for example A->B->C->A. Suppose a and
* a' are two separate instances of A with the same values for all fields on
* A, B, and C. Then a.deepEquals(a') will return true. It uses cycle detection
* storing visited objects in a Set to prevent endless loops.
*/
public class DeepEquals {
public static final Map<Class, Boolean> _customEquals = new ConcurrentHashMap<Class, Boolean>();
private static final Map<Class, Boolean> _customHash = new ConcurrentHashMap<Class, Boolean>();
private static final Map<Class, Collection<Field>> _reflectedFields = new ConcurrentHashMap<Class, Collection<Field>>();
public static Map<Class<?>, CustomDeepEquals> customDeepEquals = new HashMap<>();
static {
customDeepEquals.put(CompressionRules.class, new CustomEquals.CompressionRulesDeepEquals());
customDeepEquals.put(AttributeCoercions.class, new CustomEquals.AttributeCoercionsDeepEquals());
}
public static void printOutInequality() {
System.out.println(lastClass);
System.out.println(lastDualKey);
// trace
System.out.println("'Path' in the object tree where inequality is located:");
for (DualKey dualKey : lastDualKey.getTrace()) {
System.out.println(dualKey.getFieldName());
}
}
public static class DualKey {
private final Object _key1;
private final Object _key2;
public String fieldName;
private DualKey parent;
private DualKey(Object k1, Object k2, DualKey parent) {
_key1 = k1;
_key2 = k2;
this.parent = parent;
}
public String getFieldName() {
return fieldName;
}
private DualKey(Object k1, Object k2, String string, DualKey parent) {
this(k1, k2, null);
fieldName = string;
this.parent = parent;
}
public Deque<DualKey> getTrace() {
Deque<DualKey> l = new ArrayDeque<>();
DualKey p = this;
while (p != null) {
l.push(p);
p = p.parent;
}
return l;
}
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (!(other instanceof DualKey)) {
return false;
}
DualKey that = (DualKey) other;
return _key1 == that._key1 && _key2 == that._key2;
}
public int hashCode() {
int h1 = _key1 != null ? _key1.hashCode() : 0;
int h2 = _key2 != null ? _key2.hashCode() : 0;
return h1 + h2;
}
@Override
public String toString() {
return "Field " + fieldName + ": \n " + (_key1 == null ? "null" : _key1.toString()) + " \n " + (_key2 == null ? "null" : _key2.toString()) + " \n ------- \n";
}
}
public interface CustomDeepEquals {
public boolean deepEquals(Object first, Object second);
}
public static DualKey lastDualKey;
public static String lastClass;
/**
* Compare two objects with a 'deep' comparison. This will traverse the
* Object graph and perform either a field-by-field comparison on each
* object (if no .equals() method has been overridden from Object), or it
* will call the customized .equals() method if it exists. This method will
* allow object graphs loaded at different times (with different object ids)
* to be reliably compared. Object.equals() / Object.hashCode() rely on the
* object's identity, which would not consider to equivalent objects necessarily
* equals. This allows graphs containing instances of Classes that did no
* overide .equals() / .hashCode() to be compared. For example, testing for
* existence in a cache. Relying on an objects identity will not locate an
* object in cache, yet relying on it being equivalent will.<br/><br/>
* <p/>
* This method will handle cycles correctly, for example A->B->C->A. Suppose a and
* a' are two separate instances of the A with the same values for all fields on
* A, B, and C. Then a.deepEquals(a') will return true. It uses cycle detection
* storing visited objects in a Set to prevent endless loops.
*
* @param a Object one to compare
* @param b Object two to compare
* @return true if a is equivalent to b, false otherwise. Equivalent means that
* all field values of both subgraphs are the same, either at the field level
* or via the respectively encountered overridden .equals() methods during
* traversal.
*/
public static boolean deepEquals(Object a, Object b) {
Set visited = new HashSet<DualKey>();
LinkedList<DualKey> stack = new LinkedList<DualKey>();
stack.addFirst(new DualKey(a, b, null));
while (!stack.isEmpty()) {
DualKey dualKey = stack.removeFirst();
lastDualKey = dualKey;
visited.add(dualKey);
if (dualKey._key1 == dualKey._key2) { // Same instance is always equal to itself.
continue;
}
if (dualKey._key1 == null || dualKey._key2 == null) {
// check if one is null and another is an empty array
if (dualKey._key1 == null) {
if (dualKey._key2.getClass().isArray() && ((Object[]) dualKey._key2).length == 0)
continue;
}
if (dualKey._key2 == null) {
if (dualKey._key1.getClass().isArray() && ((Object[]) dualKey._key1).length == 0)
continue;
}
// If either one is null, not equal (both can't be null, due to above comparison).
return false;
}
if (!dualKey._key1.getClass().equals(dualKey._key2.getClass())) { // Must be same class
return false;
}
// Handle all [] types. In order to be equal, the arrays must be the same
// length, be of the same type, be in the same order, and all elements within
// the array must be deeply equivalent.
if (dualKey._key1.getClass().isArray()) {
if (!compareArrays(dualKey, stack, visited)) {
return false;
}
continue;
}
// Special handle SortedSets because they are fast to compare because their
// elements must be in the same order to be equivalent Sets.
if (dualKey._key1 instanceof SortedSet) {
if (!compareOrderedCollection(dualKey, stack, visited)) {
return false;
}
continue;
}
// Handled unordered Sets. This is a slightly more expensive comparison because order cannot
// be assumed, a temporary Map must be created, however the comparison still runs in O(N) time.
if (dualKey._key1 instanceof Set) {
if (!compareUnorderedCollection(dualKey, stack, visited)) {
return false;
}
continue;
}
// Check any Collection that is not a Set. In these cases, element order
// matters, therefore this comparison is faster than using unordered comparison.
if (dualKey._key1 instanceof Collection) {
if (!compareOrderedCollection(dualKey, stack, visited)) {
return false;
}
continue;
}
// Compare two SortedMaps. This takes advantage of the fact that these
// Maps can be compared in O(N) time due to their ordering.
if (dualKey._key1 instanceof SortedMap) {
if (!compareSortedMap(dualKey, stack, visited)) {
return false;
}
continue;
}
// Compare two Unordered Maps. This is a slightly more expensive comparison because
// order cannot be assumed, therefore a temporary Map must be created, however the
// comparison still runs in O(N) time.
if (dualKey._key1 instanceof Map) {
if (!compareUnorderedMap(dualKey, stack, visited)) {
return false;
}
continue;
}
if (hasCustomEquals(dualKey._key1.getClass())) {
if (!dualKey._key1.equals(dualKey._key2)) {
return false;
}
continue;
}
lastClass = dualKey._key1.getClass().toString();
// check if we have a custom deepequals method for this class
CustomDeepEquals de = customDeepEquals.get(dualKey._key1.getClass());
if (de != null) {
if (!de.deepEquals(dualKey._key1, dualKey._key2))
return false;
} else {
Collection<Field> fields = getDeepDeclaredFields(dualKey._key1.getClass());
for (Field field : fields) {
try {
DualKey dk = new DualKey(field.get(dualKey._key1), field.get(dualKey._key2), field.getName(), dualKey);
if (!visited.contains(dk)) {
stack.addFirst(dk);
}
} catch (Exception ignored) {
}
}
}
}
return true;
}
/**
* Deeply compare to Arrays []. Both arrays must be of the same type, same length, and all
* elements within the arrays must be deeply equal in order to return true.
*
* @param dualKey arrays
* @param stack add items to compare to the Stack (Stack versus recursion)
* @param visited Set of objects already compared (prevents cycles)
* @return true if the two arrays are the same length and contain deeply equivalent items.
*/
private static boolean compareArrays(DualKey dualKey, LinkedList stack, Set visited) {
Object array1 = dualKey._key1;
Object array2 = dualKey._key2;
// Same instance check already performed...
int len = Array.getLength(array1);
if (len != Array.getLength(array2)) {
return false;
}
// try sorting
// if (len >0 ) {
// if (Array.get(array1, 0) instanceof Comparable)
// {
//
// Class<?> c = Array.get(array1, 0).getClass();
//
// if (ClassUtils.isPrimitiveOrWrapper(c))
// {
// /* Arrays.sort(array1);
// Arrays.sort((Object[]) array2);*/
//
// } else {
//
// Arrays.sort((Object[]) array1);
// Arrays.sort((Object[]) array2);
// }
//
// }
// }
for (int i = 0; i < len; i++) {
DualKey dk = new DualKey(Array.get(array1, i), Array.get(array2, i), Integer.toString(i), dualKey);
if (!visited.contains(dk)) { // push contents for further comparison
stack.addFirst(dk);
}
}
return true;
}
/**
* Deeply compare two Collections that must be same length and in same order.
*
* @param dualKey collections
* @param stack add items to compare to the Stack (Stack versus recursion)
* @param visited Set of objects already compared (prevents cycles)
* value of 'true' indicates that the Collections may be equal, and the sets
* items will be added to the Stack for further comparison.
*/
private static boolean compareOrderedCollection(DualKey dualKey, LinkedList stack, Set visited) {
Collection col1 = (Collection) dualKey._key1;
Collection col2 = (Collection) dualKey._key2;
// Same instance check already performed...
if (col1.size() != col2.size()) {
return false;
}
// try sorting
if (col1 instanceof List) {
if (!col1.isEmpty()) {
Object el = ((List) col1).get(0);
if (el instanceof Comparable) {
Collections.sort((List) col1);
Collections.sort((List) col1);
}
}
}
Iterator i1 = col1.iterator();
Iterator i2 = col2.iterator();
int i = 0;
while (i1.hasNext()) {
DualKey dk = new DualKey(i1.next(), i2.next(), Integer.toString(i), dualKey);
if (!visited.contains(dk)) { // push contents for further comparison
stack.addFirst(dk);
}
}
return true;
}
/**
* Deeply compare the two sets referenced by dualKey. This method attempts
* to quickly determine inequality by length, then if lengths match, it
* places one collection into a temporary Map by deepHashCode(), so that it
* can walk the other collection and look for each item in the map, which
* runs in O(N) time, rather than an O(N^2) lookup that would occur if each
* item from collection one was scanned for in collection two.
*
* @param dualKey Collections
* @param stack add items to compare to the Stack (Stack versus recursion)
* @param visited Set containing items that have already been compared,
* so as to prevent cycles.
* @return boolean false if the Collections are for certain not equals. A
* value of 'true' indicates that the Collections may be equal, and the sets
* items will be added to the Stack for further comparison.
*/
private static boolean compareUnorderedCollection(DualKey dualKey, LinkedList stack, Set visited) {
Collection col1 = (Collection) dualKey._key1;
Collection col2 = (Collection) dualKey._key2;
// Same instance check already performed...
if (col1.size() != col2.size()) {
return false;
}
Map fastLookup = new HashMap();
for (Object o : col2) {
fastLookup.put(deepHashCode(o), o);
}
for (Object o : col1) {
Object other = fastLookup.get(deepHashCode(o));
if (other == null) { // Item not even found in other Collection, no need to continue.
return false;
}
DualKey dk = new DualKey(o, other, dualKey);
if (!visited.contains(dk)) { // Place items on 'stack' for further comparison.
stack.addFirst(dk);
}
}
return true;
}
/**
* Deeply compare two SortedMap instances. This method walks the Maps in order,
* taking advantage of the fact that they Maps are SortedMaps.
*
* @param dualKey SortedMaps
* @param stack add items to compare to the Stack (Stack versus recursion)
* @param visited Set containing items that have already been compared, to prevent cycles.
* @return false if the Maps are for certain not equals. 'true' indicates that 'on the surface' the maps
* are equal, however, it will place the contents of the Maps on the stack for further comparisons.
*/
private static boolean compareSortedMap(DualKey dualKey, LinkedList stack, Set visited) {
SortedMap map1 = (SortedMap) dualKey._key1;
SortedMap map2 = (SortedMap) dualKey._key2;
// Same instance check already performed...
if (map1.size() != map2.size()) {
return false;
}
Iterator i1 = map1.entrySet().iterator();
Iterator i2 = map2.entrySet().iterator();
while (i1.hasNext()) {
Map.Entry entry1 = (Map.Entry) i1.next();
Map.Entry entry2 = (Map.Entry) i2.next();
// Must split the Key and Value so that Map.Entry's equals() method is not used.
DualKey dk = new DualKey(entry1.getKey(), entry2.getKey(), dualKey);
if (!visited.contains(dk)) { // Push Keys for further comparison
stack.addFirst(dk);
}
dk = new DualKey(entry1.getValue(), entry2.getValue(), dualKey);
if (!visited.contains(dk)) { // Push values for further comparison
stack.addFirst(dk);
}
}
return true;
}
/**
* Deeply compare two Map instances. After quick short-circuit tests, this method
* uses a temporary Map so that this method can run in O(N) time.
*
* @param dualKey Maps
* @param stack add items to compare to the Stack (Stack versus recursion)
* @param visited Set containing items that have already been compared, to prevent cycles.
* @return false if the Maps are for certain not equals. 'true' indicates that 'on the surface' the maps
* are equal, however, it will place the contents of the Maps on the stack for further comparisons.
*/
private static boolean compareUnorderedMap(DualKey dualKey, LinkedList stack, Set visited) {
Map map1 = (Map) dualKey._key1;
Map map2 = (Map) dualKey._key2;
// Same instance check already performed...
if (map1.size() != map2.size()) {
return false;
}
Map fastLookup = new HashMap();
for (Map.Entry entry : (Set<Map.Entry>) map2.entrySet()) {
fastLookup.put(entry.getKey(), entry);
}
for (Map.Entry entry : (Set<Map.Entry>) map1.entrySet()) {
Map.Entry other = (Map.Entry) fastLookup.get(entry.getKey());
if (other == null) {
return false;
}
DualKey dk = new DualKey(entry.getKey(), other.getKey(), entry.getKey().toString(), dualKey);
if (!visited.contains(dk)) { // Push keys for further comparison
stack.addFirst(dk);
}
dk = new DualKey(entry.getValue(), other.getValue(), entry.getKey().toString(), dualKey);
if (!visited.contains(dk)) { // Push values for further comparison
stack.addFirst(dk);
}
}
return true;
}
/**
* Determine if the passed in class has a non-Object.equals() method. This
* method caches its results in static ConcurrentHashMap to benefit
* execution performance.
*
* @param c Class to check.
* @return true, if the passed in Class has a .equals() method somewhere between
* itself and just below Object in it's inheritance.
*/
public static boolean hasCustomEquals(Class c) {
Class origClass = c;
if (_customEquals.containsKey(c)) {
return _customEquals.get(c);
}
while (!Object.class.equals(c)) {
try {
c.getDeclaredMethod("equals", Object.class);
_customEquals.put(origClass, true);
return true;
} catch (Exception ignored) {
}
c = c.getSuperclass();
}
_customEquals.put(origClass, false);
return false;
}
/**
* Get a deterministic hashCode (int) value for an Object, regardless of
* when it was created or where it was loaded into memory. The problem
* with java.lang.Object.hashCode() is that it essentially relies on
* memory location of an object (what identity it was assigned), whereas
* this method will produce the same hashCode for any object graph, regardless
* of how many times it is created.<br/><br/>
* <p/>
* This method will handle cycles correctly (A->B->C->A). In this case,
* Starting with object A, B, or C would yield the same hashCode. If an
* object encountered (root, suboject, etc.) has a hashCode() method on it
* (that is not Object.hashCode()), that hashCode() method will be called
* and it will stop traversal on that branch.
*
* @param obj Object who hashCode is desired.
* @return the 'deep' hashCode value for the passed in object.
*/
public static int deepHashCode(Object obj) {
Set visited = new HashSet();
LinkedList<Object> stack = new LinkedList<Object>();
stack.addFirst(obj);
int hash = 0;
while (!stack.isEmpty()) {
obj = stack.removeFirst();
if (obj == null || visited.contains(obj)) {
continue;
}
visited.add(obj);
if (obj.getClass().isArray()) {
int len = Array.getLength(obj);
for (int i = 0; i < len; i++) {
stack.addFirst(Array.get(obj, i));
}
continue;
}
if (obj instanceof Collection) {
stack.addAll(0, (Collection) obj);
continue;
}
if (obj instanceof Map) {
stack.addAll(0, ((Map) obj).keySet());
stack.addAll(0, ((Map) obj).values());
continue;
}
if (hasCustomHashCode(obj.getClass())) { // A real hashCode() method exists, call it.
hash += obj.hashCode();
continue;
}
Collection<Field> fields = getDeepDeclaredFields(obj.getClass());
for (Field field : fields) {
try {
stack.addFirst(field.get(obj));
} catch (Exception ignored) {
}
}
}
return hash;
}
/**
* Determine if the passed in class has a non-Object.hashCode() method. This
* method caches its results in static ConcurrentHashMap to benefit
* execution performance.
*
* @param c Class to check.
* @return true, if the passed in Class has a .hashCode() method somewhere between
* itself and just below Object in it's inheritance.
*/
public static boolean hasCustomHashCode(Class c) {
Class origClass = c;
if (_customHash.containsKey(c)) {
return _customHash.get(c);
}
while (!Object.class.equals(c)) {
try {
c.getDeclaredMethod("hashCode");
_customHash.put(origClass, true);
return true;
} catch (Exception ignored) {
}
c = c.getSuperclass();
}
_customHash.put(origClass, false);
return false;
}
/**
* Get all non static, non transient, fields of the passed in class.
* The special this$ field is also not returned. The result is cached
* in a static ConcurrentHashMap to benefit execution performance.
*
* @param c Class instance
* @return Collection of only the fields in the passed in class
* that would need further processing (reference fields). This
* makes field traversal on a class faster as it does not need to
* continually process known fields like primitives.
*/
public static Collection<Field> getDeepDeclaredFields(Class c) {
if (_reflectedFields.containsKey(c)) {
return _reflectedFields.get(c);
}
Collection<Field> fields = new ArrayList<Field>();
Class curr = c;
while (curr != null) {
try {
Field[] local = curr.getDeclaredFields();
for (Field field : local) {
if (!field.isAccessible()) {
try {
field.setAccessible(true);
} catch (Exception ignored) {
}
}
int modifiers = field.getModifiers();
if (!Modifier.isStatic(modifiers) &&
!field.getName().startsWith("this$") &&
!Modifier.isTransient(modifiers)) { // speed up: do not count static fields, not go back up to enclosing object in nested case
fields.add(field);
}
}
} catch (ThreadDeath t) {
throw t;
} catch (Throwable ignored) {
}
curr = curr.getSuperclass();
}
_reflectedFields.put(c, fields);
return fields;
}
}