/** * Returns a map of properties that differ (via {@link Object#equals(Object)}) between two * AutoBeans. The keys are property names and the values are the value of the property in <code>b * </code>. Properties present in <code>a</code> but missing in <code>b</code> will be represented * by <code>null</code> values. This implementation will compare AutoBeans of different * parameterizations, although the diff produced is likely meaningless. * * <p>This will work for both simple and wrapper AutoBeans. * * @param a an {@link AutoBean} * @param b an {@link AutoBean} * @return a {@link Map} of differing properties */ public static Map<String, Object> diff(AutoBean<?> a, AutoBean<?> b) { // Fast check for comparing an object to itself if (a == b) { return Collections.emptyMap(); } final Map<String, Object> toReturn = getAllProperties(b); // Remove the entries that are equal, adding nulls for missing properties a.accept( new AutoBeanVisitor() { @Override public boolean visitReferenceProperty( String propertyName, AutoBean<?> previousValue, PropertyContext ctx) { if (toReturn.containsKey(propertyName)) { if (equal(propertyName, previousValue)) { // No change toReturn.remove(propertyName); } } else { // The predecessor has a value that this object doesn't. toReturn.put(propertyName, null); } return false; } @Override public boolean visitValueProperty( String propertyName, Object previousValue, PropertyContext ctx) { if (toReturn.containsKey(propertyName)) { if (equal(propertyName, previousValue)) { // No change toReturn.remove(propertyName); } } else { // The predecessor has a value that this object doesn't. toReturn.put(propertyName, null); } return false; } private boolean equal(String propertyName, AutoBean<?> previousValue) { return previousValue == null && toReturn.get(propertyName) == null || previousValue != null && equal(propertyName, previousValue.as()); } private boolean equal(String propertyName, Object previousValue) { Object currentValue = toReturn.get(propertyName); return previousValue == null && currentValue == null || previousValue != null && previousValue.equals(currentValue); } }); return toReturn; }
/** * If a comparison between two AutoBeans is currently pending, this method will skip their * comparison. */ private static boolean sameOrEquals( AutoBean<?> value, AutoBean<?> otherValue, Map<PendingComparison, Comparison> pending) { if (value == otherValue) { // Simple case return true; } else if (!value.getType().equals(otherValue.getType())) { // Beans of different types return false; } /* * The PendingComparison key allows us to break reference cycles when * crawling the graph. Since the entire operation is essentially a * concatenated && operation, it's ok to speculatively return true for * repeated a.equals(b) tests. */ PendingComparison key = new PendingComparison(value, otherValue); Comparison previous = pending.get(key); if (previous == null) { // Prevent the same comparison from being made pending.put(key, Comparison.PENDING); // Compare each property Map<String, Object> beanProperties = AutoBeanUtils.getAllProperties(value); Map<String, Object> otherProperties = AutoBeanUtils.getAllProperties(otherValue); for (Map.Entry<String, Object> entry : beanProperties.entrySet()) { Object property = entry.getValue(); Object otherProperty = otherProperties.get(entry.getKey()); if (!sameOrEquals(property, otherProperty, pending)) { pending.put(key, Comparison.FALSE); return false; } } pending.put(key, Comparison.TRUE); return true; } else { // Return true for TRUE or PENDING return !Comparison.FALSE.equals(previous); } }
/** * Returns a map that is a copy of the properties contained in an AutoBean. The returned map is * mutable, but editing it will not have any effect on the bean that produced it. * * @param bean an {@link AutoBean} * @return a {@link Map} of the bean's properties */ public static Map<String, Object> getAllProperties(AutoBean<?> bean) { final Map<String, Object> toReturn = new LinkedHashMap<String, Object>(); // Look at the previous value of all properties bean.accept( new AutoBeanVisitor() { @Override public boolean visitReferenceProperty( String propertyName, AutoBean<?> value, PropertyContext ctx) { toReturn.put(propertyName, value == null ? null : value.as()); return false; } @Override public boolean visitValueProperty( String propertyName, Object value, PropertyContext ctx) { toReturn.put(propertyName, value); return false; } }); return toReturn; }