PageRenderTime 67ms CodeModel.GetById 19ms app.highlight 41ms RepoModel.GetById 1ms app.codeStats 1ms

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