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.utils;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021
022import java.util.*;
023
024import org.apache.juneau.*;
025import org.apache.juneau.collections.*;
026import org.apache.juneau.commons.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 */
051public class BeanDiff {
052
053   /**
054    * Builder class.
055    *
056    * @param <T> The bean type.
057    */
058   public static class Builder<T> {
059      T first, second;
060      BeanContext beanContext = BeanContext.DEFAULT;
061      Set<String> include, exclude;
062
063      /**
064       * Specifies the bean context to use for introspecting beans.
065       *
066       * <p>
067       * If not specified, uses {@link BeanContext#DEFAULT}.
068       *
069       * @param value The bean context to use for introspecting beans.
070       * @return This object.
071       */
072      public Builder<T> beanContext(BeanContext value) {
073         beanContext = value;
074         return this;
075      }
076
077      /**
078       * Build the differences.
079       *
080       * @return A new {@link BeanDiff} object.
081       */
082      public BeanDiff build() {
083         return new BeanDiff(beanContext, first, second, include, exclude);
084      }
085
086      /**
087       * Specifies the properties to exclude from the comparison.
088       *
089       * @param properties The properties to exclude from the comparison.
090       * @return This object.
091       */
092      public Builder<T> exclude(Set<String> properties) {
093         exclude = properties;
094         return this;
095      }
096
097      /**
098       * Specifies the properties to exclude from the comparison.
099       *
100       * @param properties The properties to exclude from the comparison.
101       * @return This object.
102       */
103      public Builder<T> exclude(String...properties) {
104         exclude = set(properties);
105         return this;
106      }
107
108      /**
109       * Specifies the first bean to compare.
110       *
111       * @param value The first bean to compare.
112       * @return This object.
113       */
114      public Builder<T> first(T value) {
115         first = value;
116         return this;
117      }
118
119      /**
120       * Specifies the properties to include in the comparison.
121       *
122       * <p>
123       * If not specified, compares all properties.
124       *
125       * @param properties The properties to include in the comparison.
126       * @return This object.
127       */
128      public Builder<T> include(Set<String> properties) {
129         include = properties;
130         return this;
131      }
132
133      /**
134       * Specifies the properties to include in the comparison.
135       *
136       * <p>
137       * If not specified, compares all properties.
138       *
139       * @param properties The properties to include in the comparison.
140       * @return This object.
141       */
142      public Builder<T> include(String...properties) {
143         include = set(properties);
144         return this;
145      }
146
147      /**
148       * Specifies the second bean to compare.
149       *
150       * @param value The first bean to compare.
151       * @return This object.
152       */
153      public Builder<T> second(T value) {
154         second = value;
155         return this;
156      }
157   }
158
159   /**
160    * Create a new builder for this class.
161    *
162    * @param <T> The bean types.
163    * @param first The first bean to compare.
164    * @param second The second bean to compare.
165    * @return A new builder.
166    */
167   public static <T> Builder<T> create(T first, T second) {
168      return new Builder<T>().first(first).second(second);
169   }
170
171   private JsonMap v1 = new JsonMap(), v2 = new JsonMap();
172
173   /**
174    * Constructor.
175    *
176    * @param <T> The bean types.
177    * @param bc The bean context to use for comparing beans.
178    * @param first The first bean to compare.
179    * @param second The second bean to compare.
180    * @param include
181    *    Optional properties to include in the comparison.
182    *    <br>If <jk>null</jk>, all properties are included.
183    * @param exclude
184    *    Optional properties to exclude in the comparison.
185    *    <br>If <jk>null</jk>, no properties are excluded.
186    */
187   @SuppressWarnings("null")
188   public <T> BeanDiff(BeanContext bc, T first, T second, Set<String> include, Set<String> exclude) {
189      if (first == null && second == null)
190         return;
191      var bm1 = first == null ? null : bc.toBeanMap(first);
192      var bm2 = second == null ? null : bc.toBeanMap(second);
193      var keys = nn(bm1) ? bm1.keySet() : bm2.keySet();
194      keys.forEach(k -> {
195         if ((include == null || include.contains(k)) && (exclude == null || ! exclude.contains(k))) {
196            var o1 = bm1 == null ? null : bm1.get(k);
197            var o2 = bm2 == null ? null : bm2.get(k);
198            if (neq(o1, o2)) {
199               if (nn(o1))
200                  v1.put(k, o1);
201               if (nn(o2))
202                  v2.put(k, o2);
203            }
204         }
205      });
206   }
207
208   /**
209    * Returns the differences in the first bean.
210    *
211    * @return The differences in the first bean.
212    */
213   public JsonMap getV1() { return v1; }
214
215   /**
216    * Returns the differences in the second bean.
217    *
218    * @return The differences in the second bean.
219    */
220   public JsonMap getV2() { return v2; }
221
222   /**
223    * Returns <jk>true</jk> if the beans had differences.
224    *
225    * @return <jk>true</jk> if the beans had differences.
226    */
227   public boolean hasDiffs() {
228      return v1.size() > 0 || v2.size() > 0;
229   }
230
231   protected FluentMap<String,Object> properties() {
232      // @formatter:off
233      return mapb_so().buildFluent()
234         .a("v1", v1)
235         .a("v2", v2);
236      // @formatter:on
237   }
238
239   @Override
240   public String toString() {
241      return r(properties());
242   }
243}