com.google.gwt.autobean.shared.AutoBeanUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.autobean.shared.AutoBeanUtils.java

Source

/*
 * 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.
 *
 * <p><span style='color:red'>AutoBeans has moved to
 * <code>com.google.web.bindery.autobeans</code>.  This package will be
 * removed in a future version of GWT.</span></p>
 */
@Deprecated
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() {
    }
}