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

org.nuiton.util.beans.BinderModelBuilder 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.Function;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Class to create a new {@link Binder.BinderModel}.
 * 

* A such object is designed to build only one model of binder and can not be * used directly to create a new binder, it prepares only the model of a new * binder, which after must be registred in the {@link BinderFactory} to obtain * a real {@link Binder}. *

* If you want to create more than one binder model, use each time a new * binder builder. *

* To obtain a new instance of a build please use one of the factories method : *

    *
  • {@link #newEmptyBuilder(Class)}} to create a binder model with same * source and target type
  • *
  • {@link #newEmptyBuilder(Class, Class)} to create a binder model with a * possible different source and target type
  • *
  • {@link #newDefaultBuilder(Class)} to create a binder model with same * source and target type and then fill the model with all matching properties.
  • *
  • {@link #newDefaultBuilder(Class, Class)} to create a binder model * with a possible different source and target type and then fill the model * with all matching properties.
  • *
* Then you can use folowing methods to specify what to put in the copy model : *
    *
  • {@link #addSimpleProperties(String...)} to add in the binder model simple * properties (a simple property is a property present in both source and target type)
  • *
  • {@link #addProperty(String, String)} to add in the binder model a single * property (from source type) to be copied to another property (in target type)
  • *
  • {@link #addProperties(String...)} to add in the binder model properties * (says here you specify some couple of properties (sourcePropertyName, * targetPropertyName) to be added in the binder model)
  • *
  • {@link #addBinder(String, Binder)} to add in the binder model * another binder to be used to copy the given simple property (same name in * source and target type)
  • *
  • {@link #addCollectionStrategy(Binder.CollectionStrategy, String...)} to * specify the strategy to be used to bind some collection some source type to * target type
  • *
  • {@link #addCollectionBinder(Binder, String...)} to * bind a collection: a new collection will be created and all elements of sources will be * copy using the given binder.
  • *
* Note : You can chain thoses methods since all of them always return * the current instance of the builder : *
 * builder.addSimpleProperties(...).addProperty(...).addBinder(...)
 * 
* Here is an example of how to use the {@link BinderModelBuilder} : *
 * BinderModelBuilder<Bean, Bean> builder = new BinderModelBuilder(Bean.class);
 * builder.addSimpleProperties("name", "surname");
 * BinderFactory.registerBinderModel(builder);
 * Binder<Bean, Bean> binder = BinderFactory.getBinder(BeanA.class);
 * 
* Once the binder is registred into the {@link BinderFactory}, you can get it * each time you need it : *
 * Binder<Bean, Bean> binder = BinderFactory.getBinder(Bean.class);
 * 
* * @param FIXME * @param FIXME * @author Tony Chemit - [email protected] * @see Binder.BinderModel * @see Binder * @since 1.5.3 */ public class BinderModelBuilder { /** * Can the source and target type mismatch for a property ? */ protected boolean canTypeMismatch; /** * current model used to build the binder */ protected Binder.BinderModel model; /** * source properties descriptors */ protected Map sourceDescriptors; /** * target properties descriptors */ protected Map targetDescriptors; /** * Creates a new mirrored and empty model binder for the given {@code type}. * * @param FIXME * @param type the type of mirrored binder * @return the new instanciated builder */ public static BinderModelBuilder newEmptyBuilder(Class type) { return new BinderModelBuilder(type, type); } /** * Creates a new empty model binder for the given types. * * @param FIXME * @param FIXME * @param sourceType type of the source of the binder * @param targetType type of the target of the binder * @return the new instanciated builder */ public static BinderModelBuilder newEmptyBuilder(Class sourceType, Class targetType) { return new BinderModelBuilder(sourceType, targetType); } /** * Creates a new mirrored model builder and fill the model with all matching * and available property from the given type. * * @param FIXME * @param sourceType the mirrored type of the binder model to create * @param the mirrored type of the binder model to create * @return the new instanciated model builder fully filled */ public static BinderModelBuilder newDefaultBuilder(Class sourceType) { return newDefaultBuilder(sourceType, sourceType); } /** * Creates a new model builder and fill the model with all matching * and available properties from the source type to the target type. * * @param sourceType the source type of the model to create * @param targetType the target type of the model to create * @param the source type of the binder model to create * @param the target type of the binder model to create * @return the new instanciated model builder fully filled */ public static BinderModelBuilder newDefaultBuilder(Class sourceType, Class targetType) { return newDefaultBuilder(sourceType, targetType, true); } /** * Creates a new model builder and fill the model with all matching * and available properties from the source type to the target type. * * @param sourceType the source type of the model to create * @param targetType the target type of the model to create * @param the source type of the binder model to create * @param the target type of the binder model to create * @param checkType flag to check if properties has same types, otherwise skip them * @return the new instanciated model builder fully filled * @since 2.4.5 */ public static BinderModelBuilder newDefaultBuilder(Class sourceType, Class targetType, boolean checkType) { BinderModelBuilder builder = newEmptyBuilder(sourceType, targetType); Map source = builder.sourceDescriptors; Map target = builder.targetDescriptors; List properties = new ArrayList(); for (String propertyName : source.keySet()) { if (!target.containsKey(propertyName)) { // not exactly match for this property, do not use this property continue; } PropertyDescriptor sourceDescriptor = source.get(propertyName); Method readMethod = sourceDescriptor.getReadMethod(); if (readMethod == null) { // no getter on source, do not use this property continue; } PropertyDescriptor targetDescriptor = target.get(propertyName); Method writeMethod = targetDescriptor.getWriteMethod(); if (writeMethod == null) { // no setter on target, do not use this property continue; } if (checkType) { // check types are compatible Class writerType = writeMethod.getParameterTypes()[0]; Class readerType = readMethod.getReturnType(); if (!writerType.equals(readerType)) { // types are not compatible continue; } } // can safely use this property properties.add(propertyName); } // add all detected properties builder.addSimpleProperties( properties.toArray(new String[properties.size()])); return builder; } /** * Change the value of property {@code canTypeMismatch}. * * @param canTypeMismatch new {@code canTypeMismatch} value * @return the builder */ public BinderModelBuilder canTypeMismatch(boolean canTypeMismatch) { this.canTypeMismatch = canTypeMismatch; return this; } public BinderModelBuilder addFunction(Class type, Function function) { model.functions.put(type, function); return this; } /** * Convinient method to create directly a {@link Binder} using the * underlying {@link #model} the builder contains. *

* Note: Using this method will not make reusable the model * via the {@link BinderFactory}. * * @return a new binder using the model of the builder. * @see BinderFactory#newBinder(Binder.BinderModel, Class) * @since 2.1 */ public Binder toBinder() { Binder binder = toBinder(Binder.class); return binder; } /** * Convinient method to create directly a {@link Binder} using the * underlying {@link #model} the builder contains. *

* Note: Using this method will not make reusable the model * via the {@link BinderFactory}. * * @param binderType type of binder to create * @param type of binder to create * @return a new binder using the model of the builder. * @see BinderFactory#newBinder(Binder.BinderModel, Class) * @since 2.1 */ public > B toBinder(Class binderType) { B binder = BinderFactory.newBinder(model, binderType); return binder; } /** * set factory of target instance * @param instanceFactory FIXME */ public void setInstanceFactory(InstanceFactory instanceFactory) { model.setInstanceFactory(instanceFactory); } /** * Add to the binder model some simple properties (says source property name = target property name). *

* Note: If no model is present, the method will fail. * * @param properties the name of mirrored property * @return the instance of the builder * @throws IllegalStateException if no model was previously created * @throws NullPointerException if a property is {@code null} */ public BinderModelBuilder addSimpleProperties(String... properties) throws IllegalStateException, NullPointerException { for (String property : properties) { if (property == null) { throw new NullPointerException( "parameter 'properties' can not contains a null value"); } addProperty0(property, property); } return this; } /** * Add to the binder model some simple properties (says source property name = target property name). *

* Note: If no model is present, the method will fail. * * @param sourceProperty the name of the source property to bind * @param targetProperty the name of the target property to bind * @return the instance of the builder * @throws IllegalStateException if no model was previously created * @throws NullPointerException if a parameter is {@code null} */ public BinderModelBuilder addProperty(String sourceProperty, String targetProperty) throws IllegalStateException, NullPointerException { if (sourceProperty == null) { throw new NullPointerException( "parameter 'sourceProperty' can not be null"); } if (targetProperty == null) { throw new NullPointerException( "parameter 'targetProperty' can not be null"); } addProperty0(sourceProperty, targetProperty); return this; } /** * Add to the binder model some properties. *

* Parameter {@code sourceAndTargetProperties} must be a array of couple * of {@code sourceProperty}, {@code targetProperty}. *

* Example : *

     * builder.addProperties("name","name2","text","text");
     * 
*

* Note: If no model is present, the method will fail. * * @param sourceAndTargetProperties the couple of (sourceProperty - * targetProperty) to bind * @return the instance of the builder * @throws IllegalStateException if no model was previously created * @throws IllegalArgumentException if there is not the same number of * source and target properties * @throws NullPointerException if a parameter is {@code null} */ public BinderModelBuilder addProperties(String... sourceAndTargetProperties) throws IllegalStateException, IllegalArgumentException, NullPointerException { if (sourceAndTargetProperties.length % 2 != 0) { throw new IllegalArgumentException( "must have couple(s) of sourceProperty,targetProperty) " + "but had " + Arrays.toString(sourceAndTargetProperties)); } for (int i = 0, max = sourceAndTargetProperties.length / 2; i < max; i++) { String sourceProperty = sourceAndTargetProperties[2 * i]; String targetProperty = sourceAndTargetProperties[2 * i + 1]; if (sourceProperty == null) { throw new NullPointerException( "parameter 'sourceAndTargetProperties' can not " + "contains a null value"); } if (targetProperty == null) { throw new NullPointerException( "parameter 'sourceAndTargetProperties' can not " + "contains a null value"); } addProperty0(sourceProperty, targetProperty); } return this; } public BinderModelBuilder addBinder(String propertyName, Binder binder) { if (model.containsCollectionProperty(propertyName)) { throw new IllegalStateException("Can't add a property binder, there is already a collection strategy defined!"); } // check property is registred if (!model.containsSourceProperty(propertyName)) { throw new IllegalArgumentException( "source property '" + propertyName + "' " + " is NOT registred."); } // check property is the same type of given binder PropertyDescriptor descriptor = sourceDescriptors.get(propertyName); Class type = descriptor.getPropertyType(); if (!Collection.class.isAssignableFrom(type) && !binder.model.getSourceType().isAssignableFrom(type)) { throw new IllegalStateException( "source property '" + propertyName + "' has not the same type [" + type + "] of the binder [" + binder.model.getSourceType() + "]."); } // can safely add the strategy model.addBinder(propertyName, binder); return this; } public BinderModelBuilder addCollectionStrategy(Binder.CollectionStrategy strategy, String... propertyNames) { if (strategy.equals(Binder.CollectionStrategy.bind)) { throw new IllegalStateException("Can't add bind stragegy here, must use the method addCollectionBinder"); } for (String propertyName : propertyNames) { if (model.containsBinderProperty(propertyName)) { throw new IllegalStateException("Can't add a simple collection strategy, there is already a binder defined, please use now the addCollectionBinder method to do this!"); } addCollectionStrategy0(propertyName, strategy, null); } return this; } public BinderModelBuilder addCollectionBinder(Binder binder, String... propertyNames) { for (String propertyName : propertyNames) { addCollectionStrategy0(propertyName, Binder.CollectionStrategy.bind, binder); } return this; } /** * Creates a new model builder inversing the the source and target of this builder. *

* the result build will contains the inversed properties mapping of the original builder. *

* Other builder attributes are not used used * * @return the new model builder */ public BinderModelBuilder buildInverseModelBuilder() { BinderModelBuilder builder = new BinderModelBuilder(model.getTargetType(), model.getSourceType()) .canTypeMismatch(canTypeMismatch); for (Map.Entry entry : model.getPropertiesMapping().entrySet()) { String sourcePropertyName = entry.getKey(); String targetPropertyName = entry.getValue(); builder.addProperty(targetPropertyName, sourcePropertyName); } return builder; } protected BinderModelBuilder addCollectionStrategy0(String propertyName, Binder.CollectionStrategy strategy, Binder binder) { // check property is registred if (!model.containsSourceProperty(propertyName)) { throw new IllegalArgumentException( "source property '" + propertyName + "' " + " is NOT registred."); } // check property is collection type PropertyDescriptor descriptor = sourceDescriptors.get(propertyName); Class type = descriptor.getPropertyType(); if (!Collection.class.isAssignableFrom(type)) { throw new IllegalStateException( "source property '" + propertyName + "' is not a collection type [" + type + "]"); } // can safely add the strategy model.addCollectionStrategy(propertyName, strategy); if (binder != null) { // add also the binder model.addBinder(propertyName, binder); } return this; } /** * Creates a binder for the given types. * * @param sourceType type of the source of the binder * @param targetType type of the target of the binder */ protected BinderModelBuilder(Class sourceType, Class targetType) { if (sourceType == null) { throw new NullPointerException("sourceType can not be null"); } if (targetType == null) { throw new NullPointerException("targetType can not be null"); } if (model != null) { throw new IllegalStateException( "there is already a binderModel in construction, release " + "it with the method createBinder before using this method." ); } // init model model = new Binder.BinderModel(sourceType, targetType); // obtain source descriptors sourceDescriptors = new TreeMap(); loadDescriptors(model.getSourceType(), sourceDescriptors); // obtain target descriptors targetDescriptors = new TreeMap(); loadDescriptors(model.getTargetType(), targetDescriptors); } protected void addProperty0(String sourceProperty, String targetProperty) { // obtain source descriptor PropertyDescriptor sourceDescriptor = sourceDescriptors.get(sourceProperty); if (sourceDescriptor == null) { throw new IllegalArgumentException("no property '" + sourceProperty + "' " + "found on type " + model.getSourceType()); } // check srcProperty is readable Method readMethod = sourceDescriptor.getReadMethod(); if (readMethod == null) { throw new IllegalArgumentException("property '" + sourceProperty + "' " + "is not readable on type " + model.getSourceType()); } // obtain dst descriptor PropertyDescriptor targetDescriptor = targetDescriptors.get(targetProperty); if (targetDescriptor == null) { throw new IllegalArgumentException("no property '" + targetProperty + "' " + "found on type " + model.getTargetType()); } // check dstProperty is writable Method writeMethod = targetDescriptor.getWriteMethod(); if (writeMethod == null) { throw new IllegalArgumentException("property '" + targetProperty + "' " + "is not writable on type " + model.getTargetType()); } // check types are ok Class sourceType = sourceDescriptor.getPropertyType(); Class targetType = targetDescriptor.getPropertyType(); //TODO-TC20100221 : should check if primitive and boxed it in such case if (!sourceType.equals(targetType) && !canTypeMismatch) { throw new IllegalArgumentException("source property '" + sourceProperty + "' and target property '" + targetProperty + "' are not compatible ( sourceType : " + sourceType + " vs targetType :" + targetType + ')'); } // check srcProperty does not exist if (model.containsSourceProperty(sourceProperty)) { // just remove the old property mapping model.removeBinding(sourceProperty); } // check dstProperty does not exist // here we can not deal with it since we should remove the source // property for the entry and this is a bit unatural if (model.containsTargetProperty(targetProperty)) { throw new IllegalArgumentException("destination property '" + targetProperty + "' " + " was already registred."); } // safe to add the binding model.addBinding(sourceDescriptor, targetDescriptor); } protected Binder.BinderModel getModel() { return model; } protected void clear() { sourceDescriptors = null; targetDescriptors = null; model = null; } protected static void loadDescriptors( Class type, Map descriptors) { try { BeanInfo beanInfo = Introspector.getBeanInfo(type); for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) { if (!descriptors.containsKey(descriptor.getName())) { descriptors.put(descriptor.getName(), descriptor); } } } catch (IntrospectionException e) { throw new RuntimeException("Could not obtain bean properties " + "descriptors for source type " + type, e); } Class[] interfaces = type.getInterfaces(); for (Class i : interfaces) { loadDescriptors(i, descriptors); } Class superClass = type.getSuperclass(); if (superClass != null && !Object.class.equals(superClass)) { loadDescriptors(superClass, descriptors); } } }