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