/hudson-core/src/main/java/hudson/util/DeepEquals.java

http://github.com/hudson/hudson · Java · 638 lines · 397 code · 66 blank · 175 comment · 82 complexity · 6ea4f05c0f18b530d9163aa95f9c29e2 MD5 · raw file

  1. /**
  2. * Copyright 2011 John DeRegnaucourt (jdereg@gmail.com)
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package hudson.util;
  17. import java.lang.reflect.Array;
  18. import java.lang.reflect.Field;
  19. import java.lang.reflect.Modifier;
  20. import java.util.ArrayList;
  21. import java.util.Collection;
  22. import java.util.HashMap;
  23. import java.util.HashSet;
  24. import java.util.Iterator;
  25. import java.util.LinkedList;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.SortedMap;
  29. import java.util.SortedSet;
  30. import java.util.concurrent.ConcurrentHashMap;
  31. /**
  32. * Test two objects for equivalence with a 'deep' comparison. This will traverse
  33. * the Object graph and perform either a field-by-field comparison on each
  34. * object (if no .equals() method has been overridden from Object), or it
  35. * will call the customized .equals() method if it exists. This method will
  36. * allow object graphs loaded at different times (with different object ids)
  37. * to be reliably compared. Object.equals() / Object.hashCode() rely on the
  38. * object's identity, which would not consider two equivalent objects necessarily
  39. * equals. This allows graphs containing instances of Classes that did not
  40. * overide .equals() / .hashCode() to be compared. For example, testing for
  41. * existence in a cache. Relying on an object's identity will not locate an
  42. * equivalent object in a cache.<br/><br/>
  43. *
  44. * This method will handle cycles correctly, for example A->B->C->A. Suppose a and
  45. * a' are two separate instances of A with the same values for all fields on
  46. * A, B, and C. Then a.deepEquals(a') will return true. It uses cycle detection
  47. * storing visited objects in a Set to prevent endless loops.
  48. *
  49. * @author John DeRegnaucourt (jdereg@gmail.com)
  50. */
  51. public class DeepEquals
  52. {
  53. private static final Map<Class, Boolean> _customEquals = new ConcurrentHashMap<Class, Boolean>();
  54. private static final Map<Class, Boolean> _customHash = new ConcurrentHashMap<Class, Boolean>();
  55. private static final Map<Class, Collection<Field>> _reflectedFields = new ConcurrentHashMap<Class, Collection<Field>>();
  56. private static class DualKey
  57. {
  58. private final Object _key1;
  59. private final Object _key2;
  60. private DualKey(Object k1, Object k2)
  61. {
  62. _key1 = k1;
  63. _key2 = k2;
  64. }
  65. public boolean equals(Object other)
  66. {
  67. if (other == null)
  68. {
  69. return false;
  70. }
  71. if (!(other instanceof DualKey))
  72. {
  73. return false;
  74. }
  75. DualKey that = (DualKey) other;
  76. return _key1 == that._key1 && _key2 == that._key2;
  77. }
  78. public int hashCode()
  79. {
  80. int h1 = _key1 != null ? _key1.hashCode() : 0;
  81. int h2 = _key2 != null ? _key2.hashCode() : 0;
  82. return h1 + h2;
  83. }
  84. }
  85. /**
  86. * Compare two objects with a 'deep' comparison. This will traverse the
  87. * Object graph and perform either a field-by-field comparison on each
  88. * object (if no .equals() method has been overridden from Object), or it
  89. * will call the customized .equals() method if it exists. This method will
  90. * allow object graphs loaded at different times (with different object ids)
  91. * to be reliably compared. Object.equals() / Object.hashCode() rely on the
  92. * object's identity, which would not consider to equivalent objects necessarily
  93. * equals. This allows graphs containing instances of Classes that did no
  94. * overide .equals() / .hashCode() to be compared. For example, testing for
  95. * existence in a cache. Relying on an objects identity will not locate an
  96. * object in cache, yet relying on it being equivalent will.<br/><br/>
  97. *
  98. * This method will handle cycles correctly, for example A->B->C->A. Suppose a and
  99. * a' are two separate instances of the A with the same values for all fields on
  100. * A, B, and C. Then a.deepEquals(a') will return true. It uses cycle detection
  101. * storing visited objects in a Set to prevent endless loops.
  102. * @param a Object one to compare
  103. * @param b Object two to compare
  104. * @return true if a is equivalent to b, false otherwise. Equivalent means that
  105. * all field values of both subgraphs are the same, either at the field level
  106. * or via the respectively encountered overridden .equals() methods during
  107. * traversal.
  108. */
  109. public static boolean deepEquals(Object a, Object b)
  110. {
  111. Set visited = new HashSet<DualKey>();
  112. LinkedList<DualKey> stack = new LinkedList<DualKey>();
  113. stack.addFirst(new DualKey(a, b));
  114. while (!stack.isEmpty())
  115. {
  116. DualKey dualKey = stack.removeFirst();
  117. visited.add(dualKey);
  118. if (dualKey._key1 == dualKey._key2)
  119. { // Same instance is always equal to itself.
  120. continue;
  121. }
  122. if (dualKey._key1 == null || dualKey._key2 == null)
  123. { // If either one is null, not equal (both can't be null, due to above comparison).
  124. return false;
  125. }
  126. if (!dualKey._key1.getClass().equals(dualKey._key2.getClass()))
  127. { // Must be same class
  128. return false;
  129. }
  130. // Handle all [] types. In order to be equal, the arrays must be the same
  131. // length, be of the same type, be in the same order, and all elements within
  132. // the array must be deeply equivalent.
  133. if (dualKey._key1.getClass().isArray())
  134. {
  135. if (!compareArrays(dualKey._key1, dualKey._key2, stack, visited))
  136. {
  137. return false;
  138. }
  139. continue;
  140. }
  141. // Special handle SortedSets because they are fast to compare because their
  142. // elements must be in the same order to be equivalent Sets.
  143. if (dualKey._key1 instanceof SortedSet)
  144. {
  145. if (!compareOrderedCollection((Collection) dualKey._key1, (Collection) dualKey._key2, stack, visited))
  146. {
  147. return false;
  148. }
  149. continue;
  150. }
  151. // Handled unordered Sets. This is a slightly more expensive comparison because order cannot
  152. // be assumed, a temporary Map must be created, however the comparison still runs in O(N) time.
  153. if (dualKey._key1 instanceof Set)
  154. {
  155. if (!compareUnorderedCollection((Collection) dualKey._key1, (Collection) dualKey._key2, stack, visited))
  156. {
  157. return false;
  158. }
  159. continue;
  160. }
  161. // Check any Collection that is not a Set. In these cases, element order
  162. // matters, therefore this comparison is faster than using unordered comparison.
  163. if (dualKey._key1 instanceof Collection)
  164. {
  165. if (!compareOrderedCollection((Collection) dualKey._key1, (Collection) dualKey._key2, stack, visited))
  166. {
  167. return false;
  168. }
  169. continue;
  170. }
  171. // Compare two SortedMaps. This takes advantage of the fact that these
  172. // Maps can be compared in O(N) time due to their ordering.
  173. if (dualKey._key1 instanceof SortedMap)
  174. {
  175. if (!compareSortedMap((SortedMap) dualKey._key1, (SortedMap) dualKey._key2, stack, visited))
  176. {
  177. return false;
  178. }
  179. continue;
  180. }
  181. // Compare two Unordered Maps. This is a slightly more expensive comparison because
  182. // order cannot be assumed, therefore a temporary Map must be created, however the
  183. // comparison still runs in O(N) time.
  184. if (dualKey._key1 instanceof Map)
  185. {
  186. if (!compareUnorderedMap((Map) dualKey._key1, (Map) dualKey._key2, stack, visited))
  187. {
  188. return false;
  189. }
  190. continue;
  191. }
  192. if (hasCustomEquals(dualKey._key1.getClass()))
  193. {
  194. if (!dualKey._key1.equals(dualKey._key2))
  195. {
  196. return false;
  197. }
  198. continue;
  199. }
  200. Collection<Field> fields = getDeepDeclaredFields(dualKey._key1.getClass());
  201. for (Field field : fields)
  202. {
  203. try
  204. {
  205. DualKey dk = new DualKey(field.get(dualKey._key1), field.get(dualKey._key2));
  206. if (!visited.contains(dk))
  207. {
  208. stack.addFirst(dk);
  209. }
  210. }
  211. catch (Exception ignored)
  212. { }
  213. }
  214. }
  215. return true;
  216. }
  217. /**
  218. * Deeply compare to Arrays []. Both arrays must be of the same type, same length, and all
  219. * elements within the arrays must be deeply equal in order to return true.
  220. * @param array1 [] type (Object[], String[], etc.)
  221. * @param array2 [] type (Object[], String[], etc.)
  222. * @param stack add items to compare to the Stack (Stack versus recursion)
  223. * @param visited Set of objects already compared (prevents cycles)
  224. * @return true if the two arrays are the same length and contain deeply equivalent items.
  225. */
  226. private static boolean compareArrays(Object array1, Object array2, LinkedList stack, Set visited)
  227. {
  228. // Same instance check already performed...
  229. int len = Array.getLength(array1);
  230. if (len != Array.getLength(array2))
  231. {
  232. return false;
  233. }
  234. for (int i = 0; i < len; i++)
  235. {
  236. DualKey dk = new DualKey(Array.get(array1, i), Array.get(array2, i));
  237. if (!visited.contains(dk))
  238. { // push contents for further comparison
  239. stack.addFirst(dk);
  240. }
  241. }
  242. return true;
  243. }
  244. /**
  245. * Deeply compare two Collections that must be same length and in same order.
  246. * @param col1 First collection of items to compare
  247. * @param col2 Second collection of items to compare
  248. * @param stack add items to compare to the Stack (Stack versus recursion)
  249. * @param visited Set of objects already compared (prevents cycles)
  250. * value of 'true' indicates that the Collections may be equal, and the sets
  251. * items will be added to the Stack for further comparison.
  252. */
  253. private static boolean compareOrderedCollection(Collection col1, Collection col2, LinkedList stack, Set visited)
  254. {
  255. // Same instance check already performed...
  256. if (col1.size() != col2.size())
  257. {
  258. return false;
  259. }
  260. Iterator i1 = col1.iterator();
  261. Iterator i2 = col2.iterator();
  262. while (i1.hasNext())
  263. {
  264. DualKey dk = new DualKey(i1.next(), i2.next());
  265. if (!visited.contains(dk))
  266. { // push contents for further comparison
  267. stack.addFirst(dk);
  268. }
  269. }
  270. return true;
  271. }
  272. /**
  273. * Deeply compare the two sets referenced by dualKey. This method attempts
  274. * to quickly determine inequality by length, then if lengths match, it
  275. * places one collection into a temporary Map by deepHashCode(), so that it
  276. * can walk the other collection and look for each item in the map, which
  277. * runs in O(N) time, rather than an O(N^2) lookup that would occur if each
  278. * item from collection one was scanned for in collection two.
  279. * @param col1 First collection of items to compare
  280. * @param col2 Second collection of items to compare
  281. * @param stack add items to compare to the Stack (Stack versus recursion)
  282. * @param visited Set containing items that have already been compared,
  283. * so as to prevent cycles.
  284. * @return boolean false if the Collections are for certain not equals. A
  285. * value of 'true' indicates that the Collections may be equal, and the sets
  286. * items will be added to the Stack for further comparison.
  287. */
  288. private static boolean compareUnorderedCollection(Collection col1, Collection col2, LinkedList stack, Set visited)
  289. {
  290. // Same instance check already performed...
  291. if (col1.size() != col2.size())
  292. {
  293. return false;
  294. }
  295. Map fastLookup = new HashMap();
  296. for (Object o : col2)
  297. {
  298. fastLookup.put(deepHashCode(o), o);
  299. }
  300. for (Object o : col1)
  301. {
  302. Object other = fastLookup.get(deepHashCode(o));
  303. if (other == null)
  304. { // Item not even found in other Collection, no need to continue.
  305. return false;
  306. }
  307. DualKey dk = new DualKey(o, other);
  308. if (!visited.contains(dk))
  309. { // Place items on 'stack' for further comparison.
  310. stack.addFirst(dk);
  311. }
  312. }
  313. return true;
  314. }
  315. /**
  316. * Deeply compare two SortedMap instances. This method walks the Maps in order,
  317. * taking advantage of the fact that they Maps are SortedMaps.
  318. * @param map1 SortedMap one
  319. * @param map2 SortedMap two
  320. * @param stack add items to compare to the Stack (Stack versus recursion)
  321. * @param visited Set containing items that have already been compared, to prevent cycles.
  322. * @return false if the Maps are for certain not equals. 'true' indicates that 'on the surface' the maps
  323. * are equal, however, it will place the contents of the Maps on the stack for further comparisons.
  324. */
  325. private static boolean compareSortedMap(SortedMap map1, SortedMap map2, LinkedList stack, Set visited)
  326. {
  327. // Same instance check already performed...
  328. if (map1.size() != map2.size())
  329. {
  330. return false;
  331. }
  332. Iterator i1 = map1.entrySet().iterator();
  333. Iterator i2 = map2.entrySet().iterator();
  334. while (i1.hasNext())
  335. {
  336. Map.Entry entry1 = (Map.Entry)i1.next();
  337. Map.Entry entry2 = (Map.Entry)i2.next();
  338. // Must split the Key and Value so that Map.Entry's equals() method is not used.
  339. DualKey dk = new DualKey(entry1.getKey(), entry2.getKey());
  340. if (!visited.contains(dk))
  341. { // Push Keys for further comparison
  342. stack.addFirst(dk);
  343. }
  344. dk = new DualKey(entry1.getValue(), entry2.getValue());
  345. if (!visited.contains(dk))
  346. { // Push values for further comparison
  347. stack.addFirst(dk);
  348. }
  349. }
  350. return true;
  351. }
  352. /**
  353. * Deeply compare two Map instances. After quick short-circuit tests, this method
  354. * uses a temporary Map so that this method can run in O(N) time.
  355. * @param map1 Map one
  356. * @param map2 Map two
  357. * @param stack add items to compare to the Stack (Stack versus recursion)
  358. * @param visited Set containing items that have already been compared, to prevent cycles.
  359. * @return false if the Maps are for certain not equals. 'true' indicates that 'on the surface' the maps
  360. * are equal, however, it will place the contents of the Maps on the stack for further comparisons.
  361. */
  362. private static boolean compareUnorderedMap(Map map1, Map map2, LinkedList stack, Set visited)
  363. {
  364. // Same instance check already performed...
  365. if (map1.size() != map2.size())
  366. {
  367. return false;
  368. }
  369. Map fastLookup = new HashMap();
  370. for (Map.Entry entry : (Set<Map.Entry>)map2.entrySet())
  371. {
  372. fastLookup.put(deepHashCode(entry.getKey()), entry);
  373. }
  374. for (Map.Entry entry : (Set<Map.Entry>)map1.entrySet())
  375. {
  376. Map.Entry other = (Map.Entry)fastLookup.get(deepHashCode(entry.getKey()));
  377. if (other == null)
  378. {
  379. return false;
  380. }
  381. DualKey dk = new DualKey(entry.getKey(), other.getKey());
  382. if (!visited.contains(dk))
  383. { // Push keys for further comparison
  384. stack.addFirst(dk);
  385. }
  386. dk = new DualKey(entry.getValue(), other.getValue());
  387. if (!visited.contains(dk))
  388. { // Push values for further comparison
  389. stack.addFirst(dk);
  390. }
  391. }
  392. return true;
  393. }
  394. /**
  395. * Determine if the passed in class has a non-Object.equals() method. This
  396. * method caches its results in static ConcurrentHashMap to benefit
  397. * execution performance.
  398. * @param c Class to check.
  399. * @return true, if the passed in Class has a .equals() method somewhere between
  400. * itself and just below Object in it's inheritance.
  401. */
  402. public static boolean hasCustomEquals(Class c)
  403. {
  404. Class origClass = c;
  405. if (_customEquals.containsKey(c))
  406. {
  407. return _customEquals.get(c);
  408. }
  409. while (!Object.class.equals(c))
  410. {
  411. try
  412. {
  413. c.getDeclaredMethod("equals", Object.class);
  414. _customEquals.put(origClass, true);
  415. return true;
  416. }
  417. catch (Exception ignored) { }
  418. c = c.getSuperclass();
  419. }
  420. _customEquals.put(origClass, false);
  421. return false;
  422. }
  423. /**
  424. * Get a deterministic hashCode (int) value for an Object, regardless of
  425. * when it was created or where it was loaded into memory. The problem
  426. * with java.lang.Object.hashCode() is that it essentially relies on
  427. * memory location of an object (what identity it was assigned), whereas
  428. * this method will produce the same hashCode for any object graph, regardless
  429. * of how many times it is created.<br/><br/>
  430. *
  431. * This method will handle cycles correctly (A->B->C->A). In this case,
  432. * Starting with object A, B, or C would yield the same hashCode. If an
  433. * object encountered (root, suboject, etc.) has a hashCode() method on it
  434. * (that is not Object.hashCode()), that hashCode() method will be called
  435. * and it will stop traversal on that branch.
  436. * @param obj Object who hashCode is desired.
  437. * @return the 'deep' hashCode value for the passed in object.
  438. */
  439. public static int deepHashCode(Object obj)
  440. {
  441. Set visited = new HashSet();
  442. LinkedList<Object> stack = new LinkedList<Object>();
  443. stack.addFirst(obj);
  444. int hash = 0;
  445. while (!stack.isEmpty())
  446. {
  447. obj = stack.removeFirst();
  448. if (obj == null || visited.contains(obj))
  449. {
  450. continue;
  451. }
  452. visited.add(obj);
  453. if (obj.getClass().isArray())
  454. {
  455. int len = Array.getLength(obj);
  456. for (int i = 0; i < len; i++)
  457. {
  458. stack.addFirst(Array.get(obj, i));
  459. }
  460. continue;
  461. }
  462. if (obj instanceof Collection)
  463. {
  464. stack.addAll(0, (Collection)obj);
  465. continue;
  466. }
  467. if (obj instanceof Map)
  468. {
  469. stack.addAll(0, ((Map)obj).keySet());
  470. stack.addAll(0, ((Map)obj).values());
  471. continue;
  472. }
  473. if (hasCustomHashCode(obj.getClass()))
  474. { // A real hashCode() method exists, call it.
  475. hash += obj.hashCode();
  476. continue;
  477. }
  478. Collection<Field> fields = getDeepDeclaredFields(obj.getClass());
  479. for (Field field : fields)
  480. {
  481. try
  482. {
  483. stack.addFirst(field.get(obj));
  484. }
  485. catch (Exception ignored) { }
  486. }
  487. }
  488. return hash;
  489. }
  490. /**
  491. * Determine if the passed in class has a non-Object.hashCode() method. This
  492. * method caches its results in static ConcurrentHashMap to benefit
  493. * execution performance.
  494. * @param c Class to check.
  495. * @return true, if the passed in Class has a .hashCode() method somewhere between
  496. * itself and just below Object in it's inheritance.
  497. */
  498. public static boolean hasCustomHashCode(Class c)
  499. {
  500. Class origClass = c;
  501. if (_customHash.containsKey(c))
  502. {
  503. return _customHash.get(c);
  504. }
  505. while (!Object.class.equals(c))
  506. {
  507. try
  508. {
  509. c.getDeclaredMethod("hashCode");
  510. _customHash.put(origClass, true);
  511. return true;
  512. }
  513. catch (Exception ignored) { }
  514. c = c.getSuperclass();
  515. }
  516. _customHash.put(origClass, false);
  517. return false;
  518. }
  519. /**
  520. * Get all non static, non transient, fields of the passed in class.
  521. * The special this$ field is also not returned. The result is cached
  522. * in a static ConcurrentHashMap to benefit execution performance.
  523. * @param c Class instance
  524. * @return Collection of only the fields in the passed in class
  525. * that would need further processing (reference fields). This
  526. * makes field traversal on a class faster as it does not need to
  527. * continually process known fields like primitives.
  528. */
  529. public static Collection<Field> getDeepDeclaredFields(Class c)
  530. {
  531. if (_reflectedFields.containsKey(c))
  532. {
  533. return _reflectedFields.get(c);
  534. }
  535. Collection<Field> fields = new ArrayList<Field>();
  536. Class curr = c;
  537. while (curr != null)
  538. {
  539. try
  540. {
  541. Field[] local = curr.getDeclaredFields();
  542. for (Field field : local)
  543. {
  544. if (!field.isAccessible())
  545. {
  546. try
  547. {
  548. field.setAccessible(true);
  549. }
  550. catch (Exception ignored) { }
  551. }
  552. int modifiers = field.getModifiers();
  553. if (!Modifier.isStatic(modifiers) &&
  554. !field.getName().startsWith("this$") &&
  555. !Modifier.isTransient(modifiers))
  556. { // speed up: do not count static fields, not go back up to enclosing object in nested case
  557. fields.add(field);
  558. }
  559. }
  560. }
  561. catch (ThreadDeath t)
  562. {
  563. throw t;
  564. }
  565. catch (Throwable ignored)
  566. { }
  567. curr = curr.getSuperclass();
  568. }
  569. _reflectedFields.put(c, fields);
  570. return fields;
  571. }
  572. }