LANG-1034: Recursive and reflective EqualsBuilder (closes #202)
patch by yathos UG
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.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;
+
/**
* <p>Constructor for EqualsBuilder.</p>
*
@@ -223,6 +229,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 <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 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);
+ }
+
+ /**
+ * <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 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) {
//-------------------------------------------------------------------------
/**
- * <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 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 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 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);
@@ -332,6 +394,62 @@ public void testObjectBuild() {
}
@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;