/** * Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.util.beancompare; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import org.joda.beans.Bean; import org.joda.beans.JodaBeanUtils; import org.joda.beans.MetaProperty; import com.google.common.collect.ImmutableList; import com.opengamma.util.ArgumentChecker; /** * Compares two Joda beans of the same class and returns details of any differences. */ public class BeanCompare { /** Comparators for bean properties keyed on the property they can compare. */ private final Map<MetaProperty<?>, Comparator<Object>> _propertyComparators; private final Map<Class<?>, Comparator<Object>> _typeComparators; //------------------------------------------------------------------------- /** * Checks if two beans are equal ignoring one or more properties. * * @param bean1 the first bean, not null * @param bean2 the second bean, not null * @param properties the properties to ignore, not null * @return true if equal * @deprecated Use JodaBeanUtils.equalIgnoring */ @Deprecated public static boolean equalIgnoring(Bean bean1, Bean bean2, MetaProperty<?>... properties) { return JodaBeanUtils.equalIgnoring(bean1, bean2, properties); } //------------------------------------------------------------------------- /** * Creates a new instance that uses the default comparison logic when comparing bean property values. */ public BeanCompare() { this(Collections.<MetaProperty<?>, Comparator<Object>>emptyMap(), Collections.<Class<?>, Comparator<Object>>emptyMap()); } /** * Creates a new instance which uses the {@link java.util.Comparator} instances in {@code propertyComparators} * and {@code typeComparators} when comparing bean property values. If there is no comparator for the property or * type being compared the default comparison logic is used. * * @param propertyComparators comparators used for comparing properties keyed by the property they apply to * @param typeComparators comparators used for comparing properties keyed by the type they apply to */ public BeanCompare( Map<MetaProperty<?>, Comparator<Object>> propertyComparators, Map<Class<?>, Comparator<Object>> typeComparators) { ArgumentChecker.notNull(propertyComparators, "propertyComparators"); ArgumentChecker.notNull(typeComparators, "typeComparators"); _propertyComparators = propertyComparators; _typeComparators = typeComparators; } /** * Compares two beans of the same class and returns details of any differences. * If any of the bean properties* are beans themselves they are compared recursively. * * @param bean1 the first bean, not null * @param bean2 the second bbean of the same type, not null * @return the differences between the beans or an empty list if they are identical * @throws IllegalArgumentException if the beans' classes are different */ public List<BeanDifference<?>> compare(Bean bean1, Bean bean2) { ArgumentChecker.notNull(bean1, "bean1"); ArgumentChecker.notNull(bean2, "bean2"); if (!sameClass(bean1, bean2)) { throw new IllegalArgumentException("Beans must be of the same class. bean1 class: " + bean1.getClass() + ", bean2 class: " + bean2.getClass()); } return compare(bean1, bean2, Collections.<MetaProperty<?>>emptyList()); } /** * Compares two beans. * * @param path the properties required to get from the root beans to the current beans, an * empty list if the current beans are the root beans */ private List<BeanDifference<?>> compare(Bean bean1, Bean bean2, List<MetaProperty<?>> path) { Iterable<MetaProperty<?>> properties = bean1.metaBean().metaPropertyIterable(); List<BeanDifference<?>> differences = new ArrayList<BeanDifference<?>>(); for (MetaProperty<?> property : properties) { Object value1 = property.get(bean1); Object value2 = property.get(bean2); if (value1 instanceof Bean && value2 instanceof Bean && sameClass(value1, value2)) { Comparator<Object> comparator = _propertyComparators.get(property); if (comparator == null || comparator.compare(value1, value2) != 0) { List<MetaProperty<?>> newPath = ImmutableList.<MetaProperty<?>>builder().addAll(path).add(property).build(); differences.addAll(compare(((Bean) value1), ((Bean) value2), newPath)); } } else { if (!equal(property, value1, value2)) { differences.add(new BeanDifference<Object>(property, value1, value2, path)); } } } return differences; } /** * Checks if two values of a property are equal. If there is a custom comparator for {@code property} it * is used for the comparison, otherwise the default comparison logic is used to compare the * values ({@link org.joda.beans.JodaBeanUtils#equal(Object, Object)}. * * @param property the property whose values are being tested for equality * @param value1 the first property value * @param value2 the second property value * @return true if the values are equal according to the comparator for {@code property} or * {@link org.joda.beans.JodaBeanUtils#equal(Object, Object)} if there is no comparator for the property */ private boolean equal(MetaProperty<?> property, Object value1, Object value2) { Comparator<Object> comparator = _propertyComparators.get(property); if (comparator == null) { comparator = _typeComparators.get(property.propertyType()); } if (value1 == null || value2 == null || comparator == null) { return JodaBeanUtils.equal(value1, value2); } else { return comparator.compare(value1, value2) == 0; } } private static boolean sameClass(Object value1, Object value2) { return value1.getClass().equals(value2.getClass()); } }