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
 * %%
 * 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 com.google.common.base.Defaults;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
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.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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 Tony Chemit - [email protected]
 * @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 LinkedHashSet) {
                    return new LinkedHashSet((Set) 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;
            }
        },
        /**
         * To bind the collection.
         *
         * Warning: Do not use it in the method {@link BinderModelBuilder#addCollectionStrategy(CollectionStrategy, String...)}
         * since it is only use in internaly by the method {@link BinderModelBuilder#addCollectionBinder(Binder, String...)}
         *
         * Note: at this level, we will just create the collection.
         */
        bind {
            @Override
            public Object copy(Object readValue) {
                if (readValue instanceof LinkedHashSet) {
                    return new LinkedHashSet();
                }
                if (readValue instanceof Set) {
                    return new HashSet();
                }
                // in any other cases, let says this is a ArrayList
                if (readValue instanceof Collection) {
                    return new ArrayList();
                }
                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 keepPrimitiveDefaultValues to keep primitive default value and not replace them by a {@code null} value.
     * @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 3.0
     */
    public Map obtainProperties(I source,
                                                boolean keepPrimitiveDefaultValues,
                                                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()
                        && !keepPrimitiveDefaultValues
                        && 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 (Exception e) {
                throw new RuntimeException("Could not obtain property: " + sourceProperty, e);
            }
        }
        return result;
    }

    /**
     * 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) {

        return obtainProperties(source, false, includeNullValues, propertyNames);
    }

    /**
     * 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);
    }

    /**
     * Obtain a property from a source object (A source object type reflect
     * the source type of the binder).
     *
     * Note: The property value has no special treatment, the result
     * is the exact value from the source object (no binder collection transformation, ...).
     *
     * @param source       the source object to inspect
     * @param propertyName name of the property to get
     * @param          type of property to get
     * @return the property value in the source object.
     * @since 3.0
     */
    public  OO obtainSourceProperty(I source, String propertyName) {

        Preconditions.checkNotNull(source, "source can not be null");
        Preconditions.checkNotNull(propertyName, "propertyName can not be null");

        Method readMethod = model.getSourceReadMethod(propertyName);

        Preconditions.checkNotNull(readMethod, "Could not find source getter for property: " + propertyName);

        try {
            OO result = (OO) readMethod.invoke(source);
            if (log.isDebugEnabled()) {
                log.debug("property " + propertyName + ", type : " +
                                  readMethod.getReturnType() + ", value = " + result);
            }
            return result;

        } catch (Exception e) {
            throw new RuntimeException("Could not obtain property: " + propertyName, e);
        }
    }

    /**
     * Obtain a property from a target object (A target object type reflect
     * the target type of the binder).
     *
     * Note: The property value has no special treatment, the result
     * is the exact value from the target object (no binder collection transformation, ...).
     *
     * @param target       the target object to inspect
     * @param propertyName name of the property to get
     * @param          type of property to get
     * @return the property value in the target object.
     * @since 3.0
     */
    public  OO obtainTargetProperty(O target, String propertyName) {

        Preconditions.checkNotNull(target, "target can not be null");
        Preconditions.checkNotNull(propertyName, "propertyName can not be null");

        Method readMethod = model.getTargetReadMethod(propertyName);

        Preconditions.checkNotNull(readMethod, "Could not find target getter for property: " + propertyName);

        try {
            OO result = (OO) readMethod.invoke(target);
            if (log.isDebugEnabled()) {
                log.debug("property " + propertyName + ", type : " +
                                  readMethod.getReturnType() + ", value = " + result);
            }
            return result;

        } catch (Exception e) {
            throw new RuntimeException("Could not obtain property: " + propertyName, e);
        }

    }

    /**
     * Inject all not null properties to the target bean.
     *
     * @param properties properties to set into bean
     * @param target     the bean to set
     * @since 3.0
     */
    public void injectProperties(Map properties, O target) {
        injectProperties(properties, target, false);
    }

    /**
     * Inject all properties to the target bean.
     *
     * @param properties        properties to set into bean
     * @param target            the bean to set
     * @param includeNullValues {@code true} to set also null properties values
     * @since 3.0
     */
    public void injectProperties(Map properties, O target, boolean includeNullValues) {

        boolean useFunctions = model.isUseFunctions();

        for (Map.Entry entry : properties.entrySet()) {
            String propertyName = entry.getKey();
            if (!getModel().containsTargetProperty(propertyName)) {

                throw new IllegalStateException("Could not find property '" + propertyName + "' in binder " + this + ".");
            }

            Object propertyValue = entry.getValue();
            if (propertyValue == null && !includeNullValues) {

                // Skip null value
                continue;
            }

            if (log.isDebugEnabled()) {
                log.debug("Inject property: " + propertyName + " to " + target);
            }
            if (useFunctions && propertyValue != null) {
                propertyValue = transform(propertyName, propertyValue);
            }
            if (propertyValue == null) {
                Class targetPropertyType = getTargetPropertyType(propertyName);
                if (targetPropertyType.isPrimitive()) {
                    propertyValue = Defaults.defaultValue(targetPropertyType);
                }
            }
            Method writeMethod = getModel().getTargetWriteMethod(propertyName);
            try {
                writeMethod.invoke(target, propertyValue);
            } catch (Exception e) {
                throw new RuntimeException(
                        "Could not set property [" +
                                target.getClass().getName() + ":" +
                                propertyName + "]", e);
            }

        }

    }

    protected Object transform(String propertyName, Object propertyValue) {
        Function function = model.getFunction(propertyValue.getClass());
        if (function != null) {
            if (log.isDebugEnabled()) {
                log.debug("Transform property: " + propertyName);
            }
            propertyValue = function.apply(propertyValue);
        }
        return propertyValue;
    }

    /**
     * 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);
    }

    /**
     * Get the type of a source property.
     *
     * @param propertyName name of the source property
     * @return the type of the source property
     * @throws IllegalArgumentException if binder does not define this source property
     */
    public Class getSourcePropertyType(String propertyName) {
        if (!model.containsSourceProperty(propertyName)) {
            throw new IllegalArgumentException("Binder " + this + " does not contains source property: " + propertyName);
        }
        return model.getSourceReadMethod(propertyName).getReturnType();
    }

    /**
     * Get the generic type of a source property.
     *
     * @param propertyName name of the source property
     * @return the generic type of the source property
     * @throws IllegalArgumentException if binder does not define this source property
     */
    public Type getSourcePropertyGenericType(String propertyName) {
        if (!model.containsSourceProperty(propertyName)) {
            throw new IllegalArgumentException("Binder " + this + " does not contains source property: " + propertyName);
        }
        return model.getSourceReadMethod(propertyName).getGenericReturnType();
    }

    /**
     * Get the type of a target property.
     *
     * @param propertyName name of the target property
     * @return the type of the target property
     * @throws IllegalArgumentException if binder does not define this target property
     */
    public Class getTargetPropertyType(String propertyName) {
        if (!model.containsTargetProperty(propertyName)) {
            throw new IllegalArgumentException("Binder " + this + " does not contains target property: " + propertyName);
        }
        return model.getTargetWriteMethod(propertyName).getParameterTypes()[0];
    }

    /**
     * Get the generic type of a target property.
     *
     * @param propertyName name of the target property
     * @return the generic type of the target property
     * @throws IllegalArgumentException if binder does not define this target property
     */
    public Type getTargetPropertyGenericType(String propertyName) {
        if (!model.containsTargetProperty(propertyName)) {
            throw new IllegalArgumentException("Binder " + this + " does not contains target property: " + propertyName);
        }
        return model.getTargetWriteMethod(propertyName).getGenericParameterTypes()[0];
    }

    /**
     * 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);

        boolean useFunctions = model.isUseFunctions();

        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);
                }
                if (useFunctions && read != null) {
                    read = transform(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 = (Collection) model.getCollectionStrategy(sourceProperty).copy(read);

//        if (read instanceof LinkedHashSet) {
//            result = new LinkedHashSet();
//        } else 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 = null;
        if (read != null) {
            InstanceFactory instanceFactory = binder.model.getInstanceFactory();
            if (instanceFactory == null) {
                result = read.getClass().newInstance();
            } else {
                result = instanceFactory.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 Tony Chemit - [email protected]
     * @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;

        /** factory of target Instance */
        protected InstanceFactory instanceFactory;

        /**
         * Dictonnary of function to apply by source class type.
         */
        protected final Map, Function> functions;

        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>();
            functions = new LinkedHashMap, Function>();
        }

        /**
         * 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);
        }

        public boolean containsSourceProperty(String sourceProperty) {
            return propertiesMapping.containsKey(sourceProperty);
        }

        public boolean containsTargetProperty(String targetProperty) {
            return propertiesMapping.containsValue(targetProperty);
        }

        public boolean containsCollectionProperty(String propertyName) {
            return collectionStrategies.containsKey(propertyName);
        }

        public 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);
        }

        public void setInstanceFactory(InstanceFactory instanceFactory) {
            this.instanceFactory = instanceFactory;
        }

        public InstanceFactory getInstanceFactory() {
            return instanceFactory;
        }

        public Function getFunction(Class aClass) {
            return functions.get(aClass);
        }

        public boolean isUseFunctions() {
            return !functions.isEmpty();
        }

    }
}