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}