| /* |
| * 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() { |
| } |
| } |