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.Function;
26  
27  import java.beans.BeanInfo;
28  import java.beans.IntrospectionException;
29  import java.beans.Introspector;
30  import java.beans.PropertyDescriptor;
31  import java.lang.reflect.Method;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.TreeMap;
38  
39  /**
40   * Class to create a new {@link Binder.BinderModel}.
41   * <p>
42   * A such object is designed to build only one model of binder and can not be
43   * used directly to create a new binder, it prepares only the model of a new
44   * binder, which after must be registred in the {@link BinderFactory} to obtain
45   * a real {@link Binder}.
46   * <p>
47   * If you want to create more than one binder model, use each time a new
48   * binder builder.
49   * <p>
50   * To obtain a new instance of a build please use one of the factories method :
51   * <ul>
52   * <li>{@link #newEmptyBuilder(Class)}} to create a binder model with same
53   * source and target type</li>
54   * <li>{@link #newEmptyBuilder(Class, Class)} to create a binder model with a
55   * possible different source and target type</li>
56   * <li>{@link #newDefaultBuilder(Class)} to create a binder model with same
57   * source and target type and then fill the model with all matching properties.</li>
58   * <li>{@link #newDefaultBuilder(Class, Class)} to create a binder model
59   * with a possible different source and target type and then fill the model
60   * with all matching properties.</li>
61   * </ul>
62   * Then you can use folowing methods to specify what to put in the copy model :
63   * <ul>
64   * <li>{@link #addSimpleProperties(String...)} to add in the binder model simple
65   * properties (a simple property is a property present in both source and target type)</li>
66   * <li>{@link #addProperty(String, String)} to add in the binder model a single
67   * property (from source type) to be copied to another property (in target type)</li>
68   * <li>{@link #addProperties(String...)} to add in the binder model properties
69   * (says here you specify some couple of properties (sourcePropertyName,
70   * targetPropertyName) to be added in the binder model)</li>
71   * <li>{@link #addBinder(String, Binder)} to add in the binder model
72   * another binder to be used to copy the given simple property (same name in
73   * source and target type)</li>
74   * <li>{@link #addCollectionStrategy(Binder.CollectionStrategy, String...)} to
75   * specify the strategy to be used to bind some collection some source type to
76   * target type</li>
77   * <li>{@link #addCollectionBinder(Binder, String...)} to
78   * bind a collection: a new collection will be created and all elements of sources will be
79   * copy using the given binder.</li>
80   * </ul>
81   * <b>Note :</b> You can chain thoses methods since all of them always return
82   * the current instance of the builder :
83   * <pre>
84   * builder.addSimpleProperties(...).addProperty(...).addBinder(...)
85   * </pre>
86   * Here is an example of how to use the {@link BinderModelBuilder} :
87   * <pre>
88   * BinderModelBuilder&lt;Bean, Bean&gt; builder = new BinderModelBuilder(Bean.class);
89   * builder.addSimpleProperties("name", "surname");
90   * BinderFactory.registerBinderModel(builder);
91   * Binder&lt;Bean, Bean&gt; binder = BinderFactory.getBinder(BeanA.class);
92   * </pre>
93   * Once the binder is registred into the {@link BinderFactory}, you can get it
94   * each time you need it :
95   * <pre>
96   * Binder&lt;Bean, Bean&gt; binder = BinderFactory.getBinder(Bean.class);
97   * </pre>
98   *
99   * @param <S> FIXME
100  * @param <T> FIXME
101  * @author Tony Chemit - chemit@codelutin.com
102  * @see Binder.BinderModel
103  * @see Binder
104  * @since 1.5.3
105  */
106 public class BinderModelBuilder<S, T> {
107 
108     /**
109      * Can the source and target type mismatch for a property ?
110      */
111     protected boolean canTypeMismatch;
112 
113     /**
114      * current model used to build the binder
115      */
116     protected Binder.BinderModel<S, T> model;
117 
118     /**
119      * source properties descriptors
120      */
121     protected Map<String, PropertyDescriptor> sourceDescriptors;
122 
123     /**
124      * target properties descriptors
125      */
126     protected Map<String, PropertyDescriptor> targetDescriptors;
127 
128     /**
129      * Creates a new mirrored and empty model binder for the given {@code type}.
130      *
131      * @param <S>  FIXME
132      * @param type the type of mirrored binder
133      * @return the new instanciated builder
134      */
135     public static <S> BinderModelBuilder<S, S> newEmptyBuilder(Class<S> type) {
136         return new BinderModelBuilder<S, S>(type, type);
137     }
138 
139     /**
140      * Creates a new empty model binder for the given types.
141      *
142      * @param <S>        FIXME
143      * @param <T>        FIXME
144      * @param sourceType type of the source of the binder
145      * @param targetType type of the target of the binder
146      * @return the new instanciated builder
147      */
148     public static <S, T> BinderModelBuilder<S, T> newEmptyBuilder(Class<S> sourceType,
149                                                                   Class<T> targetType) {
150         return new BinderModelBuilder<S, T>(sourceType, targetType);
151     }
152 
153     /**
154      * Creates a new mirrored model builder and fill the model with all matching
155      * and available property from the given type.
156      *
157      * @param <S>        FIXME
158      * @param sourceType the mirrored type of the binder model to create
159      * @param <S>        the mirrored type of the binder model to create
160      * @return the new instanciated model builder fully filled
161      */
162     public static <S> BinderModelBuilder<S, S> newDefaultBuilder(Class<S> sourceType) {
163         return newDefaultBuilder(sourceType, sourceType);
164 
165     }
166 
167     /**
168      * Creates a new model builder and fill the model with all matching
169      * and available properties from the source type to the target type.
170      *
171      * @param sourceType the source type of the model to create
172      * @param targetType the target type of the model to create
173      * @param <S>        the source type of the binder model to create
174      * @param <T>        the target type of the binder model to create
175      * @return the new instanciated model builder fully filled
176      */
177     public static <S, T> BinderModelBuilder<S, T> newDefaultBuilder(Class<S> sourceType,
178                                                                     Class<T> targetType) {
179         return newDefaultBuilder(sourceType, targetType, true);
180     }
181 
182     /**
183      * Creates a new model builder and fill the model with all matching
184      * and available properties from the source type to the target type.
185      *
186      * @param sourceType the source type of the model to create
187      * @param targetType the target type of the model to create
188      * @param <S>        the source type of the binder model to create
189      * @param <T>        the target type of the binder model to create
190      * @param checkType  flag to check if properties has same types, otherwise skip them
191      * @return the new instanciated model builder fully filled
192      * @since 2.4.5
193      */
194     public static <S, T> BinderModelBuilder<S, T> newDefaultBuilder(Class<S> sourceType,
195                                                                     Class<T> targetType,
196                                                                     boolean checkType) {
197         BinderModelBuilder<S, T> builder =
198                 newEmptyBuilder(sourceType, targetType);
199         Map<String, PropertyDescriptor> source = builder.sourceDescriptors;
200         Map<String, PropertyDescriptor> target = builder.targetDescriptors;
201         List<String> properties = new ArrayList<String>();
202         for (String propertyName : source.keySet()) {
203             if (!target.containsKey(propertyName)) {
204 
205                 // not exactly match for this property, do not use this property
206                 continue;
207             }
208             PropertyDescriptor sourceDescriptor = source.get(propertyName);
209             Method readMethod = sourceDescriptor.getReadMethod();
210             if (readMethod == null) {
211 
212                 // no getter on source, do not use this property
213                 continue;
214             }
215             PropertyDescriptor targetDescriptor = target.get(propertyName);
216             Method writeMethod = targetDescriptor.getWriteMethod();
217             if (writeMethod == null) {
218 
219                 // no setter on target, do not use this property
220                 continue;
221             }
222 
223             if (checkType) {
224 
225                 // check types are compatible
226 
227                 Class<?> writerType = writeMethod.getParameterTypes()[0];
228                 Class<?> readerType = readMethod.getReturnType();
229                 if (!writerType.equals(readerType)) {
230 
231                     // types are not compatible
232                     continue;
233                 }
234             }
235             // can safely use this property
236             properties.add(propertyName);
237         }
238 
239         // add all detected properties
240         builder.addSimpleProperties(
241                 properties.toArray(new String[properties.size()]));
242         return builder;
243     }
244 
245     /**
246      * Change the value of property {@code canTypeMismatch}.
247      *
248      * @param canTypeMismatch new {@code canTypeMismatch} value
249      * @return the builder
250      */
251     public BinderModelBuilder<S, T> canTypeMismatch(boolean canTypeMismatch) {
252         this.canTypeMismatch = canTypeMismatch;
253         return this;
254     }
255 
256     public <K, V> BinderModelBuilder<S, T> addFunction(Class<K> type, Function<K, V> function) {
257         model.functions.put(type, function);
258         return this;
259     }
260 
261     /**
262      * Convinient method to create directly a {@link Binder} using the
263      * underlying {@link #model} the builder contains.
264      * <p>
265      * <strong>Note:</strong> Using this method will not make reusable the model
266      * via the {@link BinderFactory}.
267      *
268      * @return a new binder using the model of the builder.
269      * @see BinderFactory#newBinder(Binder.BinderModel, Class)
270      * @since 2.1
271      */
272     public Binder<S, T> toBinder() {
273         Binder<S, T> binder = toBinder(Binder.class);
274         return binder;
275     }
276 
277     /**
278      * Convinient method to create directly a {@link Binder} using the
279      * underlying {@link #model} the builder contains.
280      * <p>
281      * <strong>Note:</strong> Using this method will not make reusable the model
282      * via the {@link BinderFactory}.
283      *
284      * @param binderType type of binder to create
285      * @param <B>        type of binder to create
286      * @return a new binder using the model of the builder.
287      * @see BinderFactory#newBinder(Binder.BinderModel, Class)
288      * @since 2.1
289      */
290     public <B extends Binder<S, T>> B toBinder(Class<B> binderType) {
291         B binder = BinderFactory.newBinder(model, binderType);
292         return binder;
293     }
294 
295     /**
296      * set factory of target instance
297      * @param instanceFactory FIXME
298      */
299     public void setInstanceFactory(InstanceFactory<T> instanceFactory) {
300         model.setInstanceFactory(instanceFactory);
301     }
302 
303     /**
304      * Add to the binder model some simple properties (says source property name = target property name).
305      * <p>
306      * <b>Note:</b> If no model is present, the method will fail.
307      *
308      * @param properties the name of mirrored property
309      * @return the instance of the builder
310      * @throws IllegalStateException if no model was previously created
311      * @throws NullPointerException  if a property is {@code null}
312      */
313     public BinderModelBuilder<S, T> addSimpleProperties(String... properties)
314             throws IllegalStateException, NullPointerException {
315         for (String property : properties) {
316             if (property == null) {
317                 throw new NullPointerException(
318                         "parameter 'properties' can not contains a null value");
319             }
320             addProperty0(property, property);
321         }
322         return this;
323     }
324 
325     /**
326      * Add to the binder model some simple properties (says source property name = target property name).
327      * <p>
328      * <b>Note:</b> If no model is present, the method will fail.
329      *
330      * @param sourceProperty the name of the source property to bind
331      * @param targetProperty the name of the target property to bind
332      * @return the instance of the builder
333      * @throws IllegalStateException if no model was previously created
334      * @throws NullPointerException  if a parameter is {@code null}
335      */
336 
337     public BinderModelBuilder<S, T> addProperty(String sourceProperty,
338                                                 String targetProperty)
339             throws IllegalStateException, NullPointerException {
340         if (sourceProperty == null) {
341             throw new NullPointerException(
342                     "parameter 'sourceProperty' can not be null");
343         }
344         if (targetProperty == null) {
345             throw new NullPointerException(
346                     "parameter 'targetProperty' can not be null");
347         }
348         addProperty0(sourceProperty, targetProperty);
349         return this;
350     }
351 
352     /**
353      * Add to the binder model some properties.
354      * <p>
355      * Parameter {@code sourceAndTargetProperties} must be a array of couple
356      * of {@code sourceProperty}, {@code targetProperty}.
357      * <p>
358      * Example :
359      * <pre>
360      * builder.addProperties("name","name2","text","text");
361      * </pre>
362      * <p>
363      * <b>Note:</b> If no model is present, the method will fail.
364      *
365      * @param sourceAndTargetProperties the couple of (sourceProperty -
366      *                                  targetProperty) to bind
367      * @return the instance of the builder
368      * @throws IllegalStateException    if no model was previously created
369      * @throws IllegalArgumentException if there is not the same number of
370      *                                  source and target properties
371      * @throws NullPointerException     if a parameter is {@code null}
372      */
373     public BinderModelBuilder<S, T> addProperties(String... sourceAndTargetProperties)
374             throws IllegalStateException, IllegalArgumentException,
375             NullPointerException {
376         if (sourceAndTargetProperties.length % 2 != 0) {
377             throw new IllegalArgumentException(
378                     "must have couple(s) of sourceProperty,targetProperty) " +
379                             "but had " + Arrays.toString(sourceAndTargetProperties));
380         }
381         for (int i = 0, max = sourceAndTargetProperties.length / 2;
382              i < max; i++) {
383             String sourceProperty = sourceAndTargetProperties[2 * i];
384             String targetProperty = sourceAndTargetProperties[2 * i + 1];
385             if (sourceProperty == null) {
386                 throw new NullPointerException(
387                         "parameter 'sourceAndTargetProperties' can not " +
388                                 "contains a null value");
389             }
390             if (targetProperty == null) {
391                 throw new NullPointerException(
392                         "parameter 'sourceAndTargetProperties' can not " +
393                                 "contains a null value");
394             }
395             addProperty0(sourceProperty, targetProperty);
396         }
397         return this;
398     }
399 
400     public BinderModelBuilder<S, T> addBinder(String propertyName, Binder<?, ?> binder) {
401 
402         if (model.containsCollectionProperty(propertyName)) {
403 
404             throw new IllegalStateException("Can't add a property binder, there is already a collection strategy defined!");
405         }
406 
407         // check property is registred
408         if (!model.containsSourceProperty(propertyName)) {
409             throw new IllegalArgumentException(
410                     "source property '" + propertyName + "' " +
411                             " is NOT registred.");
412         }
413 
414         // check property is the same type of given binder
415         PropertyDescriptor descriptor = sourceDescriptors.get(propertyName);
416         Class<?> type = descriptor.getPropertyType();
417 
418         if (!Collection.class.isAssignableFrom(type) &&
419                 !binder.model.getSourceType().isAssignableFrom(type)) {
420             throw new IllegalStateException(
421                     "source property '" + propertyName +
422                             "' has not the same type [" + type +
423                             "] of the binder [" + binder.model.getSourceType() + "].");
424         }
425 
426         // can safely add the strategy
427         model.addBinder(propertyName, binder);
428 
429         return this;
430     }
431 
432     public BinderModelBuilder<S, T> addCollectionStrategy(Binder.CollectionStrategy strategy,
433                                                           String... propertyNames) {
434 
435         if (strategy.equals(Binder.CollectionStrategy.bind)) {
436             throw new IllegalStateException("Can't add bind stragegy here, must use the method addCollectionBinder");
437         }
438 
439         for (String propertyName : propertyNames) {
440 
441             if (model.containsBinderProperty(propertyName)) {
442 
443                 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!");
444             }
445 
446             addCollectionStrategy0(propertyName, strategy, null);
447 
448         }
449         return this;
450     }
451 
452     public BinderModelBuilder<S, T> addCollectionBinder(Binder binder,
453                                                         String... propertyNames) {
454 
455         for (String propertyName : propertyNames) {
456 
457             addCollectionStrategy0(propertyName, Binder.CollectionStrategy.bind, binder);
458 
459         }
460 
461         return this;
462     }
463 
464     /**
465      * Creates a new model builder inversing the the source and target of this builder.
466      * <p>
467      * the result build will contains the inversed properties mapping of the original builder.
468      * <p>
469      * Other builder attributes are not used used
470      *
471      * @return the new model builder
472      */
473     public BinderModelBuilder<T, S> buildInverseModelBuilder() {
474 
475         BinderModelBuilder<T, S> builder = new BinderModelBuilder<T, S>(model.getTargetType(), model.getSourceType())
476                 .canTypeMismatch(canTypeMismatch);
477 
478         for (Map.Entry<String, String> entry : model.getPropertiesMapping().entrySet()) {
479             String sourcePropertyName = entry.getKey();
480             String targetPropertyName = entry.getValue();
481             builder.addProperty(targetPropertyName, sourcePropertyName);
482         }
483         return builder;
484 
485     }
486 
487     protected BinderModelBuilder<S, T> addCollectionStrategy0(String propertyName,
488                                                               Binder.CollectionStrategy strategy,
489                                                               Binder binder) {
490 
491         // check property is registred
492         if (!model.containsSourceProperty(propertyName)) {
493             throw new IllegalArgumentException(
494                     "source property '" + propertyName + "' " +
495                             " is NOT registred.");
496         }
497 
498         // check property is collection type
499         PropertyDescriptor descriptor = sourceDescriptors.get(propertyName);
500         Class<?> type = descriptor.getPropertyType();
501         if (!Collection.class.isAssignableFrom(type)) {
502             throw new IllegalStateException(
503                     "source property '" + propertyName +
504                             "' is not a collection type [" + type + "]");
505         }
506 
507         // can safely add the strategy
508         model.addCollectionStrategy(propertyName, strategy);
509 
510         if (binder != null) {
511 
512             // add also the binder
513             model.addBinder(propertyName, binder);
514 
515         }
516         return this;
517     }
518 
519     /**
520      * Creates a binder for the given types.
521      *
522      * @param sourceType type of the source of the binder
523      * @param targetType type of the target of the binder
524      */
525     protected BinderModelBuilder(Class<S> sourceType, Class<T> targetType) {
526         if (sourceType == null) {
527             throw new NullPointerException("sourceType can not be null");
528         }
529         if (targetType == null) {
530             throw new NullPointerException("targetType can not be null");
531         }
532 
533         if (model != null) {
534             throw new IllegalStateException(
535                     "there is already a binderModel in construction, release " +
536                             "it with the method createBinder before using this method."
537             );
538         }
539 
540         // init model
541         model = new Binder.BinderModel<S, T>(sourceType, targetType);
542 
543         // obtain source descriptors
544         sourceDescriptors = new TreeMap<String, PropertyDescriptor>();
545         loadDescriptors(model.getSourceType(), sourceDescriptors);
546 
547         // obtain target descriptors
548         targetDescriptors = new TreeMap<String, PropertyDescriptor>();
549         loadDescriptors(model.getTargetType(), targetDescriptors);
550 
551     }
552 
553     protected void addProperty0(String sourceProperty,
554                                 String targetProperty) {
555 
556         // obtain source descriptor
557         PropertyDescriptor sourceDescriptor =
558                 sourceDescriptors.get(sourceProperty);
559         if (sourceDescriptor == null) {
560             throw new IllegalArgumentException("no property '" +
561                     sourceProperty + "' " + "found on type " +
562                     model.getSourceType());
563         }
564         // check srcProperty is readable
565         Method readMethod = sourceDescriptor.getReadMethod();
566         if (readMethod == null) {
567             throw new IllegalArgumentException("property '" + sourceProperty +
568                     "' " + "is not readable on type " + model.getSourceType());
569         }
570 
571         // obtain dst descriptor
572         PropertyDescriptor targetDescriptor =
573                 targetDescriptors.get(targetProperty);
574         if (targetDescriptor == null) {
575             throw new IllegalArgumentException("no property '" +
576                     targetProperty + "' " + "found on type " +
577                     model.getTargetType());
578         }
579         // check dstProperty is writable
580         Method writeMethod = targetDescriptor.getWriteMethod();
581         if (writeMethod == null) {
582             throw new IllegalArgumentException("property '" + targetProperty +
583                     "' " + "is not writable on type " + model.getTargetType());
584         }
585 
586         // check types are ok
587         Class<?> sourceType = sourceDescriptor.getPropertyType();
588         Class<?> targetType = targetDescriptor.getPropertyType();
589         //TODO-TC20100221 : should check if primitive and boxed it in such case
590         if (!sourceType.equals(targetType) && !canTypeMismatch) {
591             throw new IllegalArgumentException("source property '" +
592                     sourceProperty + "' and target property '" +
593                     targetProperty + "' are not compatible ( sourceType : " +
594                     sourceType + " vs targetType :" + targetType + ')');
595         }
596 
597         // check srcProperty does not exist
598         if (model.containsSourceProperty(sourceProperty)) {
599 
600             // just remove the old property mapping
601             model.removeBinding(sourceProperty);
602 
603         }
604 
605         // check dstProperty does not exist
606         // here we can not deal with it since we should remove the source
607         // property for the entry and this is a bit unatural
608         if (model.containsTargetProperty(targetProperty)) {
609             throw new IllegalArgumentException("destination property '" +
610                     targetProperty + "' " + " was already registred.");
611         }
612         // safe to add the binding
613         model.addBinding(sourceDescriptor, targetDescriptor);
614     }
615 
616     protected Binder.BinderModel<S, T> getModel() {
617         return model;
618     }
619 
620     protected void clear() {
621         sourceDescriptors = null;
622         targetDescriptors = null;
623         model = null;
624     }
625 
626     protected static void loadDescriptors(
627             Class<?> type,
628             Map<String, PropertyDescriptor> descriptors) {
629         try {
630 
631             BeanInfo beanInfo = Introspector.getBeanInfo(type);
632             for (PropertyDescriptor descriptor :
633                     beanInfo.getPropertyDescriptors()) {
634                 if (!descriptors.containsKey(descriptor.getName())) {
635                     descriptors.put(descriptor.getName(), descriptor);
636                 }
637             }
638         } catch (IntrospectionException e) {
639             throw new RuntimeException("Could not obtain bean properties " +
640                     "descriptors for source type " + type, e);
641         }
642         Class<?>[] interfaces = type.getInterfaces();
643         for (Class<?> i : interfaces) {
644             loadDescriptors(i, descriptors);
645         }
646         Class<?> superClass = type.getSuperclass();
647         if (superClass != null && !Object.class.equals(superClass)) {
648             loadDescriptors(superClass, descriptors);
649         }
650     }
651 }