All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.web.bindery.autobean.shared.AutoBeanUtils Maven / Gradle / Ivy

The newest version!
/*
 * 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 pairs) { if (collection.size() != otherCollection.size()) { 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 values = new ArrayList(collection); List otherValues = new ArrayList(otherCollection); it : for (Iterator it = values.iterator(); it.hasNext();) { Object value = it.next(); for (Iterator 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 pending) { if (map.size() != otherMap.size()) { return false; } Map pairs = new IdentityHashMap(); 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 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 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 otherKeys = new ArrayList(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() { } }