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;