From 4949adec480ddccdee4f7d09231e85e84fec00a1 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Tue, 12 Dec 2023 07:55:33 -0500 Subject: [PATCH] Add ReflectionDiffBuilder.Builder - Add ReflectionDiffBuilder.builder() --- src/changes/changes.xml | 7 +- .../commons/lang3/builder/DiffBuilder.java | 28 +++- .../lang3/builder/ReflectionDiffBuilder.java | 158 +++++++++++++----- .../builder/ReflectionDiffBuilderTest.java | 62 +++++-- 4 files changed, 195 insertions(+), 60 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 304339d08..49aef984e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -50,6 +50,8 @@ The type attribute can be add,update,fix,remove. Customize text pattern in DiffResult#toString(). Add DiffBuilder.Builder Add DiffBuilder.builder() + Add ReflectionDiffBuilder.Builder + Add ReflectionDiffBuilder.builder() Improve Javadoc in ExceptionUtils #1136. Fixed two non-deterministic tests in EnumUtilsTest.java #1131. @@ -62,7 +64,10 @@ The type attribute can be add,update,fix,remove. Bump commons-parent from 64 to 65. Drop obsolete JDK 13 Maven profile #1142. - Deprecate org.apache.commons.lang3.builder.Diff.getType(). + Deprecate Diff.getType(). + Deprecate DiffBuilder.DiffBuilder(T, T, ToStringStyle). + Deprecate DiffBuilder.DiffBuilder(T, T, ToStringStyle, boolean). + Deprecate ReflectionDiffBuilder.ReflectionDiffBuilder(T, T, ToStringStyle). diff --git a/src/main/java/org/apache/commons/lang3/builder/DiffBuilder.java b/src/main/java/org/apache/commons/lang3/builder/DiffBuilder.java index 602ae8be9..378729145 100644 --- a/src/main/java/org/apache/commons/lang3/builder/DiffBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/DiffBuilder.java @@ -32,7 +32,7 @@ import org.apache.commons.lang3.ObjectUtils; * To use this class, write code as follows: *

* - *
+ * 
{@code
  * public class Person implements Diffable<Person> {
  *   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.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();
  *   }
  * }
- * 
+ * }
* *

* 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 implements Builder> { 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; + } + } diff --git a/src/main/java/org/apache/commons/lang3/builder/ReflectionDiffBuilder.java b/src/main/java/org/apache/commons/lang3/builder/ReflectionDiffBuilder.java index 505c21e62..05f08d597 100644 --- a/src/main/java/org/apache/commons/lang3/builder/ReflectionDiffBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/ReflectionDiffBuilder.java @@ -29,16 +29,14 @@ import org.apache.commons.lang3.reflect.FieldUtils; * Assists in implementing {@link Diffable#diff(Object)} methods. * *

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

* *

* To use this class, write code as follows: *

* - *
+ * 
{@code
  * public class Person implements Diffable<Person> {
  *   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.builder()
+ *       .setDiffBuilder(DiffBuilder.builder()
+ *           .setLeft(this)
+ *           .setRight(obj)
+ *           .setStyle(ToStringStyle.SHORT_PREFIX_STYLE)
+ *           .build())
+ *       .setExcludeFieldNames("userName", "password")
  *       .build();
  *   }
  * }
- * 
+ * }
* *

- * 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)}. *

*

* See {@link DiffBuilder} for a non-reflection based version of this class. *

- * @param - * type of the left and right object to diff. + * + * @param 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 implements Builder> { - private final T left; - private final T right; + /** + * Constructs a new instance. + * + * @param type of the left and right object. + * @since 3.15.0 + */ + public static final class Builder { + + private String[] excludeFieldNames = ArrayUtils.EMPTY_STRING_ARRAY; + private DiffBuilder diffBuilder; + + /** + * Builds a new configured {@link ReflectionDiffBuilder}. + * + * @return a new configured {@link ReflectionDiffBuilder}. + */ + public ReflectionDiffBuilder build() { + return new ReflectionDiffBuilder<>(diffBuilder, excludeFieldNames); + } + + /** + * Sets the DiffBuilder. + * + * @param diffBuilder the DiffBuilder. + * @return this. + */ + public Builder setDiffBuilder(final DiffBuilder 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 setExcludeFieldNames(final String... excludeFieldNames) { + this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames); + return this; + } + + } + + /** + * Constructs a new {@link Builder}. + * + * @param type of the left and right object. + * @return a new {@link Builder}. + * @since 3.15.0 + */ + public static Builder 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 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 diffBuilder, final String[] excludeFieldNames) { + this.diffBuilder = diffBuilder; + this.excludeFieldNames = excludeFieldNames; + } + /** * Constructs a builder for the specified objects with the specified style. * *

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

- * @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.builder().setLeft(lhs).setRight(rhs).setStyle(style).build(); + @Deprecated + public ReflectionDiffBuilder(final T left, final T right, final ToStringStyle style) { + this(DiffBuilder.builder().setLeft(left).setRight(right).setStyle(style).build(), null); } private boolean accept(final Field field) { @@ -118,8 +179,7 @@ public class ReflectionDiffBuilder implements Builder> { 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 implements Builder> { 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 implements Builder> { @Override public DiffResult 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 implements Builder> { 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 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 setExcludeFieldNames(final String... excludeFieldNames) { + this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames); return this; } diff --git a/src/test/java/org/apache/commons/lang3/builder/ReflectionDiffBuilderTest.java b/src/test/java/org/apache/commons/lang3/builder/ReflectionDiffBuilderTest.java index 42c5d6f11..1582d3ade 100644 --- a/src/test/java/org/apache/commons/lang3/builder/ReflectionDiffBuilderTest.java +++ b/src/test/java/org/apache/commons/lang3/builder/ReflectionDiffBuilderTest.java @@ -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 reflectionDiffBuilder = new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE); + // @formatter:off + final ReflectionDiffBuilder reflectionDiffBuilder = ReflectionDiffBuilder.builder() + .setDiffBuilder(DiffBuilder.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 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 reflectionDiffBuilder = new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE); + // @formatter:off + final ReflectionDiffBuilder reflectionDiffBuilder = ReflectionDiffBuilder.builder() + .setDiffBuilder(DiffBuilder.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 reflectionDiffBuilder = + new ReflectionDiffBuilder<>(new TypeTestClass(), new TypeTestChildClass(), SHORT_STYLE); + // @formatter:on reflectionDiffBuilder.setExcludeFieldNames("charField", null); final String[] excludeFieldNames = reflectionDiffBuilder.getExcludeFieldNames(); assertNotNull(excludeFieldNames);