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(properties="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}