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

org.nuiton.util.beans.Binder Maven / Gradle / Ivy

There is a newer version: 3.1
Show newest version
/*
 * #%L
 * Nuiton Utils
 * 
 * $Id: Binder.java 2360 2012-06-11 10:24:36Z tchemit $
 * $HeadURL: http://svn.nuiton.org/svn/nuiton-utils/tags/nuiton-utils-2.6.10/nuiton-utils/src/main/java/org/nuiton/util/beans/Binder.java $
 * %%
 * Copyright (C) 2004 - 2010 CodeLutin
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * .
 * #L%
 */

package org.nuiton.util.beans;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuiton.util.ObjectUtil;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * A {@code binder} permits to copy some properties from an object to another
 * one.
 * 

* It is based on a {@link BinderModel} which contains the mapping of properties * to transfert from the source object to the destination object. *

* Use the method {@link #copy(Object, Object, String...)} to transfert properties. *

* Use the method {@link #obtainProperties(Object, String...)} to obtain some * properties from a given object. *

* For more informations about how to obtain a binder, see the * {@link BinderFactory} or the package info javadoc or unit tests... * * @param the source bean type * @param the destination bean type * @author tchemit * @see BinderFactory * @see BinderModelBuilder * @since 1.1.5 */ public class Binder implements Serializable { /** Logger. */ private static final Log log = LogFactory.getLog(Binder.class); private static final long serialVersionUID = 1L; /** * Types of loading of collections. * * @since 1.3 */ public enum CollectionStrategy { /** To just copy the reference of the collection. */ copy { public Object copy(Object readValue) { // by default, just return same reference return readValue; } }, /** To duplicate the collection */ duplicate { @SuppressWarnings({"unchecked"}) @Override public Object copy(Object readValue) { if (readValue instanceof Set) { return new HashSet((Set) readValue); } // in any other cases, let says this is a ArrayList if (readValue instanceof Collection) { return new ArrayList((Collection) readValue); } return readValue; } }; /** * Copy a given collection. * * @param readValue the collection value to copy * @return the copied collection */ public abstract Object copy(Object readValue); } /** the model of the binder */ protected BinderModel model; /** * Obtains the type of the source bean. * * @return the type of the source bean */ public Class getSourceType() { return getModel().getSourceType(); } /** * Obtains the type of the target bean. * * @return the type of the target bean */ public Class getTargetType() { return getModel().getTargetType(); } /** * Obtain from the given object all properties registered in the binder * model. * * @param source the bean to read * @param propertyNames subset of properties to load * @param includeNullValues get all the properties and * values for the given bean. If false, you'll get only the values * @return the map of properties obtained indexed by their property name, * or an empty map is the given {@code from} is {@code null}. * @since 2.3 */ public Map obtainProperties(I source, boolean includeNullValues, String... propertyNames) { if (source == null) { // special limit case return Collections.emptyMap(); } propertyNames = getProperties(propertyNames); Map result = new TreeMap(); for (String sourceProperty : propertyNames) { try { Object read; Method readMethod = model.getSourceReadMethod(sourceProperty); read = readMethod.invoke(source); if (log.isDebugEnabled()) { log.debug("property " + sourceProperty + ", type : " + readMethod.getReturnType() + ", value = " + read); } if (readMethod.getReturnType().isPrimitive() && ObjectUtil.getNullValue( readMethod.getReturnType()).equals(read)) { // for primitive type case, force nullity read = null; } if (read != null) { if (model.containsBinderProperty(sourceProperty)) { if (model.containsCollectionProperty(sourceProperty)) { read = bindCollection(sourceProperty, read); } else { read = bindProperty(sourceProperty, read); } } else if (model.containsCollectionProperty(sourceProperty)) { // specific collection strategy is set, must use it read = getCollectionValue(sourceProperty, read); } } boolean include = read != null || includeNullValues; if (include) { result.put(sourceProperty, read); } } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } } return result; } /** * Obtain from the given object all properties registered in the binder * model. *

* Note: If a property's value is null, it will not be injected in * the result. * * @param source the bean to read * @param propertyNames subset of properties to load * @return the map of properties obtained indexed by their property name, * or an empty map is the given {@code from} is {@code null}. */ public Map obtainProperties(I source, String... propertyNames) { return obtainProperties(source, false, propertyNames); } /** * Copy properties from a source bean to a destination one according to * the model of the binder. If {@code propertyNames} is defined, only * those properties will be copied. *

* Note: If {@code from} object is null, then {@code null} values * will be set to mapped properties into {@code dst} * * @param source the bean to read * @param target the bean to write * @param propertyNames optional subset of properties to copy (if none is * specifed, will use all the properties defined in * binder) * @throws NullPointerException if target parameter is {@code null} */ public void copy(I source, O target, String... propertyNames) { copy(source, target, false, propertyNames); } /** * Copy properties from a source bean to a destination one according to * the model of the binder excluding {@code propertyNames}. *

* Note: If {@code from} object is null, then {@code null} values * will be set to mapped properties into {@code dst}. * * @param source the bean to read * @param target the bean to write * @param propertyNames optional subset of properties to copy (if none is * specifed, will use all the properties defined in * binder) * @throws NullPointerException if target parameter is {@code null} */ public void copyExcluding(I source, O target, String... propertyNames) { copy(source, target, true, propertyNames); } /** * Copy properties from a source bean to a destination one according to * the model of the binder. *

* Note: If {@code from} object is null, then {@code null} values * will be set to mapped properties into {@code dst}. * * @param source the bean to read * @param target the bean to write * @param excludeProperties true to exclude following {@code propertyNames} * @param propertyNames optional subset of properties to copy (if none is * specifed, will use all the properties defined in * binder) * @throws NullPointerException if target parameter is {@code null} * @throws RuntimeException if a property can not be copied to the target object */ protected void copy(I source, O target, boolean excludeProperties, String... propertyNames) throws RuntimeException { if (target == null) { throw new NullPointerException("parameter 'target' can no be null"); } propertyNames = excludeProperties ? getAllPropertiesExclude(propertyNames) : getProperties(propertyNames); for (String sourceProperty : propertyNames) { String targetProperty = model.getTargetProperty(sourceProperty); try { Object read = null; Method readMethod = model.getSourceReadMethod(sourceProperty); if (source != null) { // obtain value from source read = readMethod.invoke(source); } // obtain acceptable null value (for primitive types, use // default values). if (read == null) { read = ObjectUtil.getNullValue(readMethod.getReturnType()); } if (log.isDebugEnabled()) { log.debug("property " + sourceProperty + ", type : " + readMethod.getReturnType() + ", value = " + read); } if (model.containsBinderProperty(sourceProperty)) { if (model.containsCollectionProperty(sourceProperty)) { read = bindCollection(sourceProperty, read); } else { read = bindProperty(sourceProperty, read); } } else if (model.containsCollectionProperty(sourceProperty)) { // specific collection strategy is set, must use it read = getCollectionValue(sourceProperty, read); } model.getTargetWriteMethod(targetProperty).invoke(target, read); } catch (Exception e) { throw new RuntimeException( "Could not bind property [" + source.getClass().getName() + ":" + sourceProperty + "] to [" + target.getClass().getName() + ":" + targetProperty + "]", e); } } } protected Object readProperty(String sourceProperty, Object source, Method readMethod) { try { Object read = null; if (source != null) { // obtain value from source read = readMethod.invoke(source); } // obtain acceptable null value (for primitive types, use // default values). if (read == null) { read = ObjectUtil.getNullValue(readMethod.getReturnType()); } if (log.isDebugEnabled()) { log.debug("property " + sourceProperty + ", type : " + readMethod.getReturnType() + ", value = " + read); } if (model.containsBinderProperty(sourceProperty)) { if (model.containsCollectionProperty(sourceProperty)) { read = bindCollection(sourceProperty, read); } else { read = bindProperty(sourceProperty, read); } } else if (model.containsCollectionProperty(sourceProperty)) { // specific collection strategy is set, must use it read = getCollectionValue(sourceProperty, read); } return read; } catch (Exception e) { throw new RuntimeException( "could not read property " + sourceProperty + " on source " + source); } } protected List diff(I source, O target, boolean excludeProperties, String... propertyNames) { if (source == null) { throw new NullPointerException("parameter 'source' can no be null"); } if (target == null) { throw new NullPointerException("parameter 'target' can no be null"); } propertyNames = excludeProperties ? getAllPropertiesExclude(propertyNames) : getProperties(propertyNames); List result = new LinkedList(); for (String sourceProperty : propertyNames) { Method sourceReadMethod = model.getSourceReadMethod(sourceProperty); Object sourceRead = readProperty(sourceProperty, source, sourceReadMethod); String targetProperty = model.getTargetProperty(sourceProperty); Method targetReadMethod = model.getTargetReadMethod(targetProperty); Object targetRead = readProperty(targetProperty, target, targetReadMethod); if (ObjectUtils.notEqual(sourceRead, targetRead)) { PropertyDiff propertyDiff = new PropertyDiff( sourceProperty, sourceRead, targetProperty, targetRead, sourceReadMethod.getReturnType() ); result.add(propertyDiff); } } return result; } /** * Compare two beans property by property according to the model. *

* List contains one element per property with different values (according * to the result of an equals() call) * * @param source a bean of type I * @param target a bean of type O * @return a list with all the properties which values differ in source * and target. Properties with equal values are not included. * @since 2.3 */ public List diff(I source, O target) { return diff(source, target, false); } /** * Compare two beans property by property according to the model. *

* List contains one element per property with different values (according * to the result of an equals() call) * * @param source a bean of type I * @param target a bean of type O * @param propertyNames property names to exclude from the diff * @return a list with all the properties which values differ in source * and target. Properties with equal values and excluded properties * will not be contained in the result * @since 2.3 */ public List diffExcluding(I source, O target, String... propertyNames) { return diff(source, target, true, propertyNames); } /** * Get the model of the binder. * * @return the model of the binder */ protected BinderModel getModel() { return model; } /** * Set the model of the binder. * * @param model the model of the binder */ protected void setModel(BinderModel model) { this.model = model; } /** * Obtain the properties, if none is given in {@code propertyNames} * parameter, will use all property names defined in binder's model, * otherwise, check that all given property names are safe (registred in * binder's model). * * @param propertyNames optional subset of properties to get * @return the array of property names */ protected String[] getProperties(String... propertyNames) { if (propertyNames.length == 0) { // use all properties in the binder propertyNames = model.getSourceDescriptors(); } else { // use a subset of properties, must check them for (String propertyName : propertyNames) { if (!model.containsSourceProperty(propertyName)) { throw new IllegalArgumentException( "property '" + propertyName + "' is not known by binder"); } } } return propertyNames; } /** * Obtains all properties from binder's model except those {@code * propertyNameExcludes}. Unknown properties will be ignored. * * @param propertyNameExcludes name of properties to exclude * @return the array of property names without those in argument */ protected String[] getAllPropertiesExclude(String... propertyNameExcludes) { List excludes = Arrays.asList(propertyNameExcludes); List results = new ArrayList(); for (String propertyName : model.getSourceDescriptors()) { if (!excludes.contains(propertyName)) { results.add(propertyName); } } return results.toArray(new String[results.size()]); } protected Object getCollectionValue(String sourceProperty, Object readValue) { CollectionStrategy strategy = model.getCollectionStrategy(sourceProperty); Object result = strategy.copy(readValue); return result; } protected Object bindProperty(String sourceProperty, Object read) throws IllegalAccessException, InstantiationException { Binder binder = model.getBinder(sourceProperty); Object result = bind(binder, read); return result; } @SuppressWarnings({"unchecked"}) protected Object bindCollection(String sourceProperty, Object read) throws IllegalAccessException, InstantiationException { if (read == null) { return null; } Binder binder = model.getBinder(sourceProperty); Collection result; if (read instanceof Set) { result = new HashSet(); } else { // in any other cases, let says this is a ArrayList result = new ArrayList(); } Collection collection = (Collection) read; for (Object o : collection) { Object r = bind(binder, o); result.add(r); } return result; } @SuppressWarnings({"unchecked"}) protected Object bind(Binder binder, Object read) throws IllegalAccessException, InstantiationException { Object result = read.getClass().newInstance(); binder.copy(read, result); return result; } /** * Model of a {@link Binder}. *

* TODO tchemit 20100225 should have special cases for collections treatment. * * @param the source type * @param the target type * @author tchemit * @since 1.1.5 */ public static class BinderModel implements Serializable { /** source type */ protected final Class sourceType; /** destination type */ protected final Class targetType; /** source type descriptors (key are property names) */ protected final Map sourceDescriptors; /** destination descriptors (key are property names) */ protected final Map targetDescriptors; /** * properties mapping (key are source properties, value are destination * properties) */ protected final Map propertiesMapping; /** mapping of collection properties strategies */ protected Map collectionStrategies; /** mapping of extra binders to use to copy properties */ protected Map> binders; private static final long serialVersionUID = 2L; public BinderModel(Class sourceType, Class targetType) { this.sourceType = sourceType; this.targetType = targetType; sourceDescriptors = new TreeMap(); targetDescriptors = new TreeMap(); propertiesMapping = new TreeMap(); collectionStrategies = new TreeMap(); binders = new TreeMap>(); } /** * Gets the type of the binder's source. * * @return the type of the source object in the binder */ public Class getSourceType() { return sourceType; } /** * Gets the type of the binder's destination * * @return the type of the destination object in the binder */ public Class getTargetType() { return targetType; } /** * Gets all registred property names of the binder's source type. * * @return the array of all source object properties names to bind */ public String[] getSourceDescriptors() { Set universe = sourceDescriptors.keySet(); return universe.toArray(new String[sourceDescriptors.size()]); } public CollectionStrategy getCollectionStrategy(String property) { return collectionStrategies.get(property); } /** * Gets all registred property names of the binder's destination type. * * @return the array of all source object properties names to bind */ public String[] getTargetDescriptors() { Set universe = targetDescriptors.keySet(); return universe.toArray(new String[targetDescriptors.size()]); } /** * Gets the destination property name given the * * @param sourceProperty the name of the source property to bind * @return the name of the destination object property to bind, or * {@code null} if {@code propertySrc} is unknown in the model */ public String getTargetProperty(String sourceProperty) { if (!containsSourceProperty(sourceProperty)) { return null; } String dstProperty = propertiesMapping.get(sourceProperty); return dstProperty; } /** * Gets the bean descriptor of the source type for the given * destination property. * * @param sourceProperty name of the source type property name * @return the descriptor or {@code null} if not found. */ public PropertyDescriptor getSourceDescriptor(String sourceProperty) { // check src property is registred if (!containsSourceProperty(sourceProperty)) { return null; } PropertyDescriptor descriptor = sourceDescriptors.get(sourceProperty); return descriptor; } /** * @param srcProperty the name of a property of the source object. * @return the method to read in a source object for the given property. */ public Method getSourceReadMethod(String srcProperty) { PropertyDescriptor descriptor = getSourceDescriptor(srcProperty); Method readMethod = null; if (descriptor != null) { readMethod = descriptor.getReadMethod(); } return readMethod; } /** * @param sourceProperty the name of a property of the source object. * @return the method to write in a source object for the given property. */ public Method getSourceWriteMethod(String sourceProperty) { PropertyDescriptor descriptor = getSourceDescriptor(sourceProperty); Method writeMethod = null; if (descriptor != null) { writeMethod = descriptor.getWriteMethod(); } return writeMethod; } /** * Gets the bean descriptor of the destination type for the given * destination property. * * @param targetProperty name of the destination type property name * @return the descriptor or {@code null} if not found. */ public PropertyDescriptor getTargetDescriptor(String targetProperty) { // check dst property is registred if (!containsTargetProperty(targetProperty)) { return null; } PropertyDescriptor descriptor = targetDescriptors.get(targetProperty); return descriptor; } /** * @param targetProperty the name of a property of the destination object. * @return the method to read in a destination object for the given * property. */ public Method getTargetReadMethod(String targetProperty) { PropertyDescriptor descriptor = getTargetDescriptor(targetProperty); Method readMethod = null; if (descriptor != null) { readMethod = descriptor.getReadMethod(); } return readMethod; } /** * @param targetProperty the name of a property of the destination object. * @return the method to write in a destination object for the given * property. */ public Method getTargetWriteMethod(String targetProperty) { PropertyDescriptor descriptor = getTargetDescriptor(targetProperty); Method writeMethod = null; if (descriptor != null) { writeMethod = descriptor.getWriteMethod(); } return writeMethod; } public Class getCollectionType(String sourceProperty) { Method method = getSourceReadMethod(sourceProperty); Class type = method.getReturnType(); if (Collection.class.isAssignableFrom(type)) { return type; } return null; } public void addCollectionStrategy(String propertyName, CollectionStrategy strategy) { collectionStrategies.put(propertyName, strategy); } public void addBinder(String propertyName, Binder binder) { binders.put(propertyName, binder); } protected boolean containsSourceProperty(String sourceProperty) { return propertiesMapping.containsKey(sourceProperty); } protected boolean containsTargetProperty(String targetProperty) { return propertiesMapping.containsValue(targetProperty); } protected boolean containsCollectionProperty(String propertyName) { return collectionStrategies.containsKey(propertyName); } protected boolean containsBinderProperty(String propertyName) { return binders.containsKey(propertyName); } protected void addBinding(PropertyDescriptor sourceDescriptor, PropertyDescriptor targetDescriptor) { String sourceProperty = sourceDescriptor.getName(); String targetProperty = targetDescriptor.getName(); sourceDescriptors.put(sourceProperty, sourceDescriptor); targetDescriptors.put(targetProperty, targetDescriptor); propertiesMapping.put(sourceProperty, targetProperty); } protected void removeBinding(String source) { String target = propertiesMapping.get(source); sourceDescriptors.remove(source); targetDescriptors.remove(target); propertiesMapping.remove(source); if (containsBinderProperty(source)) { binders.remove(source); } if (containsCollectionProperty(source)) { collectionStrategies.remove(source); } } protected Map getPropertiesMapping() { return propertiesMapping; } public Binder getBinder(String sourceProperty) { return binders.get(sourceProperty); } } }