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