mirror of
https://github.com/apache/commons-lang.git
synced 2025-02-09 19:45:01 +00:00
LANG-1034: Recursive and reflective EqualsBuilder (closes #202)
patch by yathos UG
This commit is contained in:
parent
9f89fd4626
commit
0095d8adf2
@ -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
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user