001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.utils;
014
015import java.util.*;
016
017import org.apache.juneau.*;
018import org.apache.juneau.annotation.Bean;
019import org.apache.juneau.internal.*;
020import org.apache.juneau.marshall.SimpleJson;
021
022/**
023 * Utility class for comparing two versions of a POJO.
024 *
025 * <p class='bpcode w800'>
026 *    <jc>// Two beans to compare.</jc>
027 *    MyBean bean1, bean2;
028 *
029 * <jc>// Get differences.</jc>
030 *    BeanDiff bf = BeanDiff.<jsm>create</jsm>(bean1, bean2).exclude(<js>"fooProperty"</js>).build();
031 *
032 *    <jc>// Check for differences.</jc>
033 *    <jk>boolean</jk> b = bf.hasDiffs();
034 *
035 *    ObjectMap v1 = bf.getV1();  <jc>// Get version 1 differences.</jc>
036 *    ObjectMap v2 = bf.getV2();  <jc>// Get version 2 differences.</jc>
037 *
038 *    <jc>// Display differences.</jc>
039 *    System.<jsf>err</jsf>.println(bf);
040 * </p>
041 */
042@Bean(bpi="v1,v2")
043public class BeanDiff {
044
045   private ObjectMap v1 = new ObjectMap(), v2 = new ObjectMap();
046
047   /**
048    * Constructor.
049    *
050    * @param bc The bean context to use for comparing beans.
051    * @param first The first bean to compare.
052    * @param second The second bean to compare.
053    * @param include
054    *    Optional properties to include in the comparison.
055    *    <br>If <jk>null</jk>, all properties are included.
056    * @param exclude
057    *    Optional properties to exclude in the comparison.
058    *    <br>If <jk>null</jk>, no properties are excluded.
059    */
060   @SuppressWarnings("null")
061   public <T> BeanDiff(BeanContext bc, T first, T second, Set<String> include, Set<String> exclude) {
062      if (first == null && second == null)
063         return;
064      BeanSession bs = bc.createBeanSession();
065      BeanMap<?> bm1 = first == null ? null : bs.toBeanMap(first);
066      BeanMap<?> bm2 = second == null ? null : bs.toBeanMap(second);
067      Set<String> keys = bm1 != null ? bm1.keySet() : bm2.keySet();
068      for (String k : keys) {
069         if ((include == null || include.contains(k)) && (exclude == null || ! exclude.contains(k))) {
070            Object o1 = bm1 == null ? null : bm1.get(k);
071            Object o2 = bm2 == null ? null : bm2.get(k);
072            if (! ObjectUtils.equals(o1, o2)) {
073               if (o1 != null)
074                  v1.put(k, o1);
075               if (o2 != null)
076                  v2.put(k, o2);
077            }
078         }
079      }
080   }
081
082   /**
083    * Create a new builder for this class.
084    *
085    * @param first The first bean to compare.
086    * @param second The second bean to compare.
087    * @return A new builder.
088    */
089   public static <T> Builder<T> create(T first, T second) {
090      return new Builder<T>().first(first).second(second);
091   }
092
093   /**
094    * Builder class.
095    *
096    * @param <T> The bean type.
097    */
098   public static class Builder<T> {
099      T first, second;
100      BeanContext beanContext = BeanContext.DEFAULT;
101      Set<String> include, exclude;
102
103      /**
104       * Specifies the first bean to compare.
105       *
106       * @param value The first bean to compare.
107       * @return This object (for method chaining).
108       */
109      public Builder<T> first(T value) {
110         this.first = value;
111         return this;
112      }
113
114      /**
115       * Specifies the second bean to compare.
116       *
117       * @param value The first bean to compare.
118       * @return This object (for method chaining).
119       */
120      public Builder<T> second(T value) {
121         this.second = value;
122         return this;
123      }
124
125      /**
126       * Specifies the bean context to use for introspecting beans.
127       *
128       * <p>
129       * If not specified, uses {@link BeanContext#DEFAULT}.
130       *
131       * @param value The bean context to use for introspecting beans.
132       * @return This object (for method chaining).
133       */
134      public Builder<T> beanContext(BeanContext value) {
135         this.beanContext = value;
136         return this;
137      }
138
139      /**
140       * Specifies the properties to include in the comparison.
141       *
142       * <p>
143       * If not specified, compares all properties.
144       *
145       * @param properties The properties to include in the comparison.
146       * @return This object (for method chaining).
147       */
148      public Builder<T> include(String...properties) {
149         include = new HashSet<>(Arrays.asList(properties));
150         return this;
151      }
152
153      /**
154       * Specifies the properties to include in the comparison.
155       *
156       * <p>
157       * If not specified, compares all properties.
158       *
159       * @param properties The properties to include in the comparison.
160       * @return This object (for method chaining).
161       */
162      public Builder<T> include(Set<String> properties) {
163         include = properties;
164         return this;
165      }
166
167      /**
168       * Specifies the properties to exclude from the comparison.
169       *
170       * @param properties The properties to exclude from the comparison.
171       * @return This object (for method chaining).
172       */
173      public Builder<T> exclude(String...properties) {
174         exclude = new HashSet<>(Arrays.asList(properties));
175         return this;
176      }
177
178      /**
179       * Specifies the properties to exclude from the comparison.
180       *
181       * @param properties The properties to exclude from the comparison.
182       * @return This object (for method chaining).
183       */
184      public Builder<T> exclude(Set<String> properties) {
185         exclude = properties;
186         return this;
187      }
188
189      /**
190       * Build the differences.
191       *
192       * @return A new {@link BeanDiff} object.
193       */
194      public BeanDiff build() {
195         return new BeanDiff(beanContext, first, second, include, exclude);
196      }
197
198   }
199
200   /**
201    * Returns <jk>true</jk> if the beans had differences.
202    *
203    * @return <jk>true</jk> if the beans had differences.
204    */
205   public boolean hasDiffs() {
206      return v1.size() > 0 || v2.size() > 0;
207   }
208
209   /**
210    * Returns the differences in the first bean.
211    *
212    * @return The differences in the first bean.
213    */
214   public ObjectMap getV1() {
215      return v1;
216   }
217
218   /**
219    * Returns the differences in the second bean.
220    *
221    * @return The differences in the second bean.
222    */
223   public ObjectMap getV2() {
224      return v2;
225   }
226
227   @Override
228   public String toString() {
229      return SimpleJson.DEFAULT.toString(this);
230   }
231}