View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.beanutils;
18  
19  import java.beans.PropertyDescriptor;
20  import java.lang.reflect.InvocationTargetException;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.TreeSet;
29  
30  import org.apache.commons.beanutils.BeanUtilsBean;
31  import org.apache.commons.beanutils.ConvertUtilsBean;
32  import org.apache.commons.beanutils.DynaBean;
33  import org.apache.commons.beanutils.FluentPropertyBeanIntrospector;
34  import org.apache.commons.beanutils.PropertyUtilsBean;
35  import org.apache.commons.beanutils.WrapDynaBean;
36  import org.apache.commons.beanutils.WrapDynaClass;
37  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
38  import org.apache.commons.lang3.ClassUtils;
39  
40  /**
41   * <p>
42   * A helper class for creating bean instances that are defined in configuration files.
43   * </p>
44   * <p>
45   * This class provides utility methods related to bean creation operations. These methods simplify such operations
46   * because a client need not deal with all involved interfaces. Usually, if a bean declaration has already been
47   * obtained, a single method call is necessary to create a new bean instance.
48   * </p>
49   * <p>
50   * This class also supports the registration of custom bean factories. Implementations of the {@link BeanFactory}
51   * interface can be registered under a symbolic name using the {@code registerBeanFactory()} method. In the
52   * configuration file the name of the bean factory can be specified in the bean declaration. Then this factory will be
53   * used to create the bean.
54   * </p>
55   * <p>
56   * In order to create beans using {@code BeanHelper}, create and instance of this class and initialize it accordingly -
57   * a default {@link BeanFactory} can be passed to the constructor, and additional bean factories can be registered (see
58   * above). Then this instance can be used to create beans from {@link BeanDeclaration} objects. {@code BeanHelper} is
59   * thread-safe. So an instance can be passed around in an application and shared between multiple components.
60   * </p>
61   *
62   * @since 1.3
63   */
64  public final class BeanHelper {
65  
66      /**
67       * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a
68       * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for
69       * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance
70       * (which is the instance that created this object).
71       */
72      private static final class BeanCreationContextImpl implements BeanCreationContext {
73  
74          /** The association BeanHelper instance. */
75          private final BeanHelper beanHelper;
76  
77          /** The class of the bean to be created. */
78          private final Class<?> beanClass;
79  
80          /** The underlying bean declaration. */
81          private final BeanDeclaration data;
82  
83          /** The parameter for the bean factory. */
84          private final Object param;
85  
86          private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) {
87              beanHelper = helper;
88              this.beanClass = beanClass;
89              this.param = param;
90              this.data = data;
91          }
92  
93          @Override
94          public Object createBean(final BeanDeclaration data) {
95              return beanHelper.createBean(data);
96          }
97  
98          @Override
99          public Class<?> getBeanClass() {
100             return beanClass;
101         }
102 
103         @Override
104         public BeanDeclaration getBeanDeclaration() {
105             return data;
106         }
107 
108         @Override
109         public Object getParameter() {
110             return param;
111         }
112 
113         @Override
114         public void initBean(final Object bean, final BeanDeclaration data) {
115             beanHelper.initBean(bean, data);
116         }
117     }
118 
119     /**
120      * A default instance of {@code BeanHelper} which can be shared between arbitrary components. If no special
121      * configuration is needed, this instance can be used throughout an application. Otherwise, new instances can be created
122      * with their own configuration.
123      */
124     public static final BeanHelper INSTANCE = new BeanHelper();
125 
126     /**
127      * A special instance of {@code BeanUtilsBean} which is used for all property set and copy operations. This instance was
128      * initialized with {@code BeanIntrospector} objects which support fluent interfaces. This is required for handling
129      * builder parameter objects correctly.
130      */
131     private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean();
132 
133     /**
134      * Copies matching properties from the source bean to the destination bean using a specially configured
135      * {@code PropertyUtilsBean} instance. This method ensures that enhanced introspection is enabled when doing the copy
136      * operation.
137      *
138      * @param dest the destination bean
139      * @param orig the source bean
140      * @throws NoSuchMethodException exception thrown by {@code PropertyUtilsBean}
141      * @throws InvocationTargetException exception thrown by {@code PropertyUtilsBean}
142      * @throws IllegalAccessException exception thrown by {@code PropertyUtilsBean}
143      * @since 2.0
144      */
145     public static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
146         BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
147     }
148 
149     /**
150      * Creates a concrete collection instance to populate a property of type collection. This method tries to guess an
151      * appropriate collection type. Mostly the type of the property will be one of the collection interfaces rather than a
152      * concrete class; so we have to create a concrete equivalent.
153      *
154      * @param propName the name of the collection property
155      * @param propertyClass the type of the property
156      * @return the newly created collection
157      */
158     private static Collection<Object> createPropertyCollection(final String propName, final Class<?> propertyClass) {
159         final Collection<Object> beanCollection;
160 
161         if (List.class.isAssignableFrom(propertyClass)) {
162             beanCollection = new ArrayList<>();
163         } else if (Set.class.isAssignableFrom(propertyClass)) {
164             beanCollection = new TreeSet<>();
165         } else {
166             throw new UnsupportedOperationException("Unable to handle collection of type : " + propertyClass.getName() + " for property " + propName);
167         }
168         return beanCollection;
169     }
170 
171     /**
172      * Creates a {@code DynaBean} instance which wraps the passed in bean.
173      *
174      * @param bean the bean to be wrapped (must not be <strong>null</strong>)
175      * @return a {@code DynaBean} wrapping the passed in bean
176      * @throws IllegalArgumentException if the bean is <strong>null</strong>
177      * @since 2.0
178      */
179     public static DynaBean createWrapDynaBean(final Object bean) {
180         if (bean == null) {
181             throw new IllegalArgumentException("Bean must not be null!");
182         }
183         final WrapDynaClass dynaClass = WrapDynaClass.createDynaClass(bean.getClass(), BEAN_UTILS_BEAN.getPropertyUtils());
184         return new WrapDynaBean(bean, dynaClass);
185     }
186 
187     /**
188      * Determines the class of the bean to be created. If the bean declaration contains a class name, this class is used.
189      * Otherwise it is checked whether a default class is provided. If this is not the case, the factory's default class is
190      * used. If this class is undefined, too, an exception is thrown.
191      *
192      * @param data the bean declaration
193      * @param defaultClass the default class
194      * @param factory the bean factory to use
195      * @return the class of the bean to be created
196      * @throws ConfigurationRuntimeException if the class cannot be determined
197      */
198     private static Class<?> fetchBeanClass(final BeanDeclaration data, final Class<?> defaultClass, final BeanFactory factory) {
199         final String clsName = data.getBeanClassName();
200         if (clsName != null) {
201             try {
202                 return loadClass(clsName);
203             } catch (final ClassNotFoundException cex) {
204                 throw new ConfigurationRuntimeException(cex);
205             }
206         }
207 
208         if (defaultClass != null) {
209             return defaultClass;
210         }
211 
212         final Class<?> clazz = factory.getDefaultBeanClass();
213         if (clazz == null) {
214             throw new ConfigurationRuntimeException("Bean class is not specified!");
215         }
216         return clazz;
217     }
218 
219     /**
220      * Gets the Class of the property if it can be determined.
221      *
222      * @param bean The bean containing the property.
223      * @param propName The name of the property.
224      * @return The class associated with the property or null.
225      */
226     private static Class<?> getDefaultClass(final Object bean, final String propName) {
227         try {
228             final PropertyDescriptor desc = BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(bean, propName);
229             if (desc == null) {
230                 return null;
231             }
232             return desc.getPropertyType();
233         } catch (final Exception ex) {
234             return null;
235         }
236     }
237 
238     /**
239      * Initializes the beans properties.
240      *
241      * @param bean the bean to be initialized
242      * @param data the bean declaration
243      * @throws ConfigurationRuntimeException if a property cannot be set
244      */
245     public static void initBeanProperties(final Object bean, final BeanDeclaration data) {
246         final Map<String, Object> properties = data.getBeanProperties();
247         if (properties != null) {
248             properties.forEach((k, v) -> initProperty(bean, k, v));
249         }
250     }
251 
252     /**
253      * Initializes the shared {@code BeanUtilsBean} instance. This method sets up custom bean introspection in a way that
254      * fluent parameter interfaces are supported.
255      *
256      * @return the {@code BeanUtilsBean} instance to be used for all property set operations
257      */
258     private static BeanUtilsBean initBeanUtilsBean() {
259         final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
260         propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
261         return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
262     }
263 
264     /**
265      * Sets a property on the given bean using Common Beanutils.
266      *
267      * @param bean the bean
268      * @param propName the name of the property
269      * @param value the property's value
270      * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
271      */
272     private static void initProperty(final Object bean, final String propName, final Object value) {
273         if (!isPropertyWriteable(bean, propName)) {
274             throw new ConfigurationRuntimeException("Property " + propName + " cannot be set on " + bean.getClass().getName());
275         }
276 
277         try {
278             BEAN_UTILS_BEAN.setProperty(bean, propName, value);
279         } catch (final IllegalAccessException | InvocationTargetException itex) {
280             throw new ConfigurationRuntimeException(itex);
281         }
282     }
283 
284     /**
285      * Tests whether the specified property of the given bean instance supports write access.
286      *
287      * @param bean the bean instance
288      * @param propName the name of the property in question
289      * @return <strong>true</strong> if this property can be written, <strong>false</strong> otherwise
290      */
291     private static boolean isPropertyWriteable(final Object bean, final String propName) {
292         return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
293     }
294 
295     /**
296      * Loads a {@code Class} object for the specified name. Because class loading can be tricky in some
297      * environments the code for retrieving a class by its name was extracted into this helper method. So if changes are
298      * necessary, they can be made at a single place.
299      *
300      * @param name the name of the class to be loaded
301      * @return the class object for the specified name
302      * @throws ClassNotFoundException if the class cannot be loaded
303      */
304     static Class<?> loadClass(final String name) throws ClassNotFoundException {
305         return ClassUtils.getClass(name);
306     }
307 
308     /**
309      * Sets a property on the bean only if the property exists
310      *
311      * @param bean the bean
312      * @param propName the name of the property
313      * @param value the property's value
314      * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
315      */
316     public static void setProperty(final Object bean, final String propName, final Object value) {
317         if (isPropertyWriteable(bean, propName)) {
318             initProperty(bean, propName, value);
319         }
320     }
321 
322     /** Stores a map with the registered bean factories. */
323     private final Map<String, BeanFactory> beanFactories = Collections.synchronizedMap(new HashMap<>());
324 
325     /**
326      * Stores the default bean factory, which is used if no other factory is provided in a bean declaration.
327      */
328     private final BeanFactory defaultBeanFactory;
329 
330     /**
331      * Constructs a new instance of {@code BeanHelper} with the default instance of {@link DefaultBeanFactory} as default
332      * {@link BeanFactory}.
333      */
334     public BeanHelper() {
335         this(null);
336     }
337 
338     /**
339      * Constructs a new instance of {@code BeanHelper} and sets the specified default {@code BeanFactory}.
340      *
341      * @param defaultBeanFactory the default {@code BeanFactory} (can be <strong>null</strong>, then a default instance is used)
342      */
343     public BeanHelper(final BeanFactory defaultBeanFactory) {
344         this.defaultBeanFactory = defaultBeanFactory != null ? defaultBeanFactory : DefaultBeanFactory.INSTANCE;
345     }
346 
347     /**
348      * Creates a bean instance for the specified declaration. This method is a short cut for
349      * {@code createBean(data, null);}.
350      *
351      * @param data the bean declaration
352      * @return the new bean
353      * @throws ConfigurationRuntimeException if an error occurs
354      */
355     public Object createBean(final BeanDeclaration data) {
356         return createBean(data, null);
357     }
358 
359     /**
360      * Creates a bean instance for the specified declaration. This method is a short cut for
361      * {@code createBean(data, null, null);}.
362      *
363      * @param data the bean declaration
364      * @param defaultClass the class to be used when in the declaration no class is specified
365      * @return the new bean
366      * @throws ConfigurationRuntimeException if an error occurs
367      */
368     public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) {
369         return createBean(data, defaultClass, null);
370     }
371 
372     /**
373      * The main method for creating and initializing beans from a configuration. This method will return an initialized
374      * instance of the bean class specified in the passed in bean declaration. If this declaration does not contain the
375      * class of the bean, the passed in default class will be used. From the bean declaration the factory to be used for
376      * creating the bean is queried. The declaration may here return <strong>null</strong>, then a default factory is used. This
377      * factory is then invoked to perform the create operation.
378      *
379      * @param data the bean declaration
380      * @param defaultClass the default class to use
381      * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
382      *        and behave different depending on the value passed in here
383      * @return the new bean
384      * @throws ConfigurationRuntimeException if an error occurs
385      */
386     public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, final Object param) {
387         if (data == null) {
388             throw new IllegalArgumentException("Bean declaration must not be null!");
389         }
390 
391         final BeanFactory factory = fetchBeanFactory(data);
392         final BeanCreationContext bcc = createBeanCreationContext(data, defaultClass, param, factory);
393         try {
394             return factory.createBean(bcc);
395         } catch (final Exception ex) {
396             throw new ConfigurationRuntimeException(ex);
397         }
398     }
399 
400     /**
401      * Creates a {@code BeanCreationContext} object for the creation of the specified bean.
402      *
403      * @param data the bean declaration
404      * @param defaultClass the default class to use
405      * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
406      *        and behave different depending on the value passed in here
407      * @param factory the current bean factory
408      * @return the {@code BeanCreationContext}
409      * @throws ConfigurationRuntimeException if the bean class cannot be determined
410      */
411     private BeanCreationContext createBeanCreationContext(final BeanDeclaration data, final Class<?> defaultClass, final Object param,
412         final BeanFactory factory) {
413         final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
414         return new BeanCreationContextImpl(this, beanClass, data, param);
415     }
416 
417     /**
418      * Deregisters the bean factory with the given name. After that this factory cannot be used any longer.
419      *
420      * @param name the name of the factory to be deregistered
421      * @return the factory that was registered under this name; <strong>null</strong> if there was no such factory
422      */
423     public BeanFactory deregisterBeanFactory(final String name) {
424         return beanFactories.remove(name);
425     }
426 
427     /**
428      * Obtains the bean factory to use for creating the specified bean. This method will check whether a factory is
429      * specified in the bean declaration. If this is not the case, the default bean factory will be used.
430      *
431      * @param data the bean declaration
432      * @return the bean factory to use
433      * @throws ConfigurationRuntimeException if the factory cannot be determined
434      */
435     private BeanFactory fetchBeanFactory(final BeanDeclaration data) {
436         final String factoryName = data.getBeanFactoryName();
437         if (factoryName != null) {
438             final BeanFactory factory = beanFactories.get(factoryName);
439             if (factory == null) {
440                 throw new ConfigurationRuntimeException("Unknown bean factory: " + factoryName);
441             }
442             return factory;
443         }
444         return getDefaultBeanFactory();
445     }
446 
447     /**
448      * Gets the default bean factory.
449      *
450      * @return the default bean factory
451      */
452     public BeanFactory getDefaultBeanFactory() {
453         return defaultBeanFactory;
454     }
455 
456     /**
457      * Initializes the passed in bean. This method will obtain all the bean's properties that are defined in the passed in
458      * bean declaration. These properties will be set on the bean. If necessary, further beans will be created recursively.
459      *
460      * @param bean the bean to be initialized
461      * @param data the bean declaration
462      * @throws ConfigurationRuntimeException if a property cannot be set
463      */
464     public void initBean(final Object bean, final BeanDeclaration data) {
465         initBeanProperties(bean, data);
466 
467         final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
468         if (nestedBeans != null) {
469             if (bean instanceof Collection) {
470                 // This is safe because the collection stores the values of the
471                 // nested beans.
472                 @SuppressWarnings("unchecked")
473                 final Collection<Object> coll = (Collection<Object>) bean;
474                 if (nestedBeans.size() == 1) {
475                     final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
476                     final String propName = e.getKey();
477                     final Class<?> defaultClass = getDefaultClass(bean, propName);
478                     if (e.getValue() instanceof List) {
479                         // This is safe, provided that the bean declaration is implemented
480                         // correctly.
481                         @SuppressWarnings("unchecked")
482                         final List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
483                         decls.forEach(decl -> coll.add(createBean(decl, defaultClass)));
484                     } else {
485                         coll.add(createBean((BeanDeclaration) e.getValue(), defaultClass));
486                     }
487                 }
488             } else {
489                 nestedBeans.forEach((propName, prop) -> {
490                     final Class<?> defaultClass = getDefaultClass(bean, propName);
491                     if (prop instanceof Collection) {
492                         final Collection<Object> beanCollection = createPropertyCollection(propName, defaultClass);
493                         ((Collection<BeanDeclaration>) prop).forEach(elemDef -> beanCollection.add(createBean(elemDef)));
494                         initProperty(bean, propName, beanCollection);
495                     } else {
496                         initProperty(bean, propName, createBean((BeanDeclaration) prop, defaultClass));
497                     }
498                 });
499             }
500         }
501     }
502 
503     /**
504      * Registers a bean factory under a symbolic name. This factory object can then be specified in bean declarations with
505      * the effect that this factory will be used to obtain an instance for the corresponding bean declaration.
506      *
507      * @param name the name of the factory
508      * @param factory the factory to be registered
509      */
510     public void registerBeanFactory(final String name, final BeanFactory factory) {
511         if (name == null) {
512             throw new IllegalArgumentException("Name for bean factory must not be null!");
513         }
514         if (factory == null) {
515             throw new IllegalArgumentException("Bean factory must not be null!");
516         }
517 
518         beanFactories.put(name, factory);
519     }
520 
521     /**
522      * Gets a set with the names of all currently registered bean factories.
523      *
524      * @return a set with the names of the registered bean factories
525      */
526     public Set<String> registeredFactoryNames() {
527         return beanFactories.keySet();
528     }
529 }