From 0095d8adf26b9469115b1be0358cb09d1fcb5fd4 Mon Sep 17 00:00:00 2001 From: pascalschumacher Date: Sun, 23 Oct 2016 19:56:28 +0200 Subject: [PATCH] LANG-1034: Recursive and reflective EqualsBuilder (closes #202) patch by yathos UG --- .../commons/lang3/builder/EqualsBuilder.java | 215 ++++++++++++++++-- .../lang3/builder/EqualsBuilderTest.java | 118 ++++++++++ 2 files changed, 313 insertions(+), 20 deletions(-) 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 cab9831e8..5f1c8e083 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.Set; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.tuple.Pair; /** @@ -210,6 +211,11 @@ private static void unregister(final Object lhs, final Object rhs) { */ private boolean isEquals = true; + private boolean testTransients = false; + private boolean testRecursive = false; + private Class reflectUpToClass = null; + private String[] excludeFields = null; + /** *

Constructor for EqualsBuilder.

* @@ -222,6 +228,88 @@ public EqualsBuilder() { //------------------------------------------------------------------------- + /** + * 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 EqualsBuilder or b< + * using equals(). + * @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 null) + * at reflective tests. + * @return Class null is same as + * java.lang.Object + */ + 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; + } + + /** *

This method uses reflection to determine if the two Objects * are equal.

@@ -332,12 +420,96 @@ public static boolean reflectionEquals(final Object lhs, final Object rhs, final */ 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); + } + + /** + *

This method uses reflection to determine if the two Objects + * are equal.

+ * + *

It uses AccessibleObject.setAccessible 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 + * equals().

+ * + *

If the testTransients parameter is set to true, transient + * members will be tested, otherwise they are ignored, as they are likely + * derived fields, and not part of the value of the Object.

+ * + *

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.

+ * + *

If the testRecursive parameter is set to true, non primitive + * (and non primitive wrapper) field types will be compared by + * EqualsBuilder recursively instead of invoking their + * equals() method. Leading to a deep reflection equals test. + * + * @param lhs this object + * @param rhs the other object + * @param testTransients whether to include transient fields + * @param reflectUpToClass the superclass to reflect up to (inclusive), + * may be null + * @param testRecursive whether to call reflection equals on non primitive + * fields recursively. + * @param excludeFields array of field names to exclude from testing + * @return true 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(); + } + + /** + *

Tests if two objects by using reflection.

+ * + *

It uses AccessibleObject.setAccessible 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 + * equals().

+ * + *

If the testTransients field is set to true, transient + * members will be tested, otherwise they are ignored, as they are likely + * derived fields, and not part of the value of the Object.

+ * + *

Static fields will not be included. Superclass fields will be appended + * up to and including the specified superclass in field reflectUpToClass. + * A null superclass is treated as java.lang.Object.

+ * + *

Field names listed in field excludeFields will be ignored.

+ * + * @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 static boolean reflectionEquals(final Object lhs, final Object rhs, final } } 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 static boolean reflectionEquals(final Object lhs, final Object rhs, final // 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 static boolean reflectionEquals(final Object lhs, final Object rhs, final * @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 @@ private static void reflectionAppend( 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 EqualsBuilder appendSuper(final boolean superEquals) { //------------------------------------------------------------------------- /** - *

Test if two Objects are equal using their + *

Test if two Objects are equal using either + * #{@link #reflectionAppend(Object, Object)}, if object are non + * primitives (or wrapper of primitives) or if field testRecursive + * is set to false. Otherwise, using their * equals method.

* * @param lhs the left hand object @@ -472,7 +643,11 @@ public EqualsBuilder append(final Object lhs, final Object rhs) { 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 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 a58604916..c2551af85 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 void setT(final int t) { } } + 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); @@ -331,6 +393,62 @@ public void testObjectBuild() { assertEquals(Boolean.TRUE, new EqualsBuilder().append((Object) null, (Object) null).build()); } + @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;