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

org.joda.beans.JodaBeanUtils Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2001-present Stephen Colebourne
 *
 *  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 org.joda.beans;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;

import org.joda.beans.impl.direct.DirectBean;
import org.joda.beans.impl.flexi.FlexiBean;
import org.joda.collect.grid.DenseGrid;
import org.joda.collect.grid.Grid;
import org.joda.collect.grid.ImmutableGrid;
import org.joda.collect.grid.SparseGrid;
import org.joda.convert.StringConvert;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.LinkedHashMultiset;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.SortedMultiset;
import com.google.common.collect.Table;
import com.google.common.collect.TreeMultiset;

/**
 * A set of utilities to assist when working with beans and properties.
 */
public final class JodaBeanUtils {

    /**
     * The cache of meta-beans.
     */
    private static final StringConvert converter = new StringConvert();

    /**
     * Restricted constructor.
     */
    private JodaBeanUtils() {
    }

    //-----------------------------------------------------------------------
    /**
     * Obtains a meta-bean from a {@code Class}.
     * 

* This will return a meta-bean if it has been registered, or if the class * implements {@link DynamicBean} and has a no-args constructor. * Note that the common case where the meta-bean is registered by a static initializer is handled. * * @param cls the class to get the meta-bean for, not null * @return the meta-bean, not null * @throws IllegalArgumentException if unable to obtain the meta-bean * @deprecated Use {@link MetaBean#of(Class)} */ @Deprecated public static MetaBean metaBean(Class cls) { return MetaBean.of(cls); } /** * Registers a meta-bean. *

* This should be done for all beans in a static factory where possible. * If the meta-bean is dynamic, this method should not be called. * * @param metaBean the meta-bean, not null * @throws IllegalArgumentException if unable to register * @deprecated Use {@link MetaBean#register(MetaBean)} */ @Deprecated public static void registerMetaBean(MetaBean metaBean) { MetaBean.register(metaBean); } //----------------------------------------------------------------------- /** * Gets the standard string format converter. *

* This returns a singleton that may be mutated (holds a concurrent map). * New conversions should be registered at program startup. * * @return the standard string converter, not null */ public static StringConvert stringConverter() { return converter; } //----------------------------------------------------------------------- /** * Checks if two objects are equal handling null. * * @param obj1 the first object, may be null * @param obj2 the second object, may be null * @return true if equal */ public static boolean equal(Object obj1, Object obj2) { if (obj1 == obj2) { return true; } if (obj1 == null || obj2 == null) { return false; } if (obj1.getClass().isArray()) { return equalsArray(obj1, obj2); } // this does not handle arrays embedded in objects, such as in lists/maps // but you shouldn't use arrays like that, should you? return obj1.equals(obj2); } // extracted from equal(Object,Object) to aid hotspot inlining private static boolean equalsArray(Object obj1, Object obj2) { if (obj1 instanceof Object[] && obj2 instanceof Object[] && obj1.getClass() == obj2.getClass()) { return Arrays.deepEquals((Object[]) obj1, (Object[]) obj2); } else if (obj1 instanceof int[] && obj2 instanceof int[]) { return Arrays.equals((int[]) obj1, (int[]) obj2); } else if (obj1 instanceof long[] && obj2 instanceof long[]) { return Arrays.equals((long[]) obj1, (long[]) obj2); } else if (obj1 instanceof byte[] && obj2 instanceof byte[]) { return Arrays.equals((byte[]) obj1, (byte[]) obj2); } else if (obj1 instanceof double[] && obj2 instanceof double[]) { return Arrays.equals((double[]) obj1, (double[]) obj2); } else if (obj1 instanceof float[] && obj2 instanceof float[]) { return Arrays.equals((float[]) obj1, (float[]) obj2); } else if (obj1 instanceof char[] && obj2 instanceof char[]) { return Arrays.equals((char[]) obj1, (char[]) obj2); } else if (obj1 instanceof short[] && obj2 instanceof short[]) { return Arrays.equals((short[]) obj1, (short[]) obj2); } else if (obj1 instanceof boolean[] && obj2 instanceof boolean[]) { return Arrays.equals((boolean[]) obj1, (boolean[]) obj2); } // reachable if obj1 is an array and obj2 is not return false; } /** * Checks if two floats are equal based on identity. *

* This performs the same check as {@link Float#equals(Object)}. * * @param val1 the first value, may be null * @param val2 the second value, may be null * @return true if equal */ public static boolean equal(float val1, float val2) { return Float.floatToIntBits(val1) == Float.floatToIntBits(val2); } /** * Checks if two floats are equal within the specified tolerance. *

* Two NaN values are equal. Positive and negative infinity are only equal with themselves. * Otherwise, the difference between the values is compared to the tolerance. * * @param val1 the first value, may be null * @param val2 the second value, may be null * @param tolerance the tolerance used to compare equal * @return true if equal */ public static boolean equalWithTolerance(float val1, float val2, double tolerance) { return (Float.floatToIntBits(val1) == Float.floatToIntBits(val2)) || Math.abs(val1 - val2) <= tolerance; } /** * Checks if two doubles are equal based on identity. *

* This performs the same check as {@link Double#equals(Object)}. * * @param val1 the first value, may be null * @param val2 the second value, may be null * @return true if equal */ public static boolean equal(double val1, double val2) { return Double.doubleToLongBits(val1) == Double.doubleToLongBits(val2); } /** * Checks if two doubles are equal within the specified tolerance. *

* Two NaN values are equal. Positive and negative infinity are only equal with themselves. * Otherwise, the difference between the values is compared to the tolerance. * The tolerance is expected to be a finite value, not NaN or infinity. * * @param val1 the first value, may be null * @param val2 the second value, may be null * @param tolerance the tolerance used to compare equal * @return true if equal */ public static boolean equalWithTolerance(double val1, double val2, double tolerance) { return (Double.doubleToLongBits(val1) == Double.doubleToLongBits(val2)) || Math.abs(val1 - val2) <= tolerance; } /** * Returns a hash code for an object handling null. * * @param obj the object, may be null * @return the hash code */ public static int hashCode(Object obj) { if (obj == null) { return 0; } if (obj.getClass().isArray()) { return hashCodeArray(obj); } return obj.hashCode(); } // extracted from hashCode(Object) to aid hotspot inlining private static int hashCodeArray(Object obj) { if (obj instanceof Object[]) { return Arrays.deepHashCode((Object[]) obj); } else if (obj instanceof int[]) { return Arrays.hashCode((int[]) obj); } else if (obj instanceof long[]) { return Arrays.hashCode((long[]) obj); } else if (obj instanceof byte[]) { return Arrays.hashCode((byte[]) obj); } else if (obj instanceof double[]) { return Arrays.hashCode((double[]) obj); } else if (obj instanceof float[]) { return Arrays.hashCode((float[]) obj); } else if (obj instanceof char[]) { return Arrays.hashCode((char[]) obj); } else if (obj instanceof short[]) { return Arrays.hashCode((short[]) obj); } else if (obj instanceof boolean[]) { return Arrays.hashCode((boolean[]) obj); } // unreachable? return obj.hashCode(); } /** * Returns a hash code for a {@code boolean}. * * @param value the value to convert to a hash code * @return the hash code */ public static int hashCode(boolean value) { return value ? 1231 : 1237; } /** * Returns a hash code for an {@code int}. * * @param value the value to convert to a hash code * @return the hash code */ public static int hashCode(int value) { return value; } /** * Returns a hash code for a {@code long}. * * @param value the value to convert to a hash code * @return the hash code */ public static int hashCode(long value) { return (int) (value ^ value >>> 32); } /** * Returns a hash code for a {@code float}. * * @param value the value to convert to a hash code * @return the hash code */ public static int hashCode(float value) { return Float.floatToIntBits(value); } /** * Returns a hash code for a {@code double}. * * @param value the value to convert to a hash code * @return the hash code */ public static int hashCode(double value) { return hashCode(Double.doubleToLongBits(value)); } //----------------------------------------------------------------------- /** * Returns the {@code toString} value handling arrays. * * @param obj the object, may be null * @return the string, not null */ public static String toString(Object obj) { if (obj == null) { return "null"; } if (obj.getClass().isArray()) { return toStringArray(obj); } return obj.toString(); } // extracted from toString(Object) to aid hotspot inlining private static String toStringArray(Object obj) { if (obj instanceof Object[]) { return Arrays.deepToString((Object[]) obj); } else if (obj instanceof int[]) { return Arrays.toString((int[]) obj); } else if (obj instanceof long[]) { return Arrays.toString((long[]) obj); } else if (obj instanceof byte[]) { return Arrays.toString((byte[]) obj); } else if (obj instanceof double[]) { return Arrays.toString((double[]) obj); } else if (obj instanceof float[]) { return Arrays.toString((float[]) obj); } else if (obj instanceof char[]) { return Arrays.toString((char[]) obj); } else if (obj instanceof short[]) { return Arrays.toString((short[]) obj); } else if (obj instanceof boolean[]) { return Arrays.toString((boolean[]) obj); } // unreachable? return obj.toString(); } //----------------------------------------------------------------------- /** * Checks if the two beans have the same set of properties. *

* This comparison checks that both beans have the same set of property names * and that the value of each property name is also equal. * It does not check the bean type, thus a {@link FlexiBean} may be equal * to a {@link DirectBean}. *

* This comparison is usable with the {@link #propertiesHashCode} method. * The result is the same as that if each bean was converted to a {@code Map} * from name to value. * * @param bean1 the first bean to compare, not null * @param bean2 the second bean to compare, not null * @return true if equal */ public static boolean propertiesEqual(Bean bean1, Bean bean2) { Set names = bean1.propertyNames(); if (names.equals(bean2.propertyNames()) == false) { return false; } for (String name : names) { Object value1 = bean1.property(name).get(); Object value2 = bean2.property(name).get(); if (equal(value1, value2) == false) { return false; } } return true; } /** * Returns a hash code based on the set of properties on a bean. *

* This hash code is usable with the {@link #propertiesEqual} method. * The result is the same as that if each bean was converted to a {@code Map} * from name to value. * * @param bean the bean to generate a hash code for, not null * @return the hash code */ public static int propertiesHashCode(Bean bean) { int hash = 7; Set names = bean.propertyNames(); for (String name : names) { Object value = bean.property(name).get(); hash += hashCode(value); } return hash; } /** * Returns a string describing the set of properties on a bean. *

* The result is the same as that if the bean was converted to a {@code Map} * from name to value. * * @param bean the bean to generate a string for, not null * @param prefix the prefix to use, null ignored * @return the string form of the bean, not null */ public static String propertiesToString(Bean bean, String prefix) { Set names = bean.propertyNames(); StringBuilder buf; if (prefix != null) { buf = new StringBuilder((names.size()) * 32 + prefix.length()).append(prefix); } else { buf = new StringBuilder((names.size()) * 32); } buf.append('{'); if (names.size() > 0) { for (String name : names) { Object value = bean.property(name).get(); buf.append(name).append('=').append(value).append(',').append(' '); } buf.setLength(buf.length() - 2); } buf.append('}'); return buf.toString(); } /** * Flattens a bean to a {@code Map}. *

* The returned map will contain all the properties from the bean with their actual values. * * @param bean the bean to generate a string for, not null * @return the bean as a map, not null */ public static Map flatten(Bean bean) { Map> propertyMap = bean.metaBean().metaPropertyMap(); Map map = new LinkedHashMap<>(propertyMap.size()); for (Entry> entry : propertyMap.entrySet()) { map.put(entry.getKey(), entry.getValue().get(bean)); } return Collections.unmodifiableMap(map); } //----------------------------------------------------------------------- /** * Copies properties from a bean to a different bean type. *

* This copies each non-null property value from the source bean to the destination builder * provided that the destination builder supports the property name and the type is compatible. * * @param the type of the bean to create * @param sourceBean the bean to copy from, not null * @param destType the type of the destination bean, not null * @return the copied bean as a builder * @throws RuntimeException if unable to copy a property */ public static BeanBuilder copy(Bean sourceBean, Class destType) { MetaBean destMeta = MetaBean.of(destType); @SuppressWarnings("unchecked") BeanBuilder destBuilder = (BeanBuilder) destMeta.builder(); return copyInto(sourceBean, destMeta, destBuilder); } /** * Copies properties from a bean to a builder, which may be for a different type. *

* This copies each non-null property value from the source bean to the destination builder * provided that the destination builder supports the property name and the type is compatible. * * @param the type of the bean to create * @param sourceBean the bean to copy from, not null * @param destMeta the meta bean of the destination, not null * @param destBuilder the builder to populate, not null * @return the updated builder * @throws RuntimeException if unable to copy a property */ public static BeanBuilder copyInto(Bean sourceBean, MetaBean destMeta, BeanBuilder destBuilder) { MetaBean sourceMeta = sourceBean.metaBean(); for (MetaProperty sourceProp : sourceMeta.metaPropertyIterable()) { if (destMeta.metaPropertyExists(sourceProp.name())) { MetaProperty destProp = destMeta.metaProperty(sourceProp.name()); if (destProp.propertyType().isAssignableFrom(sourceProp.propertyType())) { Object sourceValue = sourceProp.get(sourceBean); if (sourceValue != null) { destBuilder.set(destProp, sourceValue); } } } } return destBuilder; } //----------------------------------------------------------------------- /** * Clones a bean. *

* This performs a deep clone. There is no protection against cycles in * the object graph beyond {@code StackOverflowError}. * * @param the type of the bean * @param original the original bean to clone, null returns null * @return the cloned bean, null if null input */ public static T clone(T original) { if (original == null || original instanceof ImmutableBean) { return original; } return cloneAlways(original); } /** * Clones a bean always. *

* This performs a deep clone. There is no protection against cycles in * the object graph beyond {@code StackOverflowError}. * This differs from {@link #clone()} in that immutable beans are also cloned. * * @param the type of the bean * @param original the original bean to clone, not null * @return the cloned bean, not null */ public static T cloneAlways(T original) { @SuppressWarnings("unchecked") BeanBuilder builder = (BeanBuilder) original.metaBean().builder(); for (MetaProperty mp : original.metaBean().metaPropertyIterable()) { if (mp.style().isBuildable()) { Object value = mp.get(original); builder.set(mp.name(), Cloner.INSTANCE.clone(value)); } } return builder.build(); } //----------------------------------------------------------------------- /** * Checks if the value is not null, throwing an exception if it is. * * @param value the value to check, may be null * @param propertyName the property name, should not be null * @throws IllegalArgumentException if the value is null */ public static void notNull(Object value, String propertyName) { if (value == null) { throw new IllegalArgumentException(notNullMsg(propertyName)); } } // extracted from notNull(Object,String) to aid hotspot inlining private static String notNullMsg(String propertyName) { return "Argument '" + propertyName + "' must not be null"; } /** * Checks if the value is not blank, throwing an exception if it is. *

* Validate that the specified argument is not null and has at least one non-whitespace character. * * @param value the value to check, may be null * @param propertyName the property name, should not be null * @throws IllegalArgumentException if the value is null or empty */ public static void notBlank(String value, String propertyName) { if (isBlank(value)) { throw new IllegalArgumentException(notBlank(propertyName)); } } @SuppressWarnings("null") private static boolean isBlank(String str) { int strLen = (str != null ? str.length() : 0); if (strLen != 0) { for (int i = 0; i < strLen; i++) { if (!Character.isWhitespace(str.charAt(i))) { return false; } } } return true; } private static String notBlank(String propertyName) { return "Argument '" + propertyName + "' must not be empty"; } /** * Checks if the value is not empty, throwing an exception if it is. * * @param value the value to check, may be null * @param propertyName the property name, should not be null * @throws IllegalArgumentException if the value is null or empty */ public static void notEmpty(String value, String propertyName) { if (value == null || value.length() == 0) { throw new IllegalArgumentException(notEmpty(propertyName)); } } // extracted from notEmpty(?,String) to aid hotspot inlining private static String notEmpty(String propertyName) { return "Argument '" + propertyName + "' must not be empty"; } /** * Checks if the collection value is not empty, throwing an exception if it is. * * @param value the value to check, may be null * @param propertyName the property name, should not be null * @throws IllegalArgumentException if the value is null or empty */ public static void notEmpty(Collection value, String propertyName) { if (value == null || value.isEmpty()) { throw new IllegalArgumentException(notEmpty(propertyName)); } } /** * Checks if the map value is not empty, throwing an exception if it is. * * @param value the value to check, may be null * @param propertyName the property name, should not be null * @throws IllegalArgumentException if the value is null or empty */ public static void notEmpty(Map value, String propertyName) { if (value == null || value.isEmpty()) { throw new IllegalArgumentException(notEmpty(propertyName)); } } //----------------------------------------------------------------------- /** * Extracts the collection content type as a {@code Class} from a property. *

* This method allows the resolution of generics in certain cases. * * @param prop the property to examine, not null * @return the collection content type, null if unable to determine or type has no generic parameters */ public static Class collectionType(Property prop) { return collectionType(prop.metaProperty(), prop.bean().getClass()); } /** * Extracts the collection content type as a {@code Class} from a meta-property. *

* The target type is the type of the object, not the declaring type of the meta-property. * * @param prop the property to examine, not null * @param targetClass the target type to evaluate against, not null * @return the collection content type, null if unable to determine or type has no generic parameters */ public static Class collectionType(MetaProperty prop, Class targetClass) { return extractTypeClass(prop, targetClass, 1, 0); } /** * Extracts the map value type generic type parameters as a {@code Class} from a meta-property. *

* The target type is the type of the object, not the declaring type of the meta-property. *

* This is used when the collection generic parameter is a map or collection. * * @param prop the property to examine, not null * @param targetClass the target type to evaluate against, not null * @return the collection content type generic parameters, empty if unable to determine, no nulls */ public static List> collectionTypeTypes(MetaProperty prop, Class targetClass) { Type type = extractType(targetClass, prop, 1, 0); return extractTypeClasses(targetClass, type); } /** * Extracts the map key type as a {@code Class} from a meta-property. * * @param prop the property to examine, not null * @return the map key type, null if unable to determine or type has no generic parameters */ public static Class mapKeyType(Property prop) { return mapKeyType(prop.metaProperty(), prop.bean().getClass()); } /** * Extracts the map key type as a {@code Class} from a meta-property. *

* The target type is the type of the object, not the declaring type of the meta-property. * * @param prop the property to examine, not null * @param targetClass the target type to evaluate against, not null * @return the map key type, null if unable to determine or type has no generic parameters */ public static Class mapKeyType(MetaProperty prop, Class targetClass) { return extractTypeClass(prop, targetClass, 2, 0); } /** * Extracts the map value type as a {@code Class} from a meta-property. * * @param prop the property to examine, not null * @return the map value type, null if unable to determine or type has no generic parameters */ public static Class mapValueType(Property prop) { return mapValueType(prop.metaProperty(), prop.bean().getClass()); } /** * Extracts the map value type as a {@code Class} from a meta-property. *

* The target type is the type of the object, not the declaring type of the meta-property. * * @param prop the property to examine, not null * @param targetClass the target type to evaluate against, not null * @return the map value type, null if unable to determine or type has no generic parameters */ public static Class mapValueType(MetaProperty prop, Class targetClass) { return extractTypeClass(prop, targetClass, 2, 1); } /** * Extracts the map value type generic type parameters as a {@code Class} from a meta-property. *

* The target type is the type of the object, not the declaring type of the meta-property. *

* This is used when the map value generic parameter is a map or collection. * * @param prop the property to examine, not null * @param targetClass the target type to evaluate against, not null * @return the map value type generic parameters, empty if unable to determine, no nulls */ public static List> mapValueTypeTypes(MetaProperty prop, Class targetClass) { Type type = extractType(targetClass, prop, 2, 1); return extractTypeClasses(targetClass, type); } /** * Low-level method to extract generic type information. * * @param prop the property to examine, not null * @param targetClass the target type to evaluate against, not null * @param size the number of generic parameters expected * @param index the index of the generic parameter * @return the type, null if unable to determine or type has no generic parameters */ public static Class extractTypeClass(MetaProperty prop, Class targetClass, int size, int index) { return eraseToClass(extractType(targetClass, prop, size, index)); } private static Type extractType(Class targetClass, MetaProperty prop, int size, int index) { Type genType = prop.propertyGenericType(); if (genType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genType; Type[] types = pt.getActualTypeArguments(); if (types.length == size) { Type type = types[index]; if (type instanceof WildcardType) { WildcardType wtype = (WildcardType) type; if (wtype.getLowerBounds().length == 0 && wtype.getUpperBounds().length > 0) { type = wtype.getUpperBounds()[0]; } } if (type instanceof TypeVariable) { type = resolveGenerics(targetClass, (TypeVariable) type); } return type; } } return null; } private static List> extractTypeClasses(Class targetClass, Type type) { List> result = new ArrayList<>(); if (type != null) { if (type instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) type; Type[] actualTypes = pt.getActualTypeArguments(); for (Type actualType : actualTypes) { if (actualType instanceof TypeVariable) { actualType = resolveGenerics(targetClass, (TypeVariable) actualType); } Class cls = eraseToClass(actualType); result.add(cls != null ? cls : Object.class); } } } return result; } private static Type resolveGenerics(Class targetClass, TypeVariable typevar) { // looks up meaning of type variables like T Map resolved = new HashMap<>(); Type type = targetClass; while (type != null) { if (type instanceof Class) { type = ((Class) type).getGenericSuperclass(); } else if (type instanceof ParameterizedType) { // find actual types captured by subclass ParameterizedType pt = (ParameterizedType) type; Type[] actualTypeArguments = pt.getActualTypeArguments(); // find type variables declared in source code Class rawType = eraseToClass(pt.getRawType()); if (rawType == null) { return null; } TypeVariable[] typeParameters = rawType.getTypeParameters(); for (int i = 0; i < actualTypeArguments.length; i++) { resolved.put(typeParameters[i], actualTypeArguments[i]); } type = rawType.getGenericSuperclass(); } } // resolve type variable to a meaningful type Type result = typevar; while (resolved.containsKey(result)) { result = resolved.get(result); } return result; } private static Class eraseToClass(Type type) { if (type instanceof Class) { return (Class) type; } else if (type instanceof ParameterizedType) { return eraseToClass(((ParameterizedType) type).getRawType()); } else if (type instanceof GenericArrayType) { Type componentType = ((GenericArrayType) type).getGenericComponentType(); Class componentClass = eraseToClass(componentType); if (componentClass != null) { return Array.newInstance(componentClass, 0).getClass(); } } else if (type instanceof TypeVariable) { Type[] bounds = ((TypeVariable) type).getBounds(); if (bounds.length == 0) { return Object.class; } else { return eraseToClass(bounds[0]); } } return null; } //------------------------------------------------------------------------- /** * Checks if two beans are equal ignoring one or more properties. *

* This version of {@code equalIgnoring} only checks properties at the top level. * For example, if a {@code Person} bean contains an {@code Address} bean then * only properties on the {@code Person} bean will be checked against the ignore list. * * @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 * @throws IllegalArgumentException if inputs are null */ public static boolean equalIgnoring(Bean bean1, Bean bean2, MetaProperty... properties) { JodaBeanUtils.notNull(bean1, "bean1"); JodaBeanUtils.notNull(bean2, "bean2"); JodaBeanUtils.notNull(properties, "properties"); if (bean1 == bean2) { return true; } if (bean1.getClass() != bean2.getClass()) { return false; } switch (properties.length) { case 0: return bean1.equals(bean2); case 1: { MetaProperty ignored = properties[0]; for (MetaProperty mp : bean1.metaBean().metaPropertyIterable()) { if (ignored.equals(mp) == false && JodaBeanUtils.equal(mp.get(bean1), mp.get(bean2)) == false) { return false; } } return true; } default: Set> ignored = new HashSet<>(Arrays.asList(properties)); for (MetaProperty mp : bean1.metaBean().metaPropertyIterable()) { if (ignored.contains(mp) == false && JodaBeanUtils.equal(mp.get(bean1), mp.get(bean2)) == false) { return false; } } return true; } } //----------------------------------------------------------------------- /** * Returns an iterator over all the beans contained within the bean. *

* The iterator is a depth-first traversal of the beans within the specified bean. * The first returned bean is the specified bean. * Beans within collections will be returned. *

* A cycle in the bean structure will cause an infinite loop. * * @param bean the bean to iterate over, not null * @return the iterator, not null */ public static Iterator beanIterator(Bean bean) { return new BeanIterator(bean); } //------------------------------------------------------------------------- /** * Chains two meta-properties together. *

* The resulting function takes an instance of a bean, queries using the first * meta-property, then queries the result using the second meta-property. * If the first returns null, the result will be null. * * @param

the type of the result of the chain * @param mp1 the first meta-property, not null * @param mp2 the second meta-property, not null * @return the chain function, not null */ public static

Function chain(MetaProperty mp1, MetaProperty

mp2) { notNull(mp1, "MetaProperty 1"); notNull(mp1, "MetaProperty 2"); return b -> { Bean first = mp1.get(b); return first != null ? mp2.get(first) : null; }; } /** * Chains a function to a meta-property. *

* The resulting function takes an instance of a bean, queries using the first * function, then queries the result using the second meta-property. * If the first returns null, the result will be null. * * @param

the type of the result of the chain * @param fn1 the first meta-property, not null * @param mp2 the second meta-property, not null * @return the chain function, not null */ public static

Function chain(Function fn1, MetaProperty

mp2) { notNull(fn1, "Function 1"); notNull(fn1, "MetaProperty 2"); return b -> { Bean first = fn1.apply(b); return first != null ? mp2.get(first) : null; }; } //------------------------------------------------------------------------- /** * Obtains a comparator for the specified bean query. *

* The result of the query must be {@link Comparable}. *

* To use this with a meta-property append {@code ::get} to the meta-property, * for example {@code Address.meta().street()::get}. * * @param query the query to use, not null * @param ascending true for ascending, false for descending * @return the comparator, not null */ public static Comparator comparator(Function query, boolean ascending) { return (ascending ? comparatorAscending(query) : comparatorDescending(query)); } /** * Obtains an ascending comparator for the specified bean query. *

* The result of the query must be {@link Comparable}. * * @param query the query to use, not null * @return the comparator, not null */ public static Comparator comparatorAscending(Function query) { if (query == null) { throw new NullPointerException("Function must not be null"); } return new Comp(query); } /** * Obtains an descending comparator for the specified bean query. *

* The result of the query must be {@link Comparable}. * * @param query the query to use, not null * @return the comparator, not null */ public static Comparator comparatorDescending(Function query) { if (query == null) { throw new NullPointerException("Function must not be null"); } return Collections.reverseOrder(new Comp(query)); } //------------------------------------------------------------------------- /** * Comparator. */ private static final class Comp implements Comparator { private final Function query; private Comp(Function query) { this.query = query; } @Override public int compare(Bean bean1, Bean bean2) { @SuppressWarnings("unchecked") Comparable value1 = (Comparable) query.apply(bean1); Object value2 = query.apply(bean2); return value1.compareTo(value2); } } //------------------------------------------------------------------------- /** * Clones an object. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static class Cloner { public static final Cloner INSTANCE = getInstance(); private static Cloner getInstance() { try { Class.forName("org.joda.collect.grid.Grid"); return new CollectCloner(); } catch (Exception | LinkageError ex) { try { Class.forName("com.google.common.collect.Multimap"); return new GuavaCloner(); } catch (Exception | LinkageError ex2) { return new Cloner(); } } } Cloner() { } Object clone(Object value) { if (value == null) { return value; } else if (value instanceof Bean) { return cloneAlways((Bean) value); } else if (value instanceof SortedSet) { SortedSet set = (SortedSet) value; return cloneIterable(set, new TreeSet(set.comparator())); } else if (value instanceof Set) { return cloneIterable((Set) value, new LinkedHashSet()); } else if (value instanceof Iterable) { return cloneIterable((Iterable) value, new ArrayList()); } else if (value instanceof SortedMap) { SortedMap map = (SortedMap) value; return cloneMap(map, new TreeMap(map.comparator())); } else if (value instanceof Map) { return cloneMap((Map) value, new LinkedHashMap()); } else if (value.getClass().isArray()) { return cloneArray(value); } else if (value instanceof java.util.Date) { return ((java.util.Date) value).clone(); } return value; } Object cloneIterable(Iterable original, Collection cloned) { for (Object item : original) { cloned.add(clone(item)); } return cloned; } Object cloneMap(Map original, Map cloned) { for (Object item : original.entrySet()) { Entry entry = (Entry) item; cloned.put(clone(entry.getKey()), clone(entry.getValue())); } return cloned; } Object cloneArray(Object original) { int len = Array.getLength(original); Class arrayType = original.getClass().getComponentType(); Object copy = Array.newInstance(arrayType, len); for (int i = 0; i < len; i++) { Array.set(copy, i, clone(Array.get(original, i))); } return copy; } } //------------------------------------------------------------------------- /** * Clones an object. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static class GuavaCloner extends Cloner { GuavaCloner() { } @Override Object clone(Object value) { if (value == null) { return value; } else if (value instanceof ImmutableMap || value instanceof ImmutableCollection || value instanceof ImmutableMultimap || value instanceof ImmutableTable) { return value; } else if (value instanceof SortedMultiset) { SortedMultiset set = (SortedMultiset) value; return cloneIterable(set, TreeMultiset.create(set.comparator())); } else if (value instanceof Multiset) { return cloneIterable((Multiset) value, LinkedHashMultiset.create()); } else if (value instanceof SetMultimap) { return cloneMultimap((Multimap) value, LinkedHashMultimap.create()); } else if (value instanceof ListMultimap || value instanceof Multimap) { return cloneMultimap((Multimap) value, ArrayListMultimap.create()); } else if (value instanceof BiMap) { return cloneMap((BiMap) value, HashBiMap.create()); } else if (value instanceof Table) { return cloneTable((Table) value, HashBasedTable.create()); } return super.clone(value); } Object cloneMultimap(Multimap original, Multimap cloned) { for (Object key : original.keySet()) { Collection values = original.get(key); for (Object value : values) { cloned.put(clone(key), clone(value)); } } return cloned; } Object cloneTable(Table original, Table cloned) { for (Object item : original.cellSet()) { Table.Cell cell = (Table.Cell) item; cloned.put(clone(cell.getRowKey()), clone(cell.getColumnKey()), clone(cell.getValue())); } return cloned; } } //------------------------------------------------------------------------- /** * Clones an object. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static class CollectCloner extends GuavaCloner { CollectCloner() { } @Override Object clone(Object value) { if (value == null) { return value; } else if (value instanceof ImmutableGrid) { return value; } else if (value instanceof DenseGrid) { Grid grid = (Grid) value; return cloneGrid(grid, DenseGrid.create(grid.rowCount(), grid.columnCount())); } else if (value instanceof Grid) { Grid grid = (Grid) value; return cloneGrid(grid, SparseGrid.create(grid.rowCount(), grid.columnCount())); } return super.clone(value); } Object cloneGrid(Grid original, Grid cloned) { for (Object item : original.cells()) { Grid.Cell cell = (Grid.Cell) item; cloned.put(cell.getRow(), cell.getColumn(), clone(cell.getValue())); } return cloned; } } }