Repository: commons-lang
Updated Branches:
  refs/heads/master 9f89fd462 -> 0f6a292a2


LANG-1034: Recursive and reflective EqualsBuilder (closes #202)

patch by yathos UG


Project: http://git-wip-us.apache.org/repos/asf/commons-lang/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-lang/commit/0095d8ad
Tree: http://git-wip-us.apache.org/repos/asf/commons-lang/tree/0095d8ad
Diff: http://git-wip-us.apache.org/repos/asf/commons-lang/diff/0095d8ad

Branch: refs/heads/master
Commit: 0095d8adf26b9469115b1be0358cb09d1fcb5fd4
Parents: 9f89fd4
Author: pascalschumacher <pascalschumac...@gmx.net>
Authored: Sun Oct 23 19:56:28 2016 +0200
Committer: pascalschumacher <pascalschumac...@gmx.net>
Committed: Sun Nov 13 18:47:28 2016 +0100

----------------------------------------------------------------------
 .../commons/lang3/builder/EqualsBuilder.java    | 215 +++++++++++++++++--
 .../lang3/builder/EqualsBuilderTest.java        | 118 ++++++++++
 2 files changed, 313 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-lang/blob/0095d8ad/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java 
b/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
index cab9831..5f1c8e0 100644
--- a/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
@@ -24,6 +24,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.tuple.Pair;
 
 /**
@@ -210,6 +211,11 @@ public class EqualsBuilder implements Builder<Boolean> {
      */
     private boolean isEquals = true;
 
+    private boolean testTransients = false;
+    private boolean testRecursive = false;
+    private Class<?> reflectUpToClass = null;
+    private String[] excludeFields = null;
+
     /**
      * <p>Constructor for EqualsBuilder.</p>
      *
@@ -223,6 +229,88 @@ public class EqualsBuilder implements Builder<Boolean> {
     //-------------------------------------------------------------------------
 
     /**
+     * Whether calls of {@link #reflectionAppend(Object, Object)}
+     * will test transient fields, too.
+     * @return boolean
+     */
+    public boolean isTestTransients() {
+        return testTransients;
+    }
+
+    /**
+     * Set testing transients behavior for calls 
+     * of {@link #reflectionAppend(Object, Object)}.
+     * @param testTransients whether to test transient fields
+     * @return EqualsBuilder - used to chain calls.
+     */
+    public EqualsBuilder setTestTransients(boolean testTransients) {
+        this.testTransients = testTransients;
+        return this;
+    }
+
+    /**
+     * Whether calls of {@link #append(Object, Object)}
+     * will recursively test non primitive fields by
+     * using this <code>EqualsBuilder</code> or b<
+     * using <code>equals()</code>.
+     * @return boolean
+     */
+    public boolean isTestRecursive() {
+        return testRecursive;
+    }
+
+    /**
+     * Set recursive test behavior 
+     * of {@link #reflectionAppend(Object, Object)}.
+     * @param testRecursive  whether to do a recursive test
+     * @return EqualsBuilder - used to chain calls.
+     */
+    public EqualsBuilder setTestRecursive(boolean testRecursive) {
+        this.testRecursive = testRecursive;
+        return this;
+    }
+
+    /**
+     * The superclass to reflect up to (maybe <code>null</code>)
+     * at reflective tests.
+     * @return Class  <code>null</code> is same as 
+     *  <code>java.lang.Object</code>
+     */
+    public Class<?> getReflectUpToClass() {
+        return reflectUpToClass;
+    }
+
+    /**
+     * Set the superclass to reflect up to
+     * at reflective tests.
+     * @return EqualsBuilder - used to chain calls.
+     */
+    public EqualsBuilder setReflectUpToClass(Class<?> reflectUpToClass) {
+        this.reflectUpToClass = reflectUpToClass;
+        return this;
+    }
+
+    /**
+     * Fields names which will be ignored in any class
+     * by reflection tests.
+     * @return String[]  maybe null.
+     */
+    public String[] getExcludeFields() {
+        return excludeFields;
+    }
+
+    /**
+     * Set field names to be excluded by reflection tests.
+     * @param excludeFields
+     * @return EqualsBuilder - used to chain calls.
+     */
+    public EqualsBuilder setExcludeFields(String... excludeFields) {
+        this.excludeFields = excludeFields;
+        return this;
+    }
+    
+
+    /**
      * <p>This method uses reflection to determine if the two 
<code>Object</code>s
      * are equal.</p>
      *
@@ -332,12 +420,96 @@ public class EqualsBuilder implements Builder<Boolean> {
      */
     public static boolean reflectionEquals(final Object lhs, final Object rhs, 
final boolean testTransients, final Class<?> reflectUpToClass,
             final String... excludeFields) {
+        return reflectionEquals(lhs, rhs, testTransients, reflectUpToClass, 
false, excludeFields);
+    }
+    
+    /**
+     * <p>This method uses reflection to determine if the two 
<code>Object</code>s
+     * are equal.</p>
+     *
+     * <p>It uses <code>AccessibleObject.setAccessible</code> to gain access 
to private
+     * fields. This means that it will throw a security exception if run under
+     * a security manager, if the permissions are not set up correctly. It is 
also
+     * not as efficient as testing explicitly. Non-primitive fields are 
compared using 
+     * <code>equals()</code>.</p>
+     *
+     * <p>If the testTransients parameter is set to <code>true</code>, 
transient
+     * members will be tested, otherwise they are ignored, as they are likely
+     * derived fields, and not part of the value of the 
<code>Object</code>.</p>
+     *
+     * <p>Static fields will not be included. Superclass fields will be 
appended
+     * up to and including the specified superclass. A null superclass is 
treated
+     * as java.lang.Object.</p>
+     * 
+     * <p>If the testRecursive parameter is set to <code>true</code>, non 
primitive
+     * (and non primitive wrapper) field types will be compared by 
+     * <code>EqualsBuilder</code> recursively instead of invoking their 
+     * <code>equals()</code> method. Leading to a deep reflection equals test.
+     *
+     * @param lhs  <code>this</code> object
+     * @param rhs  the other object
+     * @param testTransients  whether to include transient fields
+     * @param reflectUpToClass  the superclass to reflect up to (inclusive),
+     *  may be <code>null</code>
+     * @param testRecursive  whether to call reflection equals on non primitive
+     *  fields recursively. 
+     * @param excludeFields  array of field names to exclude from testing
+     * @return <code>true</code> if the two Objects have tested equals.
+     * 
+     * @see EqualsExclude
+     */
+    public static boolean reflectionEquals(final Object lhs, final Object rhs, 
final boolean testTransients, final Class<?> reflectUpToClass,
+            boolean testRecursive, final String... excludeFields) {
         if (lhs == rhs) {
             return true;
         }
         if (lhs == null || rhs == null) {
             return false;
         }
+        final EqualsBuilder equalsBuilder = new EqualsBuilder();
+        equalsBuilder.setExcludeFields(excludeFields)
+                    .setReflectUpToClass(reflectUpToClass)
+                    .setTestTransients(testTransients)
+                    .setTestRecursive(testRecursive);
+        
+        equalsBuilder.reflectionAppend(lhs, rhs);
+        return equalsBuilder.isEquals();
+    }
+    
+    /**
+     * <p>Tests if two <code>objects</code> by using reflection.</p>
+     * 
+     * <p>It uses <code>AccessibleObject.setAccessible</code> to gain access 
to private
+     * fields. This means that it will throw a security exception if run under
+     * a security manager, if the permissions are not set up correctly. It is 
also
+     * not as efficient as testing explicitly. Non-primitive fields are 
compared using 
+     * <code>equals()</code>.</p>
+     * 
+     * <p>If the testTransients field is set to <code>true</code>, transient
+     * members will be tested, otherwise they are ignored, as they are likely
+     * derived fields, and not part of the value of the 
<code>Object</code>.</p>
+     *
+     * <p>Static fields will not be included. Superclass fields will be 
appended
+     * up to and including the specified superclass in field 
<code>reflectUpToClass</code>.
+     * A null superclass is treated as java.lang.Object.</p>
+     * 
+     * <p>Field names listed in field <code>excludeFields</code> will be 
ignored.</p>
+     * 
+     * @param lhs  the left hand object
+     * @param rhs  the left hand object
+     * @return EqualsBuilder - used to chain calls.
+     */
+    public EqualsBuilder reflectionAppend(final Object lhs, final Object rhs) {
+        if(!isEquals)
+            return this;
+        
+        if (lhs == rhs) {
+            return this;
+        }
+        if (lhs == null || rhs == null) {
+            isEquals = false;
+            return this;
+        }
         // Find the leaf class since there may be transients in the leaf
         // class or in classes between the leaf and root.
         // If we are not testing transients or a subclass has no ivars,
@@ -359,17 +531,18 @@ public class EqualsBuilder implements Builder<Boolean> {
             }
         } else {
             // The two classes are not related.
-            return false;
+            isEquals = false;
+            return this;
         }
-        final EqualsBuilder equalsBuilder = new EqualsBuilder();
+        
         try {
             if (testClass.isArray()) {
-                equalsBuilder.append(lhs, rhs);
+                append(lhs, rhs);
             } else {
-                reflectionAppend(lhs, rhs, testClass, equalsBuilder, 
testTransients, excludeFields);
+                reflectionAppend(lhs, rhs, testClass);
                 while (testClass.getSuperclass() != null && testClass != 
reflectUpToClass) {
                     testClass = testClass.getSuperclass();
-                    reflectionAppend(lhs, rhs, testClass, equalsBuilder, 
testTransients, excludeFields);
+                    reflectionAppend(lhs, rhs, testClass);
                 }
             }
         } catch (final IllegalArgumentException e) {
@@ -378,9 +551,10 @@ public class EqualsBuilder implements Builder<Boolean> {
             // we are testing transients.
             // If a subclass has ivars that we are trying to test them, we get 
an
             // exception and we know that the objects are not equal.
-            return false;
+            isEquals = false;
+            return this;
         }
-        return equalsBuilder.isEquals();
+        return this;
     }
 
     /**
@@ -390,17 +564,11 @@ public class EqualsBuilder implements Builder<Boolean> {
      * @param lhs  the left hand object
      * @param rhs  the right hand object
      * @param clazz  the class to append details of
-     * @param builder  the builder to append to
-     * @param useTransients  whether to test transient fields
-     * @param excludeFields  array of field names to exclude from testing
      */
-    private static void reflectionAppend(
+    private void reflectionAppend(
         final Object lhs,
         final Object rhs,
-        final Class<?> clazz,
-        final EqualsBuilder builder,
-        final boolean useTransients,
-        final String[] excludeFields) {
+        final Class<?> clazz) {
 
         if (isRegistered(lhs, rhs)) {
             return;
@@ -410,15 +578,15 @@ public class EqualsBuilder implements Builder<Boolean> {
             register(lhs, rhs);
             final Field[] fields = clazz.getDeclaredFields();
             AccessibleObject.setAccessible(fields, true);
-            for (int i = 0; i < fields.length && builder.isEquals; i++) {
+            for (int i = 0; i < fields.length && isEquals; i++) {
                 final Field f = fields[i];
                 if (!ArrayUtils.contains(excludeFields, f.getName())
                     && !f.getName().contains("$")
-                    && (useTransients || 
!Modifier.isTransient(f.getModifiers()))
+                    && (testTransients || 
!Modifier.isTransient(f.getModifiers()))
                     && !Modifier.isStatic(f.getModifiers())
                     && !f.isAnnotationPresent(EqualsExclude.class)) {
                     try {
-                        builder.append(f.get(lhs), f.get(rhs));
+                        append(f.get(lhs), f.get(rhs));
                     } catch (final IllegalAccessException e) {
                         //this can't happen. Would get a Security exception 
instead
                         //throw a runtime exception in case the impossible 
happens.
@@ -451,7 +619,10 @@ public class EqualsBuilder implements Builder<Boolean> {
     //-------------------------------------------------------------------------
 
     /**
-     * <p>Test if two <code>Object</code>s are equal using their
+     * <p>Test if two <code>Object</code>s are equal using either 
+     * #{@link #reflectionAppend(Object, Object)}, if object are non
+     * primitives (or wrapper of primitives) or if field 
<code>testRecursive</code> 
+     * is set to <code>false</code>. Otherwise, using their 
      * <code>equals</code> method.</p>
      *
      * @param lhs  the left hand object
@@ -472,7 +643,11 @@ public class EqualsBuilder implements Builder<Boolean> {
         final Class<?> lhsClass = lhs.getClass();
         if (!lhsClass.isArray()) {
             // The simple case, not an array, just test the element
-            isEquals = lhs.equals(rhs);
+            if(testRecursive && !ClassUtils.isPrimitiveOrWrapper(lhsClass)) {
+                reflectionAppend(lhs, rhs);
+            } else {
+                isEquals = lhs.equals(rhs);
+            }
         } else {
             // factor out array case in order to keep method small enough
             // to be inlined

http://git-wip-us.apache.org/repos/asf/commons-lang/blob/0095d8ad/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java 
b/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java
index a586049..c2551af 100644
--- a/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java
+++ b/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java
@@ -146,6 +146,68 @@ public class EqualsBuilderTest {
         }
     }
 
+    static class TestRecursiveObject {
+        private TestRecursiveInnerObject a;
+        private TestRecursiveInnerObject b;
+        private int z;
+        
+        public TestRecursiveObject(TestRecursiveInnerObject a, 
+                TestRecursiveInnerObject b, int z) {
+            this.a = a;
+            this.b = b;
+        }
+        
+        public TestRecursiveInnerObject getA() {
+            return a;
+        }
+        
+        public TestRecursiveInnerObject getB() {
+            return b;
+        }
+
+        public int getZ() {
+            return z;
+        }
+        
+    }
+
+    static class TestRecursiveInnerObject {
+        private int n;
+        public TestRecursiveInnerObject(int n) {
+            this.n = n;
+        }
+        
+        public int getN() {
+            return n;
+        }
+    }
+
+    static class TestRecursiveCycleObject {
+        private TestRecursiveCycleObject cycle;
+        private int n;
+        public TestRecursiveCycleObject(int n) {
+            this.n = n;
+            this.cycle = this;
+        }
+        
+        public TestRecursiveCycleObject(TestRecursiveCycleObject cycle, int n) 
{
+            this.n = n;
+            this.cycle = cycle;
+        }
+        
+        public int getN() {
+            return n;
+        }
+        
+        public TestRecursiveCycleObject getCycle() {
+            return cycle;
+        }
+        
+        public void setCycle(TestRecursiveCycleObject cycle) {
+            this.cycle = cycle;
+        }
+    }
+
     @Test
     public void testReflectionEquals() {
         final TestObject o1 = new TestObject(4);
@@ -332,6 +394,62 @@ public class EqualsBuilderTest {
     }
 
     @Test
+    public void testObjectRecursive() {
+        final TestRecursiveInnerObject i1_1 = new TestRecursiveInnerObject(1);
+        final TestRecursiveInnerObject i1_2 = new TestRecursiveInnerObject(1);
+        final TestRecursiveInnerObject i2_1 = new TestRecursiveInnerObject(2);
+        final TestRecursiveInnerObject i2_2 = new TestRecursiveInnerObject(2);
+        final TestRecursiveInnerObject i3 = new TestRecursiveInnerObject(3);
+        final TestRecursiveInnerObject i4 = new TestRecursiveInnerObject(4);
+        
+        final TestRecursiveObject o1_a = new TestRecursiveObject(i1_1, i2_1, 
1);
+        final TestRecursiveObject o1_b = new TestRecursiveObject(i1_2, i2_2, 
1);
+        final TestRecursiveObject o2 = new TestRecursiveObject(i3, i4, 2);
+        final TestRecursiveObject oNull = new TestRecursiveObject(null, null, 
2);
+        
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o1_a).isEquals());
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o1_b).isEquals());
+        
+        assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o2).isEquals());
+        
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(oNull, 
oNull).isEquals());
+        assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
oNull).isEquals());
+    }
+
+    @Test
+    public void testObjectRecursiveCycleSelfreference() {
+        final TestRecursiveCycleObject o1_a = new TestRecursiveCycleObject(1);
+        final TestRecursiveCycleObject o1_b = new TestRecursiveCycleObject(1);
+        final TestRecursiveCycleObject o2 = new TestRecursiveCycleObject(2);
+        
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o1_a).isEquals());
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o1_b).isEquals());
+        assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o2).isEquals());
+    }
+
+    @Test
+    public void testObjectRecursiveCycle() {
+        final TestRecursiveCycleObject o1_a = new TestRecursiveCycleObject(1);
+        final TestRecursiveCycleObject i1_a = new 
TestRecursiveCycleObject(o1_a, 100);
+        o1_a.setCycle(i1_a);
+        
+        final TestRecursiveCycleObject o1_b = new TestRecursiveCycleObject(1);
+        final TestRecursiveCycleObject i1_b = new 
TestRecursiveCycleObject(o1_b, 100);
+        o1_b.setCycle(i1_b);
+        
+        final TestRecursiveCycleObject o2 = new TestRecursiveCycleObject(2);
+        final TestRecursiveCycleObject i2 = new TestRecursiveCycleObject(o1_b, 
200);
+        o2.setCycle(i2);
+        
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o1_a).isEquals());
+        assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o1_b).isEquals());
+        assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1_a, 
o2).isEquals());
+        
+        assertTrue(EqualsBuilder.reflectionEquals(o1_a, o1_b, false, null, 
true));
+        assertFalse(EqualsBuilder.reflectionEquals(o1_a, o2, false, null, 
true));
+    }
+
+    @Test
     public void testLong() {
         final long o1 = 1L;
         final long o2 = 2L;

Reply via email to