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>
026 *
027 * <p class='bcode w800'>
028 *    <jc>// Two beans to compare.</jc>
029 *    MyBean bean1, bean2;
030 *
031 * <jc>// Get differences.</jc>
032 *    BeanDiff bf = BeanDiff.<jsm>create</jsm>(bean1, bean2).exclude(<js>"fooProperty"</js>).build();
033 *
034 *    <jc>// Check for differences.</jc>
035 *    <jk>boolean</jk> b = bf.hasDiffs();
036 *
037 *    ObjectMap v1 = bf.getV1();  <jc>// Get version 1 differences.</jc>
038 *    ObjectMap v2 = bf.getV2();  <jc>// Get version 2 differences.</jc>
039 *
040 *    <jc>// Display differences.</jc>
041 *    System.<jsf>err</jsf>.println(bf);
042 * </p>
043 */
044@Bean(bpi="v1,v2")
045public class BeanDiff {
046
047   private ObjectMap v1 = new ObjectMap(), v2 = new ObjectMap();
048
049   /**
050    * Constructor.
051    *
052    * @param bc The bean context to use for comparing beans.
053    * @param first The first bean to compare.
054    * @param second The second bean to compare.
055    * @param include
056    *    Optional properties to include in the comparison.
057    *    <br>If <jk>null</jk>, all properties are included.
058    * @param exclude
059    *    Optional properties to exclude in the comparison.
060    *    <br>If <jk>null</jk>, no properties are excluded.
061    */
062   @SuppressWarnings("null")
063   public <T> BeanDiff(BeanContext bc, T first, T second, Set<String> include, Set<String> exclude) {
064      if (first == null && second == null)
065         return;
066      BeanSession bs = bc.createBeanSession();
067      BeanMap<?> bm1 = first == null ? null : bs.toBeanMap(first);
068      BeanMap<?> bm2 = second == null ? null : bs.toBeanMap(second);
069      Set<String> keys = bm1 != null ? bm1.keySet() : bm2.keySet();
070      for (String k : keys) {
071         if ((include == null || include.contains(k)) && (exclude == null || ! exclude.contains(k))) {
072            Object o1 = bm1 == null ? null : bm1.get(k);
073            Object o2 = bm2 == null ? null : bm2.get(k);
074            if (! ObjectUtils.equals(o1, o2)) {
075               if (o1 != null)
076                  v1.put(k, o1);
077               if (o2 != null)
078                  v2.put(k, o2);
079            }
080         }
081      }
082   }
083
084   /**
085    * Create a new builder for this class.
086    *
087    * @param first The first bean to compare.
088    * @param second The second bean to compare.
089    * @return A new builder.
090    */
091   public static <T> Builder<T> create(T first, T second) {
092      return new Builder<T>().first(first).second(second);
093   }
094
095   /**
096    * Builder class.
097    *
098    * @param <T> The bean type.
099    */
100   public static class Builder<T> {
101      T first, second;
102      BeanContext beanContext = BeanContext.DEFAULT;
103      Set<String> include, exclude;
104
105      /**
106       * Specifies the first bean to compare.
107       *
108       * @param value The first bean to compare.
109       * @return This object (for method chaining).
110       */
111      public Builder<T> first(T value) {
112         this.first = value;
113         return this;
114      }
115
116      /**
117       * Specifies the second bean to compare.
118       *
119       * @param value The first bean to compare.
120       * @return This object (for method chaining).
121       */
122      public Builder<T> second(T value) {
123         this.second = value;
124         return this;
125      }
126
127      /**
128       * Specifies the bean context to use for introspecting beans.
129       *
130       * <p>
131       * If not specified, uses {@link BeanContext#DEFAULT}.
132       *
133       * @param value The bean context to use for introspecting beans.
134       * @return This object (for method chaining).
135       */
136      public Builder<T> beanContext(BeanContext value) {
137         this.beanContext = value;
138         return this;
139      }
140
141      /**
142       * Specifies the properties to include in the comparison.
143       *
144       * <p>
145       * If not specified, compares all properties.
146       *
147       * @param properties The properties to include in the comparison.
148       * @return This object (for method chaining).
149       */
150      public Builder<T> include(String...properties) {
151         include = new HashSet<>(Arrays.asList(properties));
152         return this;
153      }
154
155      /**
156       * Specifies the properties to include in the comparison.
157       *
158       * <p>
159       * If not specified, compares all properties.
160       *
161       * @param properties The properties to include in the comparison.
162       * @return This object (for method chaining).
163       */
164      public Builder<T> include(Set<String> properties) {
165         include = properties;
166         return this;
167      }
168
169      /**
170       * Specifies the properties to exclude from the comparison.
171       *
172       * @param properties The properties to exclude from the comparison.
173       * @return This object (for method chaining).
174       */
175      public Builder<T> exclude(String...properties) {
176         exclude = new HashSet<>(Arrays.asList(properties));
177         return this;
178      }
179
180      /**
181       * Specifies the properties to exclude from the comparison.
182       *
183       * @param properties The properties to exclude from the comparison.
184       * @return This object (for method chaining).
185       */
186      public Builder<T> exclude(Set<String> properties) {
187         exclude = properties;
188         return this;
189      }
190
191      /**
192       * Build the differences.
193       *
194       * @return A new {@link BeanDiff} object.
195       */
196      public BeanDiff build() {
197         return new BeanDiff(beanContext, first, second, include, exclude);
198      }
199
200   }
201
202   /**
203    * Returns <jk>true</jk> if the beans had differences.
204    *
205    * @return <jk>true</jk> if the beans had differences.
206    */
207   public boolean hasDiffs() {
208      return v1.size() > 0 || v2.size() > 0;
209   }
210
211   /**
212    * Returns the differences in the first bean.
213    *
214    * @return The differences in the first bean.
215    */
216   public ObjectMap getV1() {
217      return v1;
218   }
219
220   /**
221    * Returns the differences in the second bean.
222    *
223    * @return The differences in the second bean.
224    */
225   public ObjectMap getV2() {
226      return v2;
227   }
228
229   @Override
230   public String toString() {
231      return SimpleJson.DEFAULT.toString(this);
232   }
233}