Add ReflectionDiffBuilder.Builder

- Add ReflectionDiffBuilder.builder()
This commit is contained in:
Gary Gregory 2023-12-12 07:55:33 -05:00
parent aa4eef85cf
commit 4949adec48
4 changed files with 195 additions and 60 deletions

View File

@ -50,6 +50,8 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="LANG-1724" type="add" dev="ggregory" due-to="Gary Gregory, Dennis Baerten">Customize text pattern in DiffResult#toString().</action>
<action issue="LANG-1724" type="add" dev="ggregory" due-to="Gary Gregory">Add DiffBuilder.Builder</action>
<action issue="LANG-1724" type="add" dev="ggregory" due-to="Gary Gregory">Add DiffBuilder.builder()</action>
<action issue="LANG-1724" type="add" dev="ggregory" due-to="Gary Gregory">Add ReflectionDiffBuilder.Builder</action>
<action issue="LANG-1724" type="add" dev="ggregory" due-to="Gary Gregory">Add ReflectionDiffBuilder.builder()</action>
<!-- FIX -->
<action type="fix" dev="ggregory" due-to="Miklós Karakó, Gary Gregory">Improve Javadoc in ExceptionUtils #1136.</action>
<action type="fix" dev="ggregory" due-to="Saiharshith Karuneegar Ramesh, Gary Gregory">Fixed two non-deterministic tests in EnumUtilsTest.java #1131.</action>
@ -62,7 +64,10 @@ The <action> type attribute can be add,update,fix,remove.
<action type="update" dev="sebb" due-to="Dependabot">Bump commons-parent from 64 to 65.</action>
<!-- REMOVE -->
<action type="remove" dev="ggregory" due-to="Paranoïd User">Drop obsolete JDK 13 Maven profile #1142.</action>
<action type="remove" dev="ggregory" due-to="Gary Gregory">Deprecate org.apache.commons.lang3.builder.Diff.getType().</action>
<action type="remove" dev="ggregory" due-to="Gary Gregory">Deprecate Diff.getType().</action>
<action type="remove" dev="ggregory" due-to="Gary Gregory">Deprecate DiffBuilder.DiffBuilder(T, T, ToStringStyle).</action>
<action type="remove" dev="ggregory" due-to="Gary Gregory">Deprecate DiffBuilder.DiffBuilder(T, T, ToStringStyle, boolean).</action>
<action type="remove" dev="ggregory" due-to="Gary Gregory">Deprecate ReflectionDiffBuilder.ReflectionDiffBuilder(T, T, ToStringStyle).</action>
</release>
<release version="3.14.0" date="2023-11-18" description="New features and bug fixes (Java 8 or above).">
<!-- FIX -->

View File

@ -32,7 +32,7 @@ import org.apache.commons.lang3.ObjectUtils;
* To use this class, write code as follows:
* </p>
*
* <pre>
* <pre>{@code
* public class Person implements Diffable&lt;Person&gt; {
* String name;
* int age;
@ -42,14 +42,18 @@ import org.apache.commons.lang3.ObjectUtils;
*
* public DiffResult diff(Person obj) {
* // No need for null check, as NullPointerException correct if obj is null
* return new DiffBuilder(this, obj, ToStringStyle.SHORT_PREFIX_STYLE)
* return new DiffBuilder.<Person>builder()
* .setLeft(this)
* .setRight(obj)
* .setStyle(ToStringStyle.SHORT_PREFIX_STYLE))
* .build()
* .append("name", this.name, obj.name)
* .append("age", this.age, obj.age)
* .append("smoker", this.smoker, obj.smoker)
* .build();
* }
* }
* </pre>
* }</pre>
*
* <p>
* The {@link ToStringStyle} passed to the constructor is embedded in the returned {@link DiffResult} and influences the style of the
@ -574,4 +578,22 @@ public class DiffBuilder<T> implements Builder<DiffResult<T>> {
return new DiffResult<>(left, right, diffs, style, toStringFormat);
}
/**
* Gets the left object.
*
* @return the left object.
*/
T getLeft() {
return left;
}
/**
* Gets the right object.
*
* @return the right object.
*/
T getRight() {
return right;
}
}

View File

@ -29,16 +29,14 @@ import org.apache.commons.lang3.reflect.FieldUtils;
* Assists in implementing {@link Diffable#diff(Object)} methods.
*
* <p>
* All non-static, non-transient fields (including inherited fields)
* of the objects to diff are discovered using reflection and compared
* for differences.
* All non-static, non-transient fields (including inherited fields) of the objects to diff are discovered using reflection and compared for differences.
* </p>
*
* <p>
* To use this class, write code as follows:
* </p>
*
* <pre>
* <pre>{@code
* public class Person implements Diffable&lt;Person&gt; {
* String name;
* int age;
@ -47,23 +45,27 @@ import org.apache.commons.lang3.reflect.FieldUtils;
*
* public DiffResult diff(Person obj) {
* // No need for null check, as NullPointerException correct if obj is null
* return new ReflectionDiffBuilder(this, obj, ToStringStyle.SHORT_PREFIX_STYLE)
* return new ReflectionDiffBuilder.<Person>builder()
* .setDiffBuilder(DiffBuilder.<Person>builder()
* .setLeft(this)
* .setRight(obj)
* .setStyle(ToStringStyle.SHORT_PREFIX_STYLE)
* .build())
* .setExcludeFieldNames("userName", "password")
* .build();
* }
* }
* </pre>
* }</pre>
*
* <p>
* The {@link ToStringStyle} passed to the constructor is embedded in the
* returned {@link DiffResult} and influences the style of the
* {@code DiffResult.toString()} method. This style choice can be overridden by
* calling {@link DiffResult#toString(ToStringStyle)}.
* The {@link ToStringStyle} passed to the constructor is embedded in the returned {@link DiffResult} and influences the style of the
* {@code DiffResult.toString()} method. This style choice can be overridden by calling {@link DiffResult#toString(ToStringStyle)}.
* </p>
* <p>
* See {@link DiffBuilder} for a non-reflection based version of this class.
* </p>
* @param <T>
* type of the left and right object to diff.
*
* @param <T> type of the left and right object to diff.
* @see Diffable
* @see Diff
* @see DiffResult
@ -73,39 +75,98 @@ import org.apache.commons.lang3.reflect.FieldUtils;
*/
public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {
private final T left;
private final T right;
/**
* Constructs a new instance.
*
* @param <T> type of the left and right object.
* @since 3.15.0
*/
public static final class Builder<T> {
private String[] excludeFieldNames = ArrayUtils.EMPTY_STRING_ARRAY;
private DiffBuilder<T> diffBuilder;
/**
* Builds a new configured {@link ReflectionDiffBuilder}.
*
* @return a new configured {@link ReflectionDiffBuilder}.
*/
public ReflectionDiffBuilder<T> build() {
return new ReflectionDiffBuilder<>(diffBuilder, excludeFieldNames);
}
/**
* Sets the DiffBuilder.
*
* @param diffBuilder the DiffBuilder.
* @return this.
*/
public Builder<T> setDiffBuilder(final DiffBuilder<T> diffBuilder) {
this.diffBuilder = diffBuilder;
return this;
}
/**
* Sets field names to exclude from output. Intended for fields like {@code "password"} or {@code "lastModificationDate"}.
*
* @param excludeFieldNames field names to exclude.
* @return this.
*/
public Builder<T> setExcludeFieldNames(final String... excludeFieldNames) {
this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames);
return this;
}
}
/**
* Constructs a new {@link Builder}.
*
* @param <T> type of the left and right object.
* @return a new {@link Builder}.
* @since 3.15.0
*/
public static <T> Builder<T> builder() {
return new Builder<>();
}
private static String[] toExcludeFieldNames(final String[] excludeFieldNames) {
if (excludeFieldNames == null) {
return ArrayUtils.EMPTY_STRING_ARRAY;
}
// clone and remove nulls
return ArraySorter.sort(ReflectionToStringBuilder.toNoNullStringArray(excludeFieldNames));
}
private final DiffBuilder<T> diffBuilder;
/**
* Field names to exclude from output. Intended for fields like {@code "password"} or {@code "lastModificationDate"}.
*
* @since 3.13.0
*/
private String[] excludeFieldNames;
private ReflectionDiffBuilder(final DiffBuilder<T> diffBuilder, final String[] excludeFieldNames) {
this.diffBuilder = diffBuilder;
this.excludeFieldNames = excludeFieldNames;
}
/**
* Constructs a builder for the specified objects with the specified style.
*
* <p>
* If {@code lhs == rhs} or {@code lhs.equals(rhs)} then the builder will
* not evaluate any calls to {@code append(...)} and will return an empty
* If {@code left == right} or {@code left.equals(right)} then the builder will not evaluate any calls to {@code append(...)} and will return an empty
* {@link DiffResult} when {@link #build()} is executed.
* </p>
* @param lhs
* {@code this} object
* @param rhs
* the object to diff against
* @param style
* the style will use when outputting the objects, {@code null}
* uses the default
* @throws IllegalArgumentException
* if {@code lhs} or {@code rhs} is {@code null}
*
* @param left {@code this} object.
* @param right the object to diff against.
* @param style the style will use when outputting the objects, {@code null} uses the default
* @throws IllegalArgumentException if {@code left} or {@code right} is {@code null}.
* @deprecated Use {@link Builder}.
*/
public ReflectionDiffBuilder(final T lhs, final T rhs, final ToStringStyle style) {
this.left = lhs;
this.right = rhs;
this.diffBuilder = DiffBuilder.<T>builder().setLeft(lhs).setRight(rhs).setStyle(style).build();
@Deprecated
public ReflectionDiffBuilder(final T left, final T right, final ToStringStyle style) {
this(DiffBuilder.<T>builder().setLeft(left).setRight(right).setStyle(style).build(), null);
}
private boolean accept(final Field field) {
@ -118,8 +179,7 @@ public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {
if (Modifier.isStatic(field.getModifiers())) {
return false;
}
if (this.excludeFieldNames != null
&& Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0) {
if (this.excludeFieldNames != null && Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0) {
// Reject fields from the getExcludeFieldNames list.
return false;
}
@ -130,7 +190,7 @@ public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {
for (final Field field : FieldUtils.getAllFields(clazz)) {
if (accept(field)) {
try {
diffBuilder.append(field.getName(), FieldUtils.readField(field, left, true), FieldUtils.readField(field, right, true));
diffBuilder.append(field.getName(), readField(field, getLeft()), readField(field, getRight()));
} catch (final IllegalAccessException e) {
// this can't happen. Would get a Security exception instead
// throw a runtime exception in case the impossible happens.
@ -142,11 +202,11 @@ public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {
@Override
public DiffResult<T> build() {
if (left.equals(right)) {
if (getLeft().equals(getRight())) {
return diffBuilder.build();
}
appendFields(left.getClass());
appendFields(getLeft().getClass());
return diffBuilder.build();
}
@ -160,21 +220,29 @@ public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {
return this.excludeFieldNames.clone();
}
private T getLeft() {
return diffBuilder.getLeft();
}
private T getRight() {
return diffBuilder.getRight();
}
private Object readField(final Field field, final Object target) throws IllegalAccessException {
return FieldUtils.readField(field, target, true);
}
/**
* Sets the field names to exclude.
*
* @param excludeFieldNamesParam
* The field names to exclude from the diff or {@code null}.
* @param excludeFieldNames The field names to exclude from the diff or {@code null}.
* @return {@code this}
* @since 3.13.0
* @deprecated Use {@link Builder#setExcludeFieldNames(String[])}.
*/
public ReflectionDiffBuilder<T> setExcludeFieldNames(final String... excludeFieldNamesParam) {
if (excludeFieldNamesParam == null) {
this.excludeFieldNames = ArrayUtils.EMPTY_STRING_ARRAY;
} else {
// clone and remove nulls
this.excludeFieldNames = ArraySorter.sort(ReflectionToStringBuilder.toNoNullStringArray(excludeFieldNamesParam));
}
@Deprecated
public ReflectionDiffBuilder<T> setExcludeFieldNames(final String... excludeFieldNames) {
this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames);
return this;
}

View File

@ -34,23 +34,23 @@ public class ReflectionDiffBuilderTest extends AbstractLangTest {
private static int staticField;
private final ToStringStyle style = SHORT_STYLE;
private final boolean booleanField = true;
private final boolean[] booleanArrayField = {true};
private final boolean[] booleanArrayField = { true };
private final byte byteField = (byte) 0xFF;
private final byte[] byteArrayField = {(byte) 0xFF};
private final byte[] byteArrayField = { (byte) 0xFF };
private char charField = 'a';
private char[] charArrayField = {'a'};
private char[] charArrayField = { 'a' };
private final double doubleField = 1.0;
private final double[] doubleArrayField = {1.0};
private final double[] doubleArrayField = { 1.0 };
private final float floatField = 1.0f;
private final float[] floatArrayField = {1.0f};
private final float[] floatArrayField = { 1.0f };
int intField = 1;
private final int[] intArrayField = {1};
private final int[] intArrayField = { 1 };
private final long longField = 1L;
private final long[] longArrayField = {1L};
private final long[] longArrayField = { 1L };
private final short shortField = 1;
private final short[] shortArrayField = {1};
private final short[] shortArrayField = { 1 };
private final Object objectField = null;
private final Object[] objectArrayField = {null};
private final Object[] objectArrayField = { null };
private transient String transientField;
@DiffExclude
private String annotatedField = "a";
@ -166,7 +166,26 @@ public class ReflectionDiffBuilderTest extends AbstractLangTest {
@Test
public void testGetExcludeFieldNamesWithNullExcludedFieldNames() {
final ReflectionDiffBuilder<TypeTestClass> reflectionDiffBuilder = new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE);
// @formatter:off
final ReflectionDiffBuilder<TypeTestClass> reflectionDiffBuilder = ReflectionDiffBuilder.<TypeTestClass>builder()
.setDiffBuilder(DiffBuilder.<TypeTestClass>builder()
.setLeft(new TypeTestClass())
.setRight(new TypeTestChildClass())
.setStyle(SHORT_STYLE)
.build())
.build();
// @formatter:on
final String[] excludeFieldNames = reflectionDiffBuilder.getExcludeFieldNames();
assertNotNull(excludeFieldNames);
assertEquals(0, excludeFieldNames.length);
}
@Test
public void testGetExcludeFieldNamesWithNullExcludedFieldNamesCtor() {
// @formatter:off
final ReflectionDiffBuilder<TypeTestClass> reflectionDiffBuilder =
new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE);
// @formatter:on
reflectionDiffBuilder.setExcludeFieldNames(null);
final String[] excludeFieldNames = reflectionDiffBuilder.getExcludeFieldNames();
assertNotNull(excludeFieldNames);
@ -175,7 +194,28 @@ public class ReflectionDiffBuilderTest extends AbstractLangTest {
@Test
public void testGetExcludeFieldNamesWithNullValuesInExcludedFieldNames() {
final ReflectionDiffBuilder<TypeTestClass> reflectionDiffBuilder = new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE);
// @formatter:off
final ReflectionDiffBuilder<TypeTestClass> reflectionDiffBuilder = ReflectionDiffBuilder.<TypeTestClass>builder()
.setDiffBuilder(DiffBuilder.<TypeTestClass>builder()
.setLeft(new TypeTestClass())
.setRight(new TypeTestChildClass())
.setStyle(SHORT_STYLE)
.build())
.setExcludeFieldNames("charField", null)
.build();
// @formatter:on
final String[] excludeFieldNames = reflectionDiffBuilder.getExcludeFieldNames();
assertNotNull(excludeFieldNames);
assertEquals(1, excludeFieldNames.length);
assertEquals("charField", excludeFieldNames[0]);
}
@Test
public void testGetExcludeFieldNamesWithNullValuesInExcludedFieldNamesCtor() {
// @formatter:off
final ReflectionDiffBuilder<TypeTestClass> reflectionDiffBuilder =
new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE);
// @formatter:on
reflectionDiffBuilder.setExcludeFieldNames("charField", null);
final String[] excludeFieldNames = reflectionDiffBuilder.getExcludeFieldNames();
assertNotNull(excludeFieldNames);