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<Bean, Bean> builder = new BinderModelBuilder(Bean.class);
89 * builder.addSimpleProperties("name", "surname");
90 * BinderFactory.registerBinderModel(builder);
91 * Binder<Bean, Bean> 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<Bean, Bean> 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 }