ReflectionDiffBuilder.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.lang3.builder;

  18. import java.lang.reflect.Field;
  19. import java.lang.reflect.Modifier;
  20. import java.util.Arrays;

  21. import org.apache.commons.lang3.ArraySorter;
  22. import org.apache.commons.lang3.ArrayUtils;
  23. import org.apache.commons.lang3.ClassUtils;
  24. import org.apache.commons.lang3.reflect.FieldUtils;

  25. /**
  26.  * Assists in implementing {@link Diffable#diff(Object)} methods.
  27.  *
  28.  * <p>
  29.  * All non-static, non-transient fields (including inherited fields) of the objects to diff are discovered using reflection and compared for differences.
  30.  * </p>
  31.  *
  32.  * <p>
  33.  * To use this class, write code as follows:
  34.  * </p>
  35.  *
  36.  * <pre>{@code
  37.  * public class Person implements Diffable<Person> {
  38.  *   String name;
  39.  *   int age;
  40.  *   boolean smoker;
  41.  *   ...
  42.  *
  43.  *   public DiffResult diff(Person obj) {
  44.  *     // No need for null check, as NullPointerException correct if obj is null
  45.  *     return new ReflectionDiffBuilder.<Person>builder()
  46.  *       .setDiffBuilder(DiffBuilder.<Person>builder()
  47.  *           .setLeft(this)
  48.  *           .setRight(obj)
  49.  *           .setStyle(ToStringStyle.SHORT_PREFIX_STYLE)
  50.  *           .build())
  51.  *       .setExcludeFieldNames("userName", "password")
  52.  *       .build();
  53.  *   }
  54.  * }
  55.  * }</pre>
  56.  *
  57.  * <p>
  58.  * The {@link ToStringStyle} passed to the constructor is embedded in the returned {@link DiffResult} and influences the style of the
  59.  * {@code DiffResult.toString()} method. This style choice can be overridden by calling {@link DiffResult#toString(ToStringStyle)}.
  60.  * </p>
  61.  * <p>
  62.  * See {@link DiffBuilder} for a non-reflection based version of this class.
  63.  * </p>
  64.  *
  65.  * @param <T> type of the left and right object to diff.
  66.  * @see Diffable
  67.  * @see Diff
  68.  * @see DiffResult
  69.  * @see ToStringStyle
  70.  * @see DiffBuilder
  71.  * @since 3.6
  72.  */
  73. public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {

  74.     /**
  75.      * Constructs a new instance.
  76.      *
  77.      * @param <T> type of the left and right object.
  78.      * @since 3.15.0
  79.      */
  80.     public static final class Builder<T> {

  81.         private String[] excludeFieldNames = ArrayUtils.EMPTY_STRING_ARRAY;
  82.         private DiffBuilder<T> diffBuilder;

  83.         /**
  84.          * Constructs a new instance.
  85.          */
  86.         public Builder() {
  87.             // empty
  88.         }

  89.         /**
  90.          * Builds a new configured {@link ReflectionDiffBuilder}.
  91.          *
  92.          * @return a new configured {@link ReflectionDiffBuilder}.
  93.          */
  94.         public ReflectionDiffBuilder<T> build() {
  95.             return new ReflectionDiffBuilder<>(diffBuilder, excludeFieldNames);
  96.         }

  97.         /**
  98.          * Sets the DiffBuilder.
  99.          *
  100.          * @param diffBuilder the DiffBuilder.
  101.          * @return {@code this} instance.
  102.          */
  103.         public Builder<T> setDiffBuilder(final DiffBuilder<T> diffBuilder) {
  104.             this.diffBuilder = diffBuilder;
  105.             return this;
  106.         }

  107.         /**
  108.          * Sets field names to exclude from output. Intended for fields like {@code "password"} or {@code "lastModificationDate"}.
  109.          *
  110.          * @param excludeFieldNames field names to exclude.
  111.          * @return {@code this} instance.
  112.          */
  113.         public Builder<T> setExcludeFieldNames(final String... excludeFieldNames) {
  114.             this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames);
  115.             return this;
  116.         }

  117.     }

  118.     /**
  119.      * Constructs a new {@link Builder}.
  120.      *
  121.      * @param <T> type of the left and right object.
  122.      * @return a new {@link Builder}.
  123.      * @since 3.15.0
  124.      */
  125.     public static <T> Builder<T> builder() {
  126.         return new Builder<>();
  127.     }

  128.     private static String[] toExcludeFieldNames(final String[] excludeFieldNames) {
  129.         if (excludeFieldNames == null) {
  130.             return ArrayUtils.EMPTY_STRING_ARRAY;
  131.         }
  132.         // clone and remove nulls
  133.         return ArraySorter.sort(ReflectionToStringBuilder.toNoNullStringArray(excludeFieldNames));
  134.     }

  135.     private final DiffBuilder<T> diffBuilder;

  136.     /**
  137.      * Field names to exclude from output. Intended for fields like {@code "password"} or {@code "lastModificationDate"}.
  138.      */
  139.     private String[] excludeFieldNames;

  140.     private ReflectionDiffBuilder(final DiffBuilder<T> diffBuilder, final String[] excludeFieldNames) {
  141.         this.diffBuilder = diffBuilder;
  142.         this.excludeFieldNames = excludeFieldNames;
  143.     }

  144.     /**
  145.      * Constructs a builder for the specified objects with the specified style.
  146.      *
  147.      * <p>
  148.      * 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
  149.      * {@link DiffResult} when {@link #build()} is executed.
  150.      * </p>
  151.      *
  152.      * @param left  {@code this} object.
  153.      * @param right the object to diff against.
  154.      * @param style the style will use when outputting the objects, {@code null} uses the default
  155.      * @throws IllegalArgumentException if {@code left} or {@code right} is {@code null}.
  156.      * @deprecated Use {@link Builder}.
  157.      */
  158.     @Deprecated
  159.     public ReflectionDiffBuilder(final T left, final T right, final ToStringStyle style) {
  160.         this(DiffBuilder.<T>builder().setLeft(left).setRight(right).setStyle(style).build(), null);
  161.     }

  162.     private boolean accept(final Field field) {
  163.         if (field.getName().indexOf(ClassUtils.INNER_CLASS_SEPARATOR_CHAR) != -1) {
  164.             return false;
  165.         }
  166.         if (Modifier.isTransient(field.getModifiers())) {
  167.             return false;
  168.         }
  169.         if (Modifier.isStatic(field.getModifiers())) {
  170.             return false;
  171.         }
  172.         if (this.excludeFieldNames != null && Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0) {
  173.             // Reject fields from the getExcludeFieldNames list.
  174.             return false;
  175.         }
  176.         return !field.isAnnotationPresent(DiffExclude.class);
  177.     }

  178.     private void appendFields(final Class<?> clazz) {
  179.         for (final Field field : FieldUtils.getAllFields(clazz)) {
  180.             if (accept(field)) {
  181.                 try {
  182.                     diffBuilder.append(field.getName(), readField(field, getLeft()), readField(field, getRight()));
  183.                 } catch (final IllegalAccessException e) {
  184.                     // this can't happen. Would get a Security exception instead
  185.                     // throw a runtime exception in case the impossible happens.
  186.                     throw new IllegalArgumentException("Unexpected IllegalAccessException: " + e.getMessage(), e);
  187.                 }
  188.             }
  189.         }
  190.     }

  191.     @Override
  192.     public DiffResult<T> build() {
  193.         if (getLeft().equals(getRight())) {
  194.             return diffBuilder.build();
  195.         }

  196.         appendFields(getLeft().getClass());
  197.         return diffBuilder.build();
  198.     }

  199.     /**
  200.      * Gets the field names that should be excluded from the diff.
  201.      *
  202.      * @return Returns the excludeFieldNames.
  203.      * @since 3.13.0
  204.      */
  205.     public String[] getExcludeFieldNames() {
  206.         return this.excludeFieldNames.clone();
  207.     }

  208.     private T getLeft() {
  209.         return diffBuilder.getLeft();
  210.     }

  211.     private T getRight() {
  212.         return diffBuilder.getRight();
  213.     }

  214.     private Object readField(final Field field, final Object target) throws IllegalAccessException {
  215.         return FieldUtils.readField(field, target, true);
  216.     }

  217.     /**
  218.      * Sets the field names to exclude.
  219.      *
  220.      * @param excludeFieldNames The field names to exclude from the diff or {@code null}.
  221.      * @return {@code this}
  222.      * @since 3.13.0
  223.      * @deprecated Use {@link Builder#setExcludeFieldNames(String[])}.
  224.      */
  225.     @Deprecated
  226.     public ReflectionDiffBuilder<T> setExcludeFieldNames(final String... excludeFieldNames) {
  227.         this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames);
  228.         return this;
  229.     }

  230. }