001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3.builder;
018
019import java.lang.reflect.Field;
020import java.lang.reflect.Modifier;
021import java.util.Arrays;
022
023import org.apache.commons.lang3.ArraySorter;
024import org.apache.commons.lang3.ArrayUtils;
025import org.apache.commons.lang3.ClassUtils;
026import org.apache.commons.lang3.reflect.FieldUtils;
027
028/**
029 * Assists in implementing {@link Diffable#diff(Object)} methods.
030 *
031 * <p>
032 * All non-static, non-transient fields (including inherited fields) of the objects to diff are discovered using reflection and compared for differences.
033 * </p>
034 *
035 * <p>
036 * To use this class, write code as follows:
037 * </p>
038 *
039 * <pre>{@code
040 * public class Person implements Diffable<Person> {
041 *   String name;
042 *   int age;
043 *   boolean smoker;
044 *   ...
045 *
046 *   public DiffResult diff(Person obj) {
047 *     // No need for null check, as NullPointerException correct if obj is null
048 *     return new ReflectionDiffBuilder.<Person>builder()
049 *       .setDiffBuilder(DiffBuilder.<Person>builder()
050 *           .setLeft(this)
051 *           .setRight(obj)
052 *           .setStyle(ToStringStyle.SHORT_PREFIX_STYLE)
053 *           .build())
054 *       .setExcludeFieldNames("userName", "password")
055 *       .build();
056 *   }
057 * }
058 * }</pre>
059 *
060 * <p>
061 * The {@link ToStringStyle} passed to the constructor is embedded in the returned {@link DiffResult} and influences the style of the
062 * {@code DiffResult.toString()} method. This style choice can be overridden by calling {@link DiffResult#toString(ToStringStyle)}.
063 * </p>
064 * <p>
065 * See {@link DiffBuilder} for a non-reflection based version of this class.
066 * </p>
067 *
068 * @param <T> type of the left and right object to diff.
069 * @see Diffable
070 * @see Diff
071 * @see DiffResult
072 * @see ToStringStyle
073 * @see DiffBuilder
074 * @since 3.6
075 */
076public class ReflectionDiffBuilder<T> implements Builder<DiffResult<T>> {
077
078    /**
079     * Constructs a new instance.
080     *
081     * @param <T> type of the left and right object.
082     * @since 3.15.0
083     */
084    public static final class Builder<T> {
085
086        private String[] excludeFieldNames = ArrayUtils.EMPTY_STRING_ARRAY;
087        private DiffBuilder<T> diffBuilder;
088
089        /**
090         * Constructs a new instance.
091         */
092        public Builder() {
093            // empty
094        }
095
096        /**
097         * Builds a new configured {@link ReflectionDiffBuilder}.
098         *
099         * @return a new configured {@link ReflectionDiffBuilder}.
100         */
101        public ReflectionDiffBuilder<T> build() {
102            return new ReflectionDiffBuilder<>(diffBuilder, excludeFieldNames);
103        }
104
105        /**
106         * Sets the DiffBuilder.
107         *
108         * @param diffBuilder the DiffBuilder.
109         * @return {@code this} instance.
110         */
111        public Builder<T> setDiffBuilder(final DiffBuilder<T> diffBuilder) {
112            this.diffBuilder = diffBuilder;
113            return this;
114        }
115
116        /**
117         * Sets field names to exclude from output. Intended for fields like {@code "password"} or {@code "lastModificationDate"}.
118         *
119         * @param excludeFieldNames field names to exclude.
120         * @return {@code this} instance.
121         */
122        public Builder<T> setExcludeFieldNames(final String... excludeFieldNames) {
123            this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames);
124            return this;
125        }
126
127    }
128
129    /**
130     * Constructs a new {@link Builder}.
131     *
132     * @param <T> type of the left and right object.
133     * @return a new {@link Builder}.
134     * @since 3.15.0
135     */
136    public static <T> Builder<T> builder() {
137        return new Builder<>();
138    }
139
140    private static String[] toExcludeFieldNames(final String[] excludeFieldNames) {
141        if (excludeFieldNames == null) {
142            return ArrayUtils.EMPTY_STRING_ARRAY;
143        }
144        // clone and remove nulls
145        return ArraySorter.sort(ReflectionToStringBuilder.toNoNullStringArray(excludeFieldNames));
146    }
147
148    private final DiffBuilder<T> diffBuilder;
149
150    /**
151     * Field names to exclude from output. Intended for fields like {@code "password"} or {@code "lastModificationDate"}.
152     */
153    private String[] excludeFieldNames;
154
155    private ReflectionDiffBuilder(final DiffBuilder<T> diffBuilder, final String[] excludeFieldNames) {
156        this.diffBuilder = diffBuilder;
157        this.excludeFieldNames = excludeFieldNames;
158    }
159
160    /**
161     * Constructs a builder for the specified objects with the specified style.
162     *
163     * <p>
164     * 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
165     * {@link DiffResult} when {@link #build()} is executed.
166     * </p>
167     *
168     * @param left  {@code this} object.
169     * @param right the object to diff against.
170     * @param style the style will use when outputting the objects, {@code null} uses the default
171     * @throws IllegalArgumentException if {@code left} or {@code right} is {@code null}.
172     * @deprecated Use {@link Builder}.
173     */
174    @Deprecated
175    public ReflectionDiffBuilder(final T left, final T right, final ToStringStyle style) {
176        this(DiffBuilder.<T>builder().setLeft(left).setRight(right).setStyle(style).build(), null);
177    }
178
179    private boolean accept(final Field field) {
180        if (field.getName().indexOf(ClassUtils.INNER_CLASS_SEPARATOR_CHAR) != -1) {
181            return false;
182        }
183        if (Modifier.isTransient(field.getModifiers())) {
184            return false;
185        }
186        if (Modifier.isStatic(field.getModifiers())) {
187            return false;
188        }
189        if (this.excludeFieldNames != null && Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0) {
190            // Reject fields from the getExcludeFieldNames list.
191            return false;
192        }
193        return !field.isAnnotationPresent(DiffExclude.class);
194    }
195
196    private void appendFields(final Class<?> clazz) {
197        for (final Field field : FieldUtils.getAllFields(clazz)) {
198            if (accept(field)) {
199                try {
200                    diffBuilder.append(field.getName(), readField(field, getLeft()), readField(field, getRight()));
201                } catch (final IllegalAccessException e) {
202                    // this can't happen. Would get a Security exception instead
203                    // throw a runtime exception in case the impossible happens.
204                    throw new IllegalArgumentException("Unexpected IllegalAccessException: " + e.getMessage(), e);
205                }
206            }
207        }
208    }
209
210    @Override
211    public DiffResult<T> build() {
212        if (getLeft().equals(getRight())) {
213            return diffBuilder.build();
214        }
215
216        appendFields(getLeft().getClass());
217        return diffBuilder.build();
218    }
219
220    /**
221     * Gets the field names that should be excluded from the diff.
222     *
223     * @return Returns the excludeFieldNames.
224     * @since 3.13.0
225     */
226    public String[] getExcludeFieldNames() {
227        return this.excludeFieldNames.clone();
228    }
229
230    private T getLeft() {
231        return diffBuilder.getLeft();
232    }
233
234    private T getRight() {
235        return diffBuilder.getRight();
236    }
237
238    private Object readField(final Field field, final Object target) throws IllegalAccessException {
239        return FieldUtils.readField(field, target, true);
240    }
241
242    /**
243     * Sets the field names to exclude.
244     *
245     * @param excludeFieldNames The field names to exclude from the diff or {@code null}.
246     * @return {@code this}
247     * @since 3.13.0
248     * @deprecated Use {@link Builder#setExcludeFieldNames(String[])}.
249     */
250    @Deprecated
251    public ReflectionDiffBuilder<T> setExcludeFieldNames(final String... excludeFieldNames) {
252        this.excludeFieldNames = toExcludeFieldNames(excludeFieldNames);
253        return this;
254    }
255
256}