View Javadoc
1   /*
2    * #%L
3    * Nuiton Utils
4    * %%
5    * Copyright (C) 2004 - 2010 CodeLutin
6    * %%
7    * This program is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as 
9    * published by the Free Software Foundation, either version 3 of the 
10   * License, or (at your option) any later version.
11   * 
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Lesser Public License for more details.
16   * 
17   * You should have received a copy of the GNU General Lesser Public 
18   * License along with this program.  If not, see
19   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
20   * #L%
21   */
22  
23  package org.nuiton.util.beans;
24  
25  import com.google.common.base.Defaults;
26  import com.google.common.base.Function;
27  import com.google.common.base.Preconditions;
28  import org.apache.commons.lang3.ObjectUtils;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  
32  import java.beans.PropertyDescriptor;
33  import java.io.Serializable;
34  import java.lang.reflect.Method;
35  import java.lang.reflect.Type;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.HashSet;
41  import java.util.LinkedHashMap;
42  import java.util.LinkedHashSet;
43  import java.util.LinkedList;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Set;
47  import java.util.TreeMap;
48  
49  /**
50   * A {@code binder} permits to copy some properties from an object to another
51   * one.
52   *
53   * It is based on a {@link BinderModel} which contains the mapping of properties
54   * to transfert from the source object to the destination object.
55   *
56   * Use the method {@link #copy(Object, Object, String...)} to transfert properties.
57   *
58   * Use the method {@link #obtainProperties(Object, String...)} to obtain some
59   * properties from a given object.
60   *
61   * For more informations about how to obtain a binder, see the
62   * {@link BinderFactory} or the package info javadoc or unit tests...
63   *
64   * @param <I> the source bean type
65   * @param <O> the destination bean type
66   * @author Tony Chemit - chemit@codelutin.com
67   * @see BinderFactory
68   * @see BinderModelBuilder
69   * @since 1.1.5
70   */
71  public class Binder<I, O> implements Serializable {
72  
73      /** Logger. */
74      private static final Log log = LogFactory.getLog(Binder.class);
75  
76      private static final long serialVersionUID = 1L;
77  
78      /**
79       * Types of loading of collections.
80       *
81       * @since 1.3
82       */
83      public enum CollectionStrategy {
84  
85          /** To just copy the reference of the collection. */
86          copy {
87              public Object copy(Object readValue) {
88  
89                  // by default, just return same reference
90                  return readValue;
91              }
92          },
93  
94          /** To duplicate the collection */
95          duplicate {
96              @SuppressWarnings({"unchecked"})
97              @Override
98              public Object copy(Object readValue) {
99                  if (readValue instanceof LinkedHashSet<?>) {
100                     return new LinkedHashSet((Set<?>) readValue);
101                 }
102                 if (readValue instanceof Set<?>) {
103                     return new HashSet((Set<?>) readValue);
104                 }
105                 // in any other cases, let says this is a ArrayList
106                 if (readValue instanceof Collection<?>) {
107                     return new ArrayList((Collection<?>) readValue);
108                 }
109                 return readValue;
110             }
111         },
112         /**
113          * To bind the collection.
114          *
115          * <strong>Warning:</strong> Do not use it in the method {@link BinderModelBuilder#addCollectionStrategy(CollectionStrategy, String...)}
116          * since it is only use in internaly by the method {@link BinderModelBuilder#addCollectionBinder(Binder, String...)}
117          *
118          * Note: at this level, we will just create the collection.
119          */
120         bind {
121             @Override
122             public Object copy(Object readValue) {
123                 if (readValue instanceof LinkedHashSet<?>) {
124                     return new LinkedHashSet();
125                 }
126                 if (readValue instanceof Set<?>) {
127                     return new HashSet();
128                 }
129                 // in any other cases, let says this is a ArrayList
130                 if (readValue instanceof Collection<?>) {
131                     return new ArrayList();
132                 }
133                 return readValue;
134             }
135         };
136 
137         /**
138          * Copy a given collection.
139          *
140          * @param readValue the collection value to copy
141          * @return the copied collection
142          */
143         public abstract Object copy(Object readValue);
144     }
145 
146     /** the model of the binder */
147     protected BinderModel<I, O> model;
148 
149     /**
150      * Obtains the type of the source bean.
151      *
152      * @return the type of the source bean
153      */
154     public Class<I> getSourceType() {
155         return getModel().getSourceType();
156     }
157 
158     /**
159      * Obtains the type of the target bean.
160      *
161      * @return the type of the target bean
162      */
163     public Class<O> getTargetType() {
164         return getModel().getTargetType();
165     }
166 
167     /**
168      * Obtain from the given object all properties registered in the binder model.
169      *
170      * @param source                     the bean to read
171      * @param propertyNames              subset of properties to load
172      * @param keepPrimitiveDefaultValues to keep primitive default value and not replace them by a {@code null} value.
173      * @param includeNullValues          get <strong>all</strong> the properties and values for the given bean. If false, you'll get only the values
174      * @return the map of properties obtained indexed by their property name,
175      * or an empty map is the given {@code from} is {@code null}.
176      * @since 3.0
177      */
178     public Map<String, Object> obtainProperties(I source,
179                                                 boolean keepPrimitiveDefaultValues,
180                                                 boolean includeNullValues,
181                                                 String... propertyNames) {
182         if (source == null) {
183             // special limit case
184             return Collections.emptyMap();
185         }
186 
187         propertyNames = getProperties(propertyNames);
188 
189         Map<String, Object> result = new TreeMap<String, Object>();
190         for (String sourceProperty : propertyNames) {
191 
192             try {
193                 Object read;
194                 Method readMethod = model.getSourceReadMethod(sourceProperty);
195                 read = readMethod.invoke(source);
196                 if (log.isDebugEnabled()) {
197                     log.debug("property " + sourceProperty + ", type : " +
198                                       readMethod.getReturnType() + ", value = " + read);
199                 }
200                 if (readMethod.getReturnType().isPrimitive()
201                         && !keepPrimitiveDefaultValues
202                         && Defaults.defaultValue(readMethod.getReturnType()).equals(read)) {
203                     // for primitive type case, force nullity
204                     read = null;
205                 }
206                 if (read != null) {
207                     if (model.containsBinderProperty(sourceProperty)) {
208                         if (model.containsCollectionProperty(sourceProperty)) {
209                             read = bindCollection(sourceProperty, read);
210                         } else {
211                             read = bindProperty(sourceProperty, read);
212                         }
213                     } else if (model.containsCollectionProperty(sourceProperty)) {
214 
215                         // specific collection strategy is set, must use it
216                         read = getCollectionValue(sourceProperty, read);
217                     }
218                 }
219 
220                 boolean include = read != null || includeNullValues;
221                 if (include) {
222                     result.put(sourceProperty, read);
223                 }
224             } catch (Exception e) {
225                 throw new RuntimeException("Could not obtain property: " + sourceProperty, e);
226             }
227         }
228         return result;
229     }
230 
231     /**
232      * Obtain from the given object all properties registered in the binder
233      * model.
234      *
235      * @param source            the bean to read
236      * @param propertyNames     subset of properties to load
237      * @param includeNullValues get <strong>all</strong> the properties and
238      *                          values for the given bean. If false, you'll get only the values
239      * @return the map of properties obtained indexed by their property name,
240      * or an empty map is the given {@code from} is {@code null}.
241      * @since 2.3
242      */
243     public Map<String, Object> obtainProperties(I source,
244                                                 boolean includeNullValues, String... propertyNames) {
245 
246         return obtainProperties(source, false, includeNullValues, propertyNames);
247     }
248 
249     /**
250      * Obtain from the given object all properties registered in the binder
251      * model.
252      *
253      * <b>Note:</b> If a property's value is null, it will not be injected in
254      * the result.
255      *
256      * @param source        the bean to read
257      * @param propertyNames subset of properties to load
258      * @return the map of properties obtained indexed by their property name,
259      * or an empty map is the given {@code from} is {@code null}.
260      */
261     public Map<String, Object> obtainProperties(I source,
262                                                 String... propertyNames) {
263         return obtainProperties(source, false, propertyNames);
264     }
265 
266     /**
267      * Obtain a property from a source object (A source object type reflect
268      * the source type of the binder).
269      *
270      * <b>Note:</b> The property value has no special treatment, the result
271      * is the exact value from the source object (no binder collection transformation, ...).
272      *
273      * @param source       the source object to inspect
274      * @param propertyName name of the property to get
275      * @param <OO>         type of property to get
276      * @return the property value in the source object.
277      * @since 3.0
278      */
279     public <OO> OO obtainSourceProperty(I source, String propertyName) {
280 
281         Preconditions.checkNotNull(source, "source can not be null");
282         Preconditions.checkNotNull(propertyName, "propertyName can not be null");
283 
284         Method readMethod = model.getSourceReadMethod(propertyName);
285 
286         Preconditions.checkNotNull(readMethod, "Could not find source getter for property: " + propertyName);
287 
288         try {
289             OO result = (OO) readMethod.invoke(source);
290             if (log.isDebugEnabled()) {
291                 log.debug("property " + propertyName + ", type : " +
292                                   readMethod.getReturnType() + ", value = " + result);
293             }
294             return result;
295 
296         } catch (Exception e) {
297             throw new RuntimeException("Could not obtain property: " + propertyName, e);
298         }
299     }
300 
301     /**
302      * Obtain a property from a target object (A target object type reflect
303      * the target type of the binder).
304      *
305      * <b>Note:</b> The property value has no special treatment, the result
306      * is the exact value from the target object (no binder collection transformation, ...).
307      *
308      * @param target       the target object to inspect
309      * @param propertyName name of the property to get
310      * @param <OO>         type of property to get
311      * @return the property value in the target object.
312      * @since 3.0
313      */
314     public <OO> OO obtainTargetProperty(O target, String propertyName) {
315 
316         Preconditions.checkNotNull(target, "target can not be null");
317         Preconditions.checkNotNull(propertyName, "propertyName can not be null");
318 
319         Method readMethod = model.getTargetReadMethod(propertyName);
320 
321         Preconditions.checkNotNull(readMethod, "Could not find target getter for property: " + propertyName);
322 
323         try {
324             OO result = (OO) readMethod.invoke(target);
325             if (log.isDebugEnabled()) {
326                 log.debug("property " + propertyName + ", type : " +
327                                   readMethod.getReturnType() + ", value = " + result);
328             }
329             return result;
330 
331         } catch (Exception e) {
332             throw new RuntimeException("Could not obtain property: " + propertyName, e);
333         }
334 
335     }
336 
337     /**
338      * Inject all not null properties to the target bean.
339      *
340      * @param properties properties to set into bean
341      * @param target     the bean to set
342      * @since 3.0
343      */
344     public void injectProperties(Map<String, Object> properties, O target) {
345         injectProperties(properties, target, false);
346     }
347 
348     /**
349      * Inject all properties to the target bean.
350      *
351      * @param properties        properties to set into bean
352      * @param target            the bean to set
353      * @param includeNullValues {@code true} to set also null properties values
354      * @since 3.0
355      */
356     public void injectProperties(Map<String, Object> properties, O target, boolean includeNullValues) {
357 
358         boolean useFunctions = model.isUseFunctions();
359 
360         for (Map.Entry<String, Object> entry : properties.entrySet()) {
361             String propertyName = entry.getKey();
362             if (!getModel().containsTargetProperty(propertyName)) {
363 
364                 throw new IllegalStateException("Could not find property '" + propertyName + "' in binder " + this + ".");
365             }
366 
367             Object propertyValue = entry.getValue();
368             if (propertyValue == null && !includeNullValues) {
369 
370                 // Skip null value
371                 continue;
372             }
373 
374             if (log.isDebugEnabled()) {
375                 log.debug("Inject property: " + propertyName + " to " + target);
376             }
377             if (useFunctions && propertyValue != null) {
378                 propertyValue = transform(propertyName, propertyValue);
379             }
380             if (propertyValue == null) {
381                 Class<?> targetPropertyType = getTargetPropertyType(propertyName);
382                 if (targetPropertyType.isPrimitive()) {
383                     propertyValue = Defaults.defaultValue(targetPropertyType);
384                 }
385             }
386             Method writeMethod = getModel().getTargetWriteMethod(propertyName);
387             try {
388                 writeMethod.invoke(target, propertyValue);
389             } catch (Exception e) {
390                 throw new RuntimeException(
391                         "Could not set property [" +
392                                 target.getClass().getName() + ":" +
393                                 propertyName + "]", e);
394             }
395 
396         }
397 
398     }
399 
400     protected Object transform(String propertyName, Object propertyValue) {
401         Function function = model.getFunction(propertyValue.getClass());
402         if (function != null) {
403             if (log.isDebugEnabled()) {
404                 log.debug("Transform property: " + propertyName);
405             }
406             propertyValue = function.apply(propertyValue);
407         }
408         return propertyValue;
409     }
410 
411     /**
412      * Copy properties from a source bean to a destination one according to
413      * the model of the binder. If {@code propertyNames} is defined, only
414      * those properties will be copied.
415      *
416      * <b>Note:</b> If {@code from} object is null, then {@code null} values
417      * will be set to mapped properties into {@code dst}
418      *
419      * @param source        the bean to read
420      * @param target        the bean to write
421      * @param propertyNames optional subset of properties to copy (if none is
422      *                      specifed, will use all the properties defined in
423      *                      binder)
424      * @throws NullPointerException if target parameter is {@code null}
425      */
426     public void copy(I source, O target, String... propertyNames) {
427         copy(source, target, false, propertyNames);
428     }
429 
430     /**
431      * Copy properties from a source bean to a destination one according to
432      * the model of the binder excluding {@code propertyNames}.
433      *
434      * <b>Note:</b> If {@code from} object is null, then {@code null} values
435      * will be set to mapped properties into {@code dst}.
436      *
437      * @param source        the bean to read
438      * @param target        the bean to write
439      * @param propertyNames optional subset of properties to copy (if none is
440      *                      specifed, will use all the properties defined in
441      *                      binder)
442      * @throws NullPointerException if target parameter is {@code null}
443      */
444     public void copyExcluding(I source, O target, String... propertyNames) {
445         copy(source, target, true, propertyNames);
446     }
447 
448     /**
449      * Get the type of a source property.
450      *
451      * @param propertyName name of the source property
452      * @return the type of the source property
453      * @throws IllegalArgumentException if binder does not define this source property
454      */
455     public Class<?> getSourcePropertyType(String propertyName) {
456         if (!model.containsSourceProperty(propertyName)) {
457             throw new IllegalArgumentException("Binder " + this + " does not contains source property: " + propertyName);
458         }
459         return model.getSourceReadMethod(propertyName).getReturnType();
460     }
461 
462     /**
463      * Get the generic type of a source property.
464      *
465      * @param propertyName name of the source property
466      * @return the generic type of the source property
467      * @throws IllegalArgumentException if binder does not define this source property
468      */
469     public Type getSourcePropertyGenericType(String propertyName) {
470         if (!model.containsSourceProperty(propertyName)) {
471             throw new IllegalArgumentException("Binder " + this + " does not contains source property: " + propertyName);
472         }
473         return model.getSourceReadMethod(propertyName).getGenericReturnType();
474     }
475 
476     /**
477      * Get the type of a target property.
478      *
479      * @param propertyName name of the target property
480      * @return the type of the target property
481      * @throws IllegalArgumentException if binder does not define this target property
482      */
483     public Class<?> getTargetPropertyType(String propertyName) {
484         if (!model.containsTargetProperty(propertyName)) {
485             throw new IllegalArgumentException("Binder " + this + " does not contains target property: " + propertyName);
486         }
487         return model.getTargetWriteMethod(propertyName).getParameterTypes()[0];
488     }
489 
490     /**
491      * Get the generic type of a target property.
492      *
493      * @param propertyName name of the target property
494      * @return the generic type of the target property
495      * @throws IllegalArgumentException if binder does not define this target property
496      */
497     public Type getTargetPropertyGenericType(String propertyName) {
498         if (!model.containsTargetProperty(propertyName)) {
499             throw new IllegalArgumentException("Binder " + this + " does not contains target property: " + propertyName);
500         }
501         return model.getTargetWriteMethod(propertyName).getGenericParameterTypes()[0];
502     }
503 
504     /**
505      * Copy properties from a source bean to a destination one according to
506      * the model of the binder.
507      *
508      * <b>Note:</b> If {@code from} object is null, then {@code null} values
509      * will be set to mapped properties into {@code dst}.
510      *
511      * @param source            the bean to read
512      * @param target            the bean to write
513      * @param excludeProperties true to exclude following {@code propertyNames}
514      * @param propertyNames     optional subset of properties to copy (if none is
515      *                          specifed, will use all the properties defined in
516      *                          binder)
517      * @throws NullPointerException if target parameter is {@code null}
518      * @throws RuntimeException     if a property can not be copied to the target object
519      */
520     protected void copy(I source, O target, boolean excludeProperties,
521                         String... propertyNames)
522             throws RuntimeException {
523         if (target == null) {
524             throw new NullPointerException("parameter 'target' can no be null");
525         }
526 
527         propertyNames = excludeProperties ?
528                 getAllPropertiesExclude(propertyNames) :
529                 getProperties(propertyNames);
530 
531         boolean useFunctions = model.isUseFunctions();
532 
533         for (String sourceProperty : propertyNames) {
534 
535             String targetProperty = model.getTargetProperty(sourceProperty);
536 
537             try {
538                 Object read = null;
539                 Method readMethod = model.getSourceReadMethod(sourceProperty);
540                 if (source != null) {
541                     // obtain value from source
542                     read = readMethod.invoke(source);
543                 }
544                 // obtain acceptable null value (for primitive types, use
545                 // default values).
546                 if (read == null) {
547                     read = Defaults.defaultValue(readMethod.getReturnType());
548                 }
549                 if (log.isDebugEnabled()) {
550                     log.debug("property " + sourceProperty + ", type : " +
551                                       readMethod.getReturnType() + ", value = " + read);
552                 }
553 
554                 if (model.containsBinderProperty(sourceProperty)) {
555                     if (model.containsCollectionProperty(sourceProperty)) {
556                         read = bindCollection(sourceProperty, read);
557                     } else {
558                         read = bindProperty(sourceProperty, read);
559                     }
560                 } else if (model.containsCollectionProperty(sourceProperty)) {
561 
562                     // specific collection strategy is set, must use it
563                     read = getCollectionValue(sourceProperty, read);
564                 }
565                 if (useFunctions && read != null) {
566                     read = transform(sourceProperty, read);
567                 }
568                 model.getTargetWriteMethod(targetProperty).invoke(target, read);
569             } catch (Exception e) {
570                 throw new RuntimeException(
571                         "Could not bind property [" +
572                                 source.getClass().getName() + ":" +
573                                 sourceProperty + "] to [" +
574                                 target.getClass().getName() + ":" +
575                                 targetProperty + "]", e);
576             }
577         }
578     }
579 
580     protected Object readProperty(String sourceProperty, Object source,
581                                   Method readMethod) {
582         try {
583             Object read = null;
584             if (source != null) {
585                 // obtain value from source
586                 read = readMethod.invoke(source);
587             }
588             // obtain acceptable null value (for primitive types, use
589             // default values).
590             if (read == null) {
591                 read = Defaults.defaultValue(readMethod.getReturnType());
592             }
593             if (log.isDebugEnabled()) {
594                 log.debug("property " + sourceProperty + ", type : " +
595                                   readMethod.getReturnType() + ", value = " + read);
596             }
597 
598             if (model.containsBinderProperty(sourceProperty)) {
599                 if (model.containsCollectionProperty(sourceProperty)) {
600                     read = bindCollection(sourceProperty, read);
601                 } else {
602                     read = bindProperty(sourceProperty, read);
603                 }
604             } else if (model.containsCollectionProperty(sourceProperty)) {
605 
606                 // specific collection strategy is set, must use it
607                 read = getCollectionValue(sourceProperty, read);
608             }
609 
610             return read;
611         } catch (Exception e) {
612             throw new RuntimeException(
613                     "could not read property " + sourceProperty +
614                             " on source " + source);
615         }
616     }
617 
618     protected List<PropertyDiff> diff(I source,
619                                       O target,
620                                       boolean excludeProperties,
621                                       String... propertyNames) {
622         if (source == null) {
623             throw new NullPointerException("parameter 'source' can no be null");
624         }
625         if (target == null) {
626             throw new NullPointerException("parameter 'target' can no be null");
627         }
628 
629         propertyNames = excludeProperties ?
630                 getAllPropertiesExclude(propertyNames) :
631                 getProperties(propertyNames);
632 
633         List<PropertyDiff> result = new LinkedList<PropertyDiff>();
634 
635         for (String sourceProperty : propertyNames) {
636 
637             Method sourceReadMethod = model.getSourceReadMethod(sourceProperty);
638 
639             Object sourceRead = readProperty(sourceProperty, source,
640                                              sourceReadMethod);
641 
642             String targetProperty = model.getTargetProperty(sourceProperty);
643 
644             Method targetReadMethod = model.getTargetReadMethod(targetProperty);
645 
646             Object targetRead = readProperty(targetProperty, target,
647                                              targetReadMethod);
648 
649             if (ObjectUtils.notEqual(sourceRead, targetRead)) {
650                 PropertyDiff propertyDiff = new PropertyDiff(
651                         sourceProperty,
652                         sourceRead,
653                         targetProperty,
654                         targetRead,
655                         sourceReadMethod.getReturnType()
656                 );
657                 result.add(propertyDiff);
658             }
659         }
660 
661         return result;
662     }
663 
664     /**
665      * Compare two beans property by property according to the model.
666      *
667      * List contains one element per property with different values (according
668      * to the result of an equals() call)
669      *
670      * @param source a bean of type I
671      * @param target a bean of type O
672      * @return a list with all the properties which values differ in source
673      * and target. Properties with equal values are not included.
674      * @since 2.3
675      */
676     public List<PropertyDiff> diff(I source, O target) {
677         return diff(source, target, false);
678     }
679 
680     /**
681      * Compare two beans property by property according to the model.
682      *
683      * List contains one element per property with different values (according
684      * to the result of an equals() call)
685      *
686      * @param source        a bean of type I
687      * @param target        a bean of type O
688      * @param propertyNames property names to exclude from the diff
689      * @return a list with all the properties which values differ in source
690      * and target. Properties with equal values and excluded properties
691      * will not be contained in the result
692      * @since 2.3
693      */
694     public List<PropertyDiff> diffExcluding(I source,
695                                             O target,
696                                             String... propertyNames) {
697         return diff(source, target, true, propertyNames);
698     }
699 
700     /**
701      * Get the model of the binder.
702      *
703      * @return the model of the binder
704      */
705     protected BinderModel<I, O> getModel() {
706         return model;
707     }
708 
709     /**
710      * Set the model of the binder.
711      *
712      * @param model the model of the binder
713      */
714     protected void setModel(BinderModel<I, O> model) {
715         this.model = model;
716     }
717 
718     /**
719      * Obtain the properties, if none is given in {@code propertyNames}
720      * parameter, will use all property names defined in binder's model,
721      * otherwise, check that all given property names are safe (registred in
722      * binder's model).
723      *
724      * @param propertyNames optional subset of properties to get
725      * @return the array of property names
726      */
727     protected String[] getProperties(String... propertyNames) {
728 
729         if (propertyNames.length == 0) {
730             // use all properties in the binder
731             propertyNames = model.getSourceDescriptors();
732         } else {
733 
734             // use a subset of properties, must check them
735             for (String propertyName : propertyNames) {
736                 if (!model.containsSourceProperty(propertyName)) {
737                     throw new IllegalArgumentException(
738                             "property '" + propertyName +
739                                     "' is not known by binder");
740                 }
741             }
742         }
743         return propertyNames;
744     }
745 
746     /**
747      * Obtains all properties from binder's model except those {@code
748      * propertyNameExcludes}. Unknown properties will be ignored.
749      *
750      * @param propertyNameExcludes name of properties to exclude
751      * @return the array of property names without those in argument
752      */
753     protected String[] getAllPropertiesExclude(String... propertyNameExcludes) {
754         List<String> excludes = Arrays.asList(propertyNameExcludes);
755         List<String> results = new ArrayList<String>();
756         for (String propertyName : model.getSourceDescriptors()) {
757             if (!excludes.contains(propertyName)) {
758                 results.add(propertyName);
759             }
760         }
761         return results.toArray(new String[results.size()]);
762     }
763 
764     protected Object getCollectionValue(String sourceProperty, Object readValue) {
765         CollectionStrategy strategy =
766                 model.getCollectionStrategy(sourceProperty);
767         Object result = strategy.copy(readValue);
768         return result;
769     }
770 
771     protected Object bindProperty(String sourceProperty, Object read) throws IllegalAccessException, InstantiationException {
772         Binder<?, ?> binder = model.getBinder(sourceProperty);
773         Object result = bind(binder, read);
774         return result;
775     }
776 
777     @SuppressWarnings({"unchecked"})
778     protected Object bindCollection(String sourceProperty, Object read) throws IllegalAccessException, InstantiationException {
779 
780         if (read == null) {
781             return null;
782         }
783 
784         Binder<?, ?> binder = model.getBinder(sourceProperty);
785 
786         Collection result = (Collection) model.getCollectionStrategy(sourceProperty).copy(read);
787 
788 //        if (read instanceof LinkedHashSet<?>) {
789 //            result = new LinkedHashSet();
790 //        } else if (read instanceof Set<?>) {
791 //            result = new HashSet();
792 //        } else {
793 //
794 //            // in any other cases, let says this is a ArrayList
795 //            result = new ArrayList();
796 //        }
797         Collection<?> collection = (Collection<?>) read;
798         for (Object o : collection) {
799             Object r = bind(binder, o);
800             result.add(r);
801         }
802         return result;
803     }
804 
805     @SuppressWarnings({"unchecked"})
806     protected Object bind(Binder binder, Object read) throws IllegalAccessException, InstantiationException {
807         Object result = null;
808         if (read != null) {
809             InstanceFactory<O> instanceFactory = binder.model.getInstanceFactory();
810             if (instanceFactory == null) {
811                 result = read.getClass().newInstance();
812             } else {
813                 result = instanceFactory.newInstance();
814             }
815             binder.copy(read, result);
816         }
817         return result;
818     }
819 
820     /**
821      * Model of a {@link Binder}.
822      *
823      * TODO tchemit 20100225 should have special cases for collections treatment.
824      *
825      * @param <S> the source type
826      * @param <T> the target type
827      * @author Tony Chemit - chemit@codelutin.com
828      * @since 1.1.5
829      */
830     public static class BinderModel<S, T> implements Serializable {
831 
832         /** source type */
833         protected final Class<S> sourceType;
834 
835         /** destination type */
836         protected final Class<T> targetType;
837 
838         /** source type descriptors (key are property names) */
839         protected final Map<String, PropertyDescriptor> sourceDescriptors;
840 
841         /** destination descriptors (key are property names) */
842         protected final Map<String, PropertyDescriptor> targetDescriptors;
843 
844         /**
845          * properties mapping (key are source properties, value are destination
846          * properties)
847          */
848         protected final Map<String, String> propertiesMapping;
849 
850         /** mapping of collection properties strategies */
851         protected Map<String, CollectionStrategy> collectionStrategies;
852 
853         /** mapping of extra binders to use to copy properties */
854         protected Map<String, Binder<?, ?>> binders;
855 
856         /** factory of target Instance */
857         protected InstanceFactory<T> instanceFactory;
858 
859         /**
860          * Dictonnary of function to apply by source class type.
861          */
862         protected final Map<Class<?>, Function<?, ?>> functions;
863 
864         private static final long serialVersionUID = 2L;
865 
866         public BinderModel(Class<S> sourceType, Class<T> targetType) {
867             this.sourceType = sourceType;
868             this.targetType = targetType;
869             sourceDescriptors = new TreeMap<String, PropertyDescriptor>();
870             targetDescriptors = new TreeMap<String, PropertyDescriptor>();
871             propertiesMapping = new TreeMap<String, String>();
872             collectionStrategies = new TreeMap<String, CollectionStrategy>();
873             binders = new TreeMap<String, Binder<?, ?>>();
874             functions = new LinkedHashMap<Class<?>, Function<?, ?>>();
875         }
876 
877         /**
878          * Gets the type of the binder's source.
879          *
880          * @return the type of the source object in the binder
881          */
882         public Class<S> getSourceType() {
883             return sourceType;
884         }
885 
886         /**
887          * Gets the type of the binder's destination
888          *
889          * @return the type of the destination object in the binder
890          */
891         public Class<T> getTargetType() {
892             return targetType;
893         }
894 
895         /**
896          * Gets all registred property names of the binder's source type.
897          *
898          * @return the array of all source object properties names to bind
899          */
900         public String[] getSourceDescriptors() {
901             Set<String> universe = sourceDescriptors.keySet();
902             return universe.toArray(new String[sourceDescriptors.size()]);
903         }
904 
905         public CollectionStrategy getCollectionStrategy(String property) {
906             return collectionStrategies.get(property);
907         }
908 
909         /**
910          * Gets all registred property names of the binder's destination type.
911          *
912          * @return the array of all source object properties names to bind
913          */
914         public String[] getTargetDescriptors() {
915             Set<String> universe = targetDescriptors.keySet();
916             return universe.toArray(new String[targetDescriptors.size()]);
917         }
918 
919         /**
920          * Gets the destination property name given the
921          *
922          * @param sourceProperty the name of the source property to bind
923          * @return the name of the destination object property to bind, or
924          * {@code null} if {@code propertySrc} is unknown in the model
925          */
926         public String getTargetProperty(String sourceProperty) {
927             if (!containsSourceProperty(sourceProperty)) {
928                 return null;
929             }
930             String dstProperty = propertiesMapping.get(sourceProperty);
931             return dstProperty;
932         }
933 
934         /**
935          * Gets the bean descriptor of the source type for the given
936          * destination property.
937          *
938          * @param sourceProperty name of the source type property name
939          * @return the descriptor or {@code null} if not found.
940          */
941         public PropertyDescriptor getSourceDescriptor(String sourceProperty) {
942             // check src property is registred
943             if (!containsSourceProperty(sourceProperty)) {
944                 return null;
945             }
946             PropertyDescriptor descriptor = sourceDescriptors.get(sourceProperty);
947             return descriptor;
948         }
949 
950         /**
951          * @param srcProperty the name of a property of the source object.
952          * @return the method to read in a source object for the given property.
953          */
954         public Method getSourceReadMethod(String srcProperty) {
955             PropertyDescriptor descriptor = getSourceDescriptor(srcProperty);
956             Method readMethod = null;
957             if (descriptor != null) {
958                 readMethod = descriptor.getReadMethod();
959             }
960             return readMethod;
961         }
962 
963         /**
964          * @param sourceProperty the name of a property of the source object.
965          * @return the method to write in a source object for the given property.
966          */
967         public Method getSourceWriteMethod(String sourceProperty) {
968             PropertyDescriptor descriptor = getSourceDescriptor(sourceProperty);
969             Method writeMethod = null;
970             if (descriptor != null) {
971                 writeMethod = descriptor.getWriteMethod();
972             }
973             return writeMethod;
974         }
975 
976         /**
977          * Gets the bean descriptor of the destination type for the given
978          * destination property.
979          *
980          * @param targetProperty name of the destination type property name
981          * @return the descriptor or {@code null} if not found.
982          */
983         public PropertyDescriptor getTargetDescriptor(String targetProperty) {
984             // check dst property is registred
985             if (!containsTargetProperty(targetProperty)) {
986                 return null;
987             }
988             PropertyDescriptor descriptor = targetDescriptors.get(targetProperty);
989             return descriptor;
990         }
991 
992         /**
993          * @param targetProperty the name of a property of the destination object.
994          * @return the method to read in a destination object for the given
995          * property.
996          */
997         public Method getTargetReadMethod(String targetProperty) {
998             PropertyDescriptor descriptor = getTargetDescriptor(targetProperty);
999             Method readMethod = null;
1000             if (descriptor != null) {
1001                 readMethod = descriptor.getReadMethod();
1002             }
1003             return readMethod;
1004         }
1005 
1006         /**
1007          * @param targetProperty the name of a property of the destination object.
1008          * @return the method to write in a destination object for the given
1009          * property.
1010          */
1011         public Method getTargetWriteMethod(String targetProperty) {
1012             PropertyDescriptor descriptor = getTargetDescriptor(targetProperty);
1013             Method writeMethod = null;
1014             if (descriptor != null) {
1015                 writeMethod = descriptor.getWriteMethod();
1016             }
1017             return writeMethod;
1018         }
1019 
1020         public Class<?> getCollectionType(String sourceProperty) {
1021             Method method = getSourceReadMethod(sourceProperty);
1022             Class<?> type = method.getReturnType();
1023             if (Collection.class.isAssignableFrom(type)) {
1024                 return type;
1025             }
1026             return null;
1027         }
1028 
1029         public void addCollectionStrategy(String propertyName,
1030                                           CollectionStrategy strategy) {
1031             collectionStrategies.put(propertyName, strategy);
1032         }
1033 
1034         public void addBinder(String propertyName, Binder<?, ?> binder) {
1035             binders.put(propertyName, binder);
1036         }
1037 
1038         public boolean containsSourceProperty(String sourceProperty) {
1039             return propertiesMapping.containsKey(sourceProperty);
1040         }
1041 
1042         public boolean containsTargetProperty(String targetProperty) {
1043             return propertiesMapping.containsValue(targetProperty);
1044         }
1045 
1046         public boolean containsCollectionProperty(String propertyName) {
1047             return collectionStrategies.containsKey(propertyName);
1048         }
1049 
1050         public boolean containsBinderProperty(String propertyName) {
1051             return binders.containsKey(propertyName);
1052         }
1053 
1054         protected void addBinding(PropertyDescriptor sourceDescriptor,
1055                                   PropertyDescriptor targetDescriptor) {
1056 
1057             String sourceProperty = sourceDescriptor.getName();
1058             String targetProperty = targetDescriptor.getName();
1059             sourceDescriptors.put(sourceProperty, sourceDescriptor);
1060             targetDescriptors.put(targetProperty, targetDescriptor);
1061             propertiesMapping.put(sourceProperty, targetProperty);
1062         }
1063 
1064         protected void removeBinding(String source) {
1065             String target = propertiesMapping.get(source);
1066 
1067             sourceDescriptors.remove(source);
1068             targetDescriptors.remove(target);
1069             propertiesMapping.remove(source);
1070 
1071             if (containsBinderProperty(source)) {
1072                 binders.remove(source);
1073             }
1074             if (containsCollectionProperty(source)) {
1075                 collectionStrategies.remove(source);
1076             }
1077         }
1078 
1079         protected Map<String, String> getPropertiesMapping() {
1080             return propertiesMapping;
1081         }
1082 
1083         public Binder<?, ?> getBinder(String sourceProperty) {
1084             return binders.get(sourceProperty);
1085         }
1086 
1087         public void setInstanceFactory(InstanceFactory<T> instanceFactory) {
1088             this.instanceFactory = instanceFactory;
1089         }
1090 
1091         public InstanceFactory<T> getInstanceFactory() {
1092             return instanceFactory;
1093         }
1094 
1095         public Function getFunction(Class<?> aClass) {
1096             return functions.get(aClass);
1097         }
1098 
1099         public boolean isUseFunctions() {
1100             return !functions.isEmpty();
1101         }
1102 
1103     }
1104 }