blob: ed1930f316eeaca281ccd3535070502940516dc4 [file] [log] [blame]
/*
* 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.gwt.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.
* <p>
* <ul>
* <li>AutoBeans are compared based on type and property values</li>
* <li>Lists are compared with element-order equality</li>
* <li>Sets and all other Collection types are compare with bag equality</li>
* <li>Maps are compared as a lists of keys-value pairs</li>
* <li>{@link Splittable Splittables} are compared by value</li>
* </ul>
* <p>
* This will work for both simple and wrapper AutoBeans.
* <p>
* 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
* <code>a</code> are different from those reachable from
* <code>b</code>
*/
public static boolean deepEquals(AutoBean<?> a, AutoBean<?> b) {
return sameOrEquals(a, b, new HashMap<PendingComparison, Comparison>());
}
/**
* 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.equals(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;
}
/**
* 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;
}
/**
* 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 <T, U extends T> AutoBean<T> getAutoBean(U delegate) {
return delegate == null ? null : (AutoBean<T>) WeakMapping.get(delegate,
AutoBean.class.getName());
}
/**
* Compare two AutoBeans, this method has the type fan-out.
*/
static boolean sameOrEquals(Object value, Object otherValue,
Map<PendingComparison, Comparison> 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<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);
}
}
/**
* 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<PendingComparison, Comparison> pending, Map<Object, Object> pairs) {
if (collection.size() != otherCollection.size()
|| !collection.getClass().equals(otherCollection.getClass())) {
return false;
}
if (collection instanceof List<?>) {
// Lists we can simply iterate over
Iterator<?> it = collection.iterator();
Iterator<?> otherIt = otherCollection.iterator();
while (it.hasNext()) {
assert otherIt.hasNext();
Object element = it.next();
Object otherElement = otherIt.next();
if (!sameOrEquals(element, otherElement, pending)) {
return false;
}
if (pairs != null) {
pairs.put(element, otherElement);
}
}
} else {
// Do an n*m comparison on any other collection type
List<Object> values = new ArrayList<Object>(collection);
List<Object> otherValues = new ArrayList<Object>(otherCollection);
it : for (Iterator<Object> it = values.iterator(); it.hasNext();) {
Object value = it.next();
for (Iterator<Object> otherIt = otherValues.iterator(); otherIt.hasNext();) {
Object otherValue = otherIt.next();
if (sameOrEquals(value, otherValue, pending)) {
if (pairs != null) {
pairs.put(value, otherValue);
}
// If a match is found, remove both values from their lists
it.remove();
otherIt.remove();
continue it;
}
}
// A match for the value wasn't found
return false;
}
assert values.isEmpty() && otherValues.isEmpty();
}
return true;
}
/**
* Compare two Maps by size, and key-value pairs.
*/
private static boolean sameOrEquals(Map<?, ?> map, Map<?, ?> otherMap,
Map<PendingComparison, Comparison> pending) {
if (map.size() != otherMap.size()) {
return false;
}
Map<Object, Object> pairs = new IdentityHashMap<Object, Object>();
if (!sameOrEquals(map.keySet(), otherMap.keySet(), pending, pairs)) {
return false;
}
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object otherValue = otherMap.get(pairs.get(entry.getKey()));
if (!sameOrEquals(entry.getValue(), otherValue, pending)) {
return false;
}
}
return true;
}
/**
* Compare Splittables by kind and values.
*/
private static boolean sameOrEquals(Splittable value, Splittable otherValue,
Map<PendingComparison, Comparison> pending) {
if (value == otherValue) {
return true;
}
// Strings
if (value.isString()) {
if (!otherValue.isString()) {
return false;
}
return value.asString().equals(otherValue.asString());
}
// Arrays
if (value.isIndexed()) {
if (!otherValue.isIndexed()) {
return false;
}
if (value.size() != otherValue.size()) {
return false;
}
for (int i = 0, j = value.size(); i < j; i++) {
if (!sameOrEquals(value.get(i), otherValue.get(i), pending)) {
return false;
}
}
return true;
}
// Objects
if (value.isKeyed()) {
if (!otherValue.isKeyed()) {
return false;
}
/*
* We want to treat a missing property key as a null value, so we can't
* just compare the key lists.
*/
List<String> keys = value.getPropertyKeys();
for (String key : keys) {
if (value.isNull(key)) {
// If value['foo'] is null, other['foo'] must also be null
if (!otherValue.isNull(key)) {
return false;
}
} else if (otherValue.isNull(key)
|| !sameOrEquals(value.get(key), otherValue.get(key), pending)) {
return false;
}
}
// Look at keys only in otherValue, and ensure nullness
List<String> otherKeys = new ArrayList<String>(
otherValue.getPropertyKeys());
otherKeys.removeAll(keys);
for (String key : otherKeys) {
if (!value.isNull(key)) {
return false;
}
}
return true;
}
// Unexpected
throw new UnsupportedOperationException("Splittable of unknown type");
}
/**
* Utility class.
*/
private AutoBeanUtils() {
}
}