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.juneau.marshall.utils;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.util.*;
023
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.collections.*;
027
028/**
029 * Utility class for comparing two versions of a POJO.
030 *
031 * <p>
032 *
033 * <p class='bjava'>
034 *    <jc>// Two beans to compare.</jc>
035 *    MyBean <jv>bean1</jv>, <jv>bean2</jv>;
036 *
037 * <jc>// Get differences.</jc>
038 *    BeanDiff <jv>beanDiff</jv> = BeanDiff.<jsm>create</jsm>(<jv>bean1</jv>, <jv>bean2</jv>).exclude(<js>"fooProperty"</js>).build();
039 *
040 *    <jc>// Check for differences.</jc>
041 *    <jk>boolean</jk> <jv>hasDiff</jv> = <jv>beanDiff</jv>.hasDiffs();
042 *
043 *    JsonMap <jv>v1Diffs</jv> = <jv>beanDiff</jv>.getV1();  <jc>// Get version 1 differences.</jc>
044 *    JsonMap <jv>v2Diffs</jv> = <jv>beanDiff</jv>.getV2();  <jc>// Get version 2 differences.</jc>
045 *
046 *    <jc>// Display differences.</jc>
047 *    System.<jsf>err</jsf>.println(<jv>beanDiff</jv>);
048 * </p>
049 *
050 */
051@SuppressWarnings({
052   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
053})
054public class BeanDiff {
055
056   /**
057    * Builder class.
058    *
059    * @param <T> The bean type.
060    */
061   public static class Builder<T> {
062      T first;
063      T second;
064      MarshallingContext marshallingContext = MarshallingContext.DEFAULT;
065      Set<String> include;
066      Set<String> exclude;
067
068      /**
069       * Specifies the bean context to use for introspecting beans.
070       *
071       * <p>
072       * If not specified, uses {@link MarshallingContext#DEFAULT}.
073       *
074       * @param value The bean context to use for introspecting beans.
075       * @return This object.
076       */
077      public Builder<T> marshallingContext(MarshallingContext value) {
078         marshallingContext = value;
079         return this;
080      }
081
082      /**
083       * Build the differences.
084       *
085       * @return A new {@link BeanDiff} object.
086       */
087      public BeanDiff build() {
088         return new BeanDiff(marshallingContext, first, second, include, exclude);
089      }
090
091      /**
092       * Specifies the properties to exclude from the comparison.
093       *
094       * @param properties The properties to exclude from the comparison.
095       * @return This object.
096       */
097      public Builder<T> exclude(Set<String> properties) {
098         exclude = properties;
099         return this;
100      }
101
102      /**
103       * Specifies the properties to exclude from the comparison.
104       *
105       * @param properties The properties to exclude from the comparison.
106       * @return This object.
107       */
108      public Builder<T> exclude(String...properties) {
109         exclude = set(properties);
110         return this;
111      }
112
113      /**
114       * Specifies the first bean to compare.
115       *
116       * @param value The first bean to compare.
117       * @return This object.
118       */
119      public Builder<T> first(T value) {
120         first = value;
121         return this;
122      }
123
124      /**
125       * Specifies the properties to include in the comparison.
126       *
127       * <p>
128       * If not specified, compares all properties.
129       *
130       * @param properties The properties to include in the comparison.
131       * @return This object.
132       */
133      public Builder<T> include(Set<String> properties) {
134         include = properties;
135         return this;
136      }
137
138      /**
139       * Specifies the properties to include in the comparison.
140       *
141       * <p>
142       * If not specified, compares all properties.
143       *
144       * @param properties The properties to include in the comparison.
145       * @return This object.
146       */
147      public Builder<T> include(String...properties) {
148         include = set(properties);
149         return this;
150      }
151
152      /**
153       * Specifies the second bean to compare.
154       *
155       * @param value The first bean to compare.
156       * @return This object.
157       */
158      public Builder<T> second(T value) {
159         second = value;
160         return this;
161      }
162   }
163
164   /**
165    * Create a new builder for this class.
166    *
167    * @param <T> The bean types.
168    * @param first The first bean to compare.
169    * @param second The second bean to compare.
170    * @return A new builder.
171    */
172   public static <T> Builder<T> create(T first, T second) {
173      return new Builder<T>().first(first).second(second);
174   }
175
176   private final JsonMap v1 = new JsonMap();
177   private final JsonMap v2 = new JsonMap();
178
179   /**
180    * Constructor.
181    *
182    * @param <T> The bean types.
183    * @param bc The bean context to use for comparing beans.
184    * @param first The first bean to compare.  Can be <jk>null</jk> (treated as an empty bean with no properties).
185    * @param second The second bean to compare.  Can be <jk>null</jk> (treated as an empty bean with no properties).
186    * @param include
187    *    Optional properties to include in the comparison.
188    *    <br>If <jk>null</jk>, all properties are included.
189    * @param exclude
190    *    Optional properties to exclude in the comparison.
191    *    <br>If <jk>null</jk>, no properties are excluded.
192    */
193   @SuppressWarnings({
194      "java:S3776", // Cognitive complexity acceptable for this specific logic
195      "null" // Null handling verified by context or framework
196   })
197   public <T> BeanDiff(MarshallingContext bc, T first, T second, Set<String> include, Set<String> exclude) {
198      if (first == null && second == null)
199         return;
200      var bm1 = first == null ? null : bc.toBeanMap(first);
201      var bm2 = second == null ? null : bc.toBeanMap(second);
202      var keys = nn(bm1) ? bm1.keySet() : bm2.keySet();
203      keys.forEach(k -> {
204         if ((include == null || include.contains(k)) && (exclude == null || ! exclude.contains(k))) {
205            var o1 = bm1 == null ? null : bm1.get(k);
206            var o2 = bm2 == null ? null : bm2.get(k);
207            if (neq(o1, o2)) {
208               if (nn(o1))
209                  v1.put(k, o1);
210               if (nn(o2))
211                  v2.put(k, o2);
212            }
213         }
214      });
215   }
216
217   /**
218    * Returns the differences in the first bean.
219    *
220    * @return The differences in the first bean.
221    */
222   public JsonMap getV1() { return v1; }
223
224   /**
225    * Returns the differences in the second bean.
226    *
227    * @return The differences in the second bean.
228    */
229   public JsonMap getV2() { return v2; }
230
231   /**
232    * Returns <jk>true</jk> if the beans had differences.
233    *
234    * @return <jk>true</jk> if the beans had differences.
235    */
236   public boolean hasDiffs() {
237      return v1.size() > 0 || v2.size() > 0;
238   }
239
240   protected FluentMap<String,Object> properties() {
241      // @formatter:off
242      return mapb_so().buildFluent()
243         .a("v1", v1)
244         .a("v2", v2);
245      // @formatter:on
246   }
247
248   @Override
249   public String toString() {
250      return r(properties());
251   }
252}