com.google.web.bindery.autobean.shared.AutoBeanUtils Maven / Gradle / Ivy
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.web.bindery.autobean.shared;
import com.google.gwt.core.client.impl.WeakMapping;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Utility methods for working with AutoBeans.
*/
public final class AutoBeanUtils {
/*
* TODO(bobv): Make Comparison a real type that holds a map contain the diff
* between the two objects. Then export a Map of PendingComparison to
* Comparisons as a public API to make it easy for developers to perform deep
* diffs across a graph structure.
*
* Three-way merge...
*/
private enum Comparison {
TRUE, FALSE, PENDING;
}
/**
* A Pair where order does not matter and the objects are compared by
* identity.
*/
private static class PendingComparison {
private final AutoBean> a;
private final AutoBean> b;
private final int hashCode;
public PendingComparison(AutoBean> a, AutoBean> b) {
this.a = a;
this.b = b;
// Don't make relatively prime since order does not matter
hashCode = System.identityHashCode(a) + System.identityHashCode(b);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof PendingComparison)) {
return false;
}
PendingComparison other = (PendingComparison) o;
return a == other.a && b == other.b || // Direct match
a == other.b && b == other.a; // Swapped
}
@Override
public int hashCode() {
return hashCode;
}
}
/**
* Compare two graphs of AutoBeans based on values.
*
*
* - AutoBeans are compared based on type and property values
* - Lists are compared with element-order equality
* - Sets and all other Collection types are compare with bag equality
* - Maps are compared as a lists of keys-value pairs
* - {@link Splittable Splittables} are compared by value
*
*
* This will work for both simple and wrapper AutoBeans.
*
* This method may crawl the entire object graph reachable from the input
* parameters and may be arbitrarily expensive to compute.
*
* @param a an {@link AutoBean}
* @param b an {@link AutoBean}
* @return {@code false} if any values in the graph reachable through
* a
are different from those reachable from
* b
*/
public static boolean deepEquals(AutoBean> a, AutoBean> b) {
return sameOrEquals(a, b, new HashMap());
}
/**
* 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 b
. Properties present in
* a
but missing in b
will be represented by
* null
values. This implementation will compare AutoBeans of
* different parameterizations, although the diff produced is likely
* meaningless.
*
* 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 diff(AutoBean> a, AutoBean> b) {
// Fast check for comparing an object to itself
if (a == b) {
return Collections.emptyMap();
}
final Map 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;
}
/**
* 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 getAllProperties(AutoBean> bean) {
final Map toReturn = new LinkedHashMap();
// 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;
}
/**
* Return the single AutoBean wrapper that is observing the delegate object or
* {@code null} if the parameter is {@code null}or not wrapped by an AutoBean.
*
* @param delegate a delegate object, or {@code null}
* @return the {@link AutoBean} wrapper for the delegate, or {@code null}
*/
@SuppressWarnings("unchecked")
public static AutoBean getAutoBean(U delegate) {
return delegate == null ? null : (AutoBean) WeakMapping.get(delegate, AutoBean.class
.getName());
}
/**
* Compare two AutoBeans, this method has the type fan-out.
*/
static boolean sameOrEquals(Object value, Object otherValue,
Map pending) {
if (value == otherValue) {
// Fast exit
return true;
}
if (value instanceof Collection> && otherValue instanceof Collection>) {
// Check collections
return sameOrEquals((Collection>) value, (Collection>) otherValue, pending, null);
}
if (value instanceof Map, ?> && otherValue instanceof Map, ?>) {
// Check maps
return sameOrEquals((Map, ?>) value, (Map, ?>) otherValue, pending);
}
if (value instanceof Splittable && otherValue instanceof Splittable) {
return sameOrEquals((Splittable) value, (Splittable) otherValue, pending);
}
// Possibly substitute the AutoBean for its shim
{
AutoBean> maybeValue = AutoBeanUtils.getAutoBean(value);
AutoBean> maybeOther = AutoBeanUtils.getAutoBean(otherValue);
if (maybeValue != null && maybeOther != null) {
value = maybeValue;
otherValue = maybeOther;
}
}
if (value instanceof AutoBean> && otherValue instanceof AutoBean>) {
// Check ValueProxies
return sameOrEquals((AutoBean>) value, (AutoBean>) otherValue, pending);
}
if (value == null ^ otherValue == null) {
// One is null, the other isn't
return false;
}
if (value != null && !value.equals(otherValue)) {
// Regular object equality
return false;
}
return true;
}
/**
* If a comparison between two AutoBeans is currently pending, this method
* will skip their comparison.
*/
private static boolean sameOrEquals(AutoBean> value, AutoBean> otherValue,
Map 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 beanProperties = AutoBeanUtils.getAllProperties(value);
Map otherProperties = AutoBeanUtils.getAllProperties(otherValue);
for (Map.Entry 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);
}
}
/**
* Compare two collections by size, then by contents. List comparisons will
* preserve order. All other collections will be treated with bag semantics.
*/
private static boolean sameOrEquals(Collection> collection, Collection> otherCollection,
Map pending, Map