001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.utils; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.util.*; 023 024import org.apache.juneau.commons.collections.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.collections.*; 027 028/** 029 * Utility class for comparing two versions of a POJO. 030 * 031 * <p> 032 * 033 * <p class='bjava'> 034 * <jc>// Two beans to compare.</jc> 035 * MyBean <jv>bean1</jv>, <jv>bean2</jv>; 036 * 037 * <jc>// Get differences.</jc> 038 * BeanDiff <jv>beanDiff</jv> = BeanDiff.<jsm>create</jsm>(<jv>bean1</jv>, <jv>bean2</jv>).exclude(<js>"fooProperty"</js>).build(); 039 * 040 * <jc>// Check for differences.</jc> 041 * <jk>boolean</jk> <jv>hasDiff</jv> = <jv>beanDiff</jv>.hasDiffs(); 042 * 043 * JsonMap <jv>v1Diffs</jv> = <jv>beanDiff</jv>.getV1(); <jc>// Get version 1 differences.</jc> 044 * JsonMap <jv>v2Diffs</jv> = <jv>beanDiff</jv>.getV2(); <jc>// Get version 2 differences.</jc> 045 * 046 * <jc>// Display differences.</jc> 047 * System.<jsf>err</jsf>.println(<jv>beanDiff</jv>); 048 * </p> 049 * 050 */ 051@SuppressWarnings({ 052 "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants 053}) 054public class BeanDiff { 055 056 /** 057 * Builder class. 058 * 059 * @param <T> The bean type. 060 */ 061 public static class Builder<T> { 062 T first; 063 T second; 064 MarshallingContext marshallingContext = MarshallingContext.DEFAULT; 065 Set<String> include; 066 Set<String> exclude; 067 068 /** 069 * Specifies the bean context to use for introspecting beans. 070 * 071 * <p> 072 * If not specified, uses {@link MarshallingContext#DEFAULT}. 073 * 074 * @param value The bean context to use for introspecting beans. 075 * @return This object. 076 */ 077 public Builder<T> marshallingContext(MarshallingContext value) { 078 marshallingContext = value; 079 return this; 080 } 081 082 /** 083 * Build the differences. 084 * 085 * @return A new {@link BeanDiff} object. 086 */ 087 public BeanDiff build() { 088 return new BeanDiff(marshallingContext, first, second, include, exclude); 089 } 090 091 /** 092 * Specifies the properties to exclude from the comparison. 093 * 094 * @param properties The properties to exclude from the comparison. 095 * @return This object. 096 */ 097 public Builder<T> exclude(Set<String> properties) { 098 exclude = properties; 099 return this; 100 } 101 102 /** 103 * Specifies the properties to exclude from the comparison. 104 * 105 * @param properties The properties to exclude from the comparison. 106 * @return This object. 107 */ 108 public Builder<T> exclude(String...properties) { 109 exclude = set(properties); 110 return this; 111 } 112 113 /** 114 * Specifies the first bean to compare. 115 * 116 * @param value The first bean to compare. 117 * @return This object. 118 */ 119 public Builder<T> first(T value) { 120 first = value; 121 return this; 122 } 123 124 /** 125 * Specifies the properties to include in the comparison. 126 * 127 * <p> 128 * If not specified, compares all properties. 129 * 130 * @param properties The properties to include in the comparison. 131 * @return This object. 132 */ 133 public Builder<T> include(Set<String> properties) { 134 include = properties; 135 return this; 136 } 137 138 /** 139 * Specifies the properties to include in the comparison. 140 * 141 * <p> 142 * If not specified, compares all properties. 143 * 144 * @param properties The properties to include in the comparison. 145 * @return This object. 146 */ 147 public Builder<T> include(String...properties) { 148 include = set(properties); 149 return this; 150 } 151 152 /** 153 * Specifies the second bean to compare. 154 * 155 * @param value The first bean to compare. 156 * @return This object. 157 */ 158 public Builder<T> second(T value) { 159 second = value; 160 return this; 161 } 162 } 163 164 /** 165 * Create a new builder for this class. 166 * 167 * @param <T> The bean types. 168 * @param first The first bean to compare. 169 * @param second The second bean to compare. 170 * @return A new builder. 171 */ 172 public static <T> Builder<T> create(T first, T second) { 173 return new Builder<T>().first(first).second(second); 174 } 175 176 private final JsonMap v1 = new JsonMap(); 177 private final JsonMap v2 = new JsonMap(); 178 179 /** 180 * Constructor. 181 * 182 * @param <T> The bean types. 183 * @param bc The bean context to use for comparing beans. 184 * @param first The first bean to compare. Can be <jk>null</jk> (treated as an empty bean with no properties). 185 * @param second The second bean to compare. Can be <jk>null</jk> (treated as an empty bean with no properties). 186 * @param include 187 * Optional properties to include in the comparison. 188 * <br>If <jk>null</jk>, all properties are included. 189 * @param exclude 190 * Optional properties to exclude in the comparison. 191 * <br>If <jk>null</jk>, no properties are excluded. 192 */ 193 @SuppressWarnings({ 194 "java:S3776", // Cognitive complexity acceptable for this specific logic 195 "null" // Null handling verified by context or framework 196 }) 197 public <T> BeanDiff(MarshallingContext bc, T first, T second, Set<String> include, Set<String> exclude) { 198 if (first == null && second == null) 199 return; 200 var bm1 = first == null ? null : bc.toBeanMap(first); 201 var bm2 = second == null ? null : bc.toBeanMap(second); 202 var keys = nn(bm1) ? bm1.keySet() : bm2.keySet(); 203 keys.forEach(k -> { 204 if ((include == null || include.contains(k)) && (exclude == null || ! exclude.contains(k))) { 205 var o1 = bm1 == null ? null : bm1.get(k); 206 var o2 = bm2 == null ? null : bm2.get(k); 207 if (neq(o1, o2)) { 208 if (nn(o1)) 209 v1.put(k, o1); 210 if (nn(o2)) 211 v2.put(k, o2); 212 } 213 } 214 }); 215 } 216 217 /** 218 * Returns the differences in the first bean. 219 * 220 * @return The differences in the first bean. 221 */ 222 public JsonMap getV1() { return v1; } 223 224 /** 225 * Returns the differences in the second bean. 226 * 227 * @return The differences in the second bean. 228 */ 229 public JsonMap getV2() { return v2; } 230 231 /** 232 * Returns <jk>true</jk> if the beans had differences. 233 * 234 * @return <jk>true</jk> if the beans had differences. 235 */ 236 public boolean hasDiffs() { 237 return v1.size() > 0 || v2.size() > 0; 238 } 239 240 protected FluentMap<String,Object> properties() { 241 // @formatter:off 242 return mapb_so().buildFluent() 243 .a("v1", v1) 244 .a("v2", v2); 245 // @formatter:on 246 } 247 248 @Override 249 public String toString() { 250 return r(properties()); 251 } 252}