| author | akozak | 2011-11-25 03:30:18 (EST) |
|---|---|---|
| committer | Winston Prakash | 2011-12-01 20:47:35 (EST) |
| commit | 42bd7dabdc7f08cc0e9230168157284c0e7cb1d5 (patch) (side-by-side diff) | |
| tree | caeb2dd7afc3084e3610a3e43805dd0ab55447f5 | |
| parent | 1be6375134f8dca687e2f655dce99356c2d9ba5b (diff) | |
| download | org.eclipse.hudson.core-42bd7dabdc7f08cc0e9230168157284c0e7cb1d5.zip org.eclipse.hudson.core-42bd7dabdc7f08cc0e9230168157284c0e7cb1d5.tar.gz org.eclipse.hudson.core-42bd7dabdc7f08cc0e9230168157284c0e7cb1d5.tar.bz2 | |
Used DeepEquals for the properties comparison. It resolves the issue in some tier2 plugins with properties highlighting as overridden if no changes was made. Improved onProjectPropertyChanged.
Signed-off-by: Winston Prakash <winston.prakash@gmail.com>
5 files changed, 790 insertions, 7 deletions
diff --git a/hudson-core/src/main/java/hudson/model/AbstractProject.java b/hudson-core/src/main/java/hudson/model/AbstractProject.java index 39b9253..13f8163 100644 --- a/hudson-core/src/main/java/hudson/model/AbstractProject.java +++ b/hudson-core/src/main/java/hudson/model/AbstractProject.java @@ -93,7 +93,6 @@ import net.sf.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.eclipse.hudson.api.model.IAbstractProject; -import org.eclipse.hudson.api.model.project.property.SCMProjectProperty; import org.eclipse.hudson.api.model.project.property.TriggerProjectProperty; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; diff --git a/hudson-core/src/main/java/hudson/util/DeepEquals.java b/hudson-core/src/main/java/hudson/util/DeepEquals.java new file mode 100644 index 0000000..a67330e --- a/dev/null +++ b/hudson-core/src/main/java/hudson/util/DeepEquals.java @@ -0,0 +1,638 @@ +/** + * Copyright 2011 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. + */ +package hudson.util; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +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/> + * + * 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. + * + * @author John DeRegnaucourt (jdereg@gmail.com) + */ +public class DeepEquals +{ + private 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>>(); + + private static class DualKey + { + private final Object _key1; + private final Object _key2; + + private DualKey(Object k1, Object k2) + { + _key1 = k1; + _key2 = k2; + } + + 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; + } + } + + /** + * 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/> + * + * 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)); + + while (!stack.isEmpty()) + { + DualKey dualKey = stack.removeFirst(); + visited.add(dualKey); + + if (dualKey._key1 == dualKey._key2) + { // Same instance is always equal to itself. + continue; + } + + if (dualKey._key1 == null || dualKey._key2 == null) + { // 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._key1, dualKey._key2, 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((Collection) dualKey._key1, (Collection) dualKey._key2, 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((Collection) dualKey._key1, (Collection) dualKey._key2, 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((Collection) dualKey._key1, (Collection) dualKey._key2, 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((SortedMap) dualKey._key1, (SortedMap) dualKey._key2, 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((Map) dualKey._key1, (Map) dualKey._key2, stack, visited)) + { + return false; + } + continue; + } + + if (hasCustomEquals(dualKey._key1.getClass())) + { + if (!dualKey._key1.equals(dualKey._key2)) + { + return false; + } + continue; + } + + Collection<Field> fields = getDeepDeclaredFields(dualKey._key1.getClass()); + + for (Field field : fields) + { + try + { + DualKey dk = new DualKey(field.get(dualKey._key1), field.get(dualKey._key2)); + 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 array1 [] type (Object[], String[], etc.) + * @param array2 [] type (Object[], String[], etc.) + * @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(Object array1, Object array2, LinkedList stack, Set visited) + { + // Same instance check already performed... + + int len = Array.getLength(array1); + if (len != Array.getLength(array2)) + { + return false; + } + + for (int i = 0; i < len; i++) + { + DualKey dk = new DualKey(Array.get(array1, i), Array.get(array2, i)); + 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 col1 First collection of items to compare + * @param col2 Second collection of items to compare + * @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(Collection col1, Collection col2, LinkedList stack, Set visited) + { + // Same instance check already performed... + + if (col1.size() != col2.size()) + { + return false; + } + + Iterator i1 = col1.iterator(); + Iterator i2 = col2.iterator(); + + while (i1.hasNext()) + { + DualKey dk = new DualKey(i1.next(), i2.next()); + 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 col1 First collection of items to compare + * @param col2 Second collection of items to compare + * @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(Collection col1, Collection col2, LinkedList stack, Set visited) + { + // 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); + 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 map1 SortedMap one + * @param map2 SortedMap two + * @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(SortedMap map1, SortedMap map2, LinkedList stack, Set visited) + { + // 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()); + if (!visited.contains(dk)) + { // Push Keys for further comparison + stack.addFirst(dk); + } + + dk = new DualKey(entry1.getValue(), entry2.getValue()); + 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 map1 Map one + * @param map2 Map two + * @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(Map map1, Map map2, LinkedList stack, Set visited) + { + // 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(deepHashCode(entry.getKey()), entry); + } + + for (Map.Entry entry : (Set<Map.Entry>)map1.entrySet()) + { + Map.Entry other = (Map.Entry)fastLookup.get(deepHashCode(entry.getKey())); + if (other == null) + { + return false; + } + + DualKey dk = new DualKey(entry.getKey(), other.getKey()); + if (!visited.contains(dk)) + { // Push keys for further comparison + stack.addFirst(dk); + } + + dk = new DualKey(entry.getValue(), other.getValue()); + 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/> + * + * 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; + } +} diff --git a/hudson-core/src/main/java/org/eclipse/hudson/api/model/project/property/BaseProjectProperty.java b/hudson-core/src/main/java/org/eclipse/hudson/api/model/project/property/BaseProjectProperty.java index 3049460..f983b83 100644 --- a/hudson-core/src/main/java/org/eclipse/hudson/api/model/project/property/BaseProjectProperty.java +++ b/hudson-core/src/main/java/org/eclipse/hudson/api/model/project/property/BaseProjectProperty.java @@ -15,8 +15,8 @@ package org.eclipse.hudson.api.model.project.property; +import hudson.util.DeepEquals; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.builder.EqualsBuilder; import org.eclipse.hudson.api.model.IJob; import org.eclipse.hudson.api.model.IProjectProperty; @@ -198,7 +198,7 @@ public class BaseProjectProperty<T> implements IProjectProperty<T> { */ public boolean allowOverrideValue(T cascadingValue, T candidateValue) { return ObjectUtils.notEqual(cascadingValue, candidateValue) - && !EqualsBuilder.reflectionEquals(cascadingValue, candidateValue, true); + && !DeepEquals.deepEquals(cascadingValue, candidateValue); } /** diff --git a/hudson-core/src/test/java/hudson/util/DeepEqualsTest.java b/hudson-core/src/test/java/hudson/util/DeepEqualsTest.java new file mode 100644 index 0000000..c8cf0a8 --- a/dev/null +++ b/hudson-core/src/test/java/hudson/util/DeepEqualsTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * + * Copyright (c) 2011 Oracle Corporation. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * + * Anton Kozak + * + * + *******************************************************************************/ +package hudson.util; + +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.assertFalse; + + +/** + * Test for {@link DeepEquals#deepEquals(Object, Object)} + */ +public class DeepEqualsTest { + + private static class Person { + private String name; + private Pet pet; + + private Person(String name, Pet pet) { + this.name = name; + this.pet = pet; + } + } + + private static class Pet { + private String name; + + private Pet(String name) { + this.name = name; + } + } + + /** + * Tests simple deepEquals functionality. + */ + @Test + public void testDeepEqualsSimple() { + + Pet pet1 = new Pet("petName"); + Pet pet2 = new Pet("petName"); + Pet pet3 = new Pet("petName1"); + + Person person1 = new Person("name", pet1); + Person person2 = new Person("name", pet2); + Person person3 = new Person("name", pet3); + Person person4 = new Person("name1", pet1); + + assertTrue(DeepEquals.deepEquals(null, null)); + assertFalse(DeepEquals.deepEquals(pet1, null)); + assertFalse(DeepEquals.deepEquals(null, pet1)); + + assertTrue(DeepEquals.deepEquals(pet1, pet1)); + assertTrue(DeepEquals.deepEquals(pet1, pet2)); + assertFalse(DeepEquals.deepEquals(pet1, pet3)); + + assertTrue(DeepEquals.deepEquals(person1, person1)); + assertTrue(DeepEquals.deepEquals(person1, person2)); + assertFalse(DeepEquals.deepEquals(person1, person3)); + assertFalse(DeepEquals.deepEquals(person1, person4)); + } + + private static class Node { + private Object value; + private Node next; + + private Node(Object value) { + this(value, null); + } + + private Node(Object value, Node next) { + this.value = value; + this.next = next; + } + + public void setNext(Node next) { + this.next = next; + } + } + + /** + * Tests deepEquals for deeply nested objects. + */ + @Test + public void testDeepEquals() { + Node n0 = new Node(1, new Node(2)); + Node n1 = new Node(1, new Node(2, new Node(3))); + Node n2 = new Node(1, new Node(2, new Node(3))); + Node n3 = new Node(1, new Node(2, new Node(4))); + Node n4 = new Node(1, new Node(2, new Node(4))); + Node n5 = new Node(1, new Node(2, new Node(4, new Node(5)))); + + assertTrue(DeepEquals.deepEquals(n1, n1)); + assertTrue(DeepEquals.deepEquals(n1, n2)); + assertTrue(DeepEquals.deepEquals(n3, n4)); + + assertFalse(DeepEquals.deepEquals(n0, n1)); + assertFalse(DeepEquals.deepEquals(n0, n3)); + assertFalse(DeepEquals.deepEquals(n2, n4)); + assertFalse(DeepEquals.deepEquals(n4, n5)); + } + + /** + * Tests whether recursive objects process correctly. + */ + @Test + public void testDeepEqualsRecursively() { + Node n0 = new Node(1); + Node n1 = new Node(1, n0); + n0.setNext(n1); + assertTrue(DeepEquals.deepEquals(n0, n1)); + } + + /** + * Tests deepEquals for lists. + */ + @Test + public void testDeepEqualsLists() { + List<Node> list1 = Arrays.asList(new Node(1, new Node(3)), new Node(2, new Node(4))); + List<Node> list2 = Arrays.asList(new Node(1, new Node(3)), new Node(2, new Node(4))); + List<Node> list3 = Arrays.asList(new Node(1, new Node(3)), new Node(2, new Node(3))); + assertTrue(DeepEquals.deepEquals(list1, list2)); + assertFalse(DeepEquals.deepEquals(list1, list3)); + } +} diff --git a/hudson-war/src/main/webapp/scripts/cascading.js b/hudson-war/src/main/webapp/scripts/cascading.js index f07108c..6857ef9 100644 --- a/hudson-war/src/main/webapp/scripts/cascading.js +++ b/hudson-war/src/main/webapp/scripts/cascading.js @@ -52,22 +52,28 @@ function onCascadingProjectUpdated() { function onProjectPropertyChanged() { if(isRunAsTest) return; - jQuery('form[name=config] input').change(function() { + var modify = function() { var ref = jQuery(this).attr('id'); var cascadingProperty = ''; if (ref != '') { cascadingProperty = jQuery(this).attr('name'); } else { - var childRef = jQuery(this).parents('tr').attr('nameref'); + var parent = jQuery(this).parents('tr'); + while (parent.attr("nameref") == undefined && parent.size() !== 0) { + parent = jQuery(parent).parents('tr'); + } + var childRef = parent.attr("nameref"); cascadingProperty = jQuery('#'+childRef).attr('name'); } - if(cascadingProperty !== undefined){ + if(cascadingProperty !== undefined) { var jobUrl = getJobUrl()+'/modifyCascadingProperty?propertyName='+cascadingProperty; new Ajax.Request(jobUrl, { method : 'get' }); } - }); + }; + jQuery('form[name=config] input, form[name=config] .setting-input').live("change", modify); + jQuery('form[name=config] button').live("click", modify); } jQuery(document).ready(function(){ |

