
org.nuiton.util.beans.BinderModelBuilder Maven / Gradle / Ivy
Show all versions of nuiton-utils Show documentation
/*
* #%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);
}
}
}