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

patch by yathos UG
This commit is contained in:
pascalschumacher 2016-10-23 19:56:28 +02:00
parent 9f89fd4626
commit 0095d8adf2
2 changed files with 313 additions and 20 deletions

View File

@ -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>
*
@ -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 <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

View File

@ -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;