001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2.beanutils;
018
019import java.beans.PropertyDescriptor;
020import java.lang.reflect.InvocationTargetException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.TreeSet;
029
030import org.apache.commons.beanutils.BeanUtilsBean;
031import org.apache.commons.beanutils.ConvertUtilsBean;
032import org.apache.commons.beanutils.DynaBean;
033import org.apache.commons.beanutils.FluentPropertyBeanIntrospector;
034import org.apache.commons.beanutils.PropertyUtilsBean;
035import org.apache.commons.beanutils.WrapDynaBean;
036import org.apache.commons.beanutils.WrapDynaClass;
037import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
038import org.apache.commons.lang3.ClassUtils;
039
040/**
041 * <p>
042 * A helper class for creating bean instances that are defined in configuration
043 * files.
044 * </p>
045 * <p>
046 * This class provides utility methods related to bean creation
047 * operations. These methods simplify such operations because a client need not
048 * deal with all involved interfaces. Usually, if a bean declaration has already
049 * been obtained, a single method call is necessary to create a new bean
050 * instance.
051 * </p>
052 * <p>
053 * This class also supports the registration of custom bean factories.
054 * Implementations of the {@link BeanFactory} interface can be
055 * registered under a symbolic name using the {@code registerBeanFactory()}
056 * method. In the configuration file the name of the bean factory can be
057 * specified in the bean declaration. Then this factory will be used to create
058 * the bean.
059 * </p>
060 * <p>
061 * In order to create beans using {@code BeanHelper}, create and instance of
062 * this class and initialize it accordingly - a default {@link BeanFactory}
063 * can be passed to the constructor, and additional bean factories can be
064 * registered (see above). Then this instance can be used to create beans from
065 * {@link BeanDeclaration} objects. {@code BeanHelper} is thread-safe. So an
066 * instance can be passed around in an application and shared between multiple
067 * components.
068 * </p>
069 *
070 * @since 1.3
071 */
072public final class BeanHelper
073{
074    /**
075     * A default instance of {@code BeanHelper} which can be shared between
076     * arbitrary components. If no special configuration is needed, this
077     * instance can be used throughout an application. Otherwise, new instances
078     * can be created with their own configuration.
079     */
080    public static final BeanHelper INSTANCE = new BeanHelper();
081
082    /**
083     * A special instance of {@code BeanUtilsBean} which is used for all
084     * property set and copy operations. This instance was initialized with
085     * {@code BeanIntrospector} objects which support fluent interfaces. This is
086     * required for handling builder parameter objects correctly.
087     */
088    private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean();
089
090    /** Stores a map with the registered bean factories. */
091    private final Map<String, BeanFactory> beanFactories = Collections
092            .synchronizedMap(new HashMap<String, BeanFactory>());
093
094    /**
095     * Stores the default bean factory, which is used if no other factory
096     * is provided in a bean declaration.
097     */
098    private final BeanFactory defaultBeanFactory;
099
100    /**
101     * Creates a new instance of {@code BeanHelper} with the default instance of
102     * {@link DefaultBeanFactory} as default {@link BeanFactory}.
103     */
104    public BeanHelper()
105    {
106        this(null);
107    }
108
109    /**
110     * Creates a new instance of {@code BeanHelper} and sets the specified
111     * default {@code BeanFactory}.
112     *
113     * @param defFactory the default {@code BeanFactory} (can be <b>null</b>,
114     *        then a default instance is used)
115     */
116    public BeanHelper(final BeanFactory defFactory)
117    {
118        defaultBeanFactory =
119                defFactory != null ? defFactory : DefaultBeanFactory.INSTANCE;
120    }
121
122    /**
123     * Register a bean factory under a symbolic name. This factory object can
124     * then be specified in bean declarations with the effect that this factory
125     * will be used to obtain an instance for the corresponding bean
126     * declaration.
127     *
128     * @param name the name of the factory
129     * @param factory the factory to be registered
130     */
131    public void registerBeanFactory(final String name, final BeanFactory factory)
132    {
133        if (name == null)
134        {
135            throw new IllegalArgumentException(
136                    "Name for bean factory must not be null!");
137        }
138        if (factory == null)
139        {
140            throw new IllegalArgumentException("Bean factory must not be null!");
141        }
142
143        beanFactories.put(name, factory);
144    }
145
146    /**
147     * Deregisters the bean factory with the given name. After that this factory
148     * cannot be used any longer.
149     *
150     * @param name the name of the factory to be deregistered
151     * @return the factory that was registered under this name; <b>null</b> if
152     * there was no such factory
153     */
154    public BeanFactory deregisterBeanFactory(final String name)
155    {
156        return beanFactories.remove(name);
157    }
158
159    /**
160     * Returns a set with the names of all currently registered bean factories.
161     *
162     * @return a set with the names of the registered bean factories
163     */
164    public Set<String> registeredFactoryNames()
165    {
166        return beanFactories.keySet();
167    }
168
169    /**
170     * Returns the default bean factory.
171     *
172     * @return the default bean factory
173     */
174    public BeanFactory getDefaultBeanFactory()
175    {
176        return defaultBeanFactory;
177    }
178
179    /**
180     * Initializes the passed in bean. This method will obtain all the bean's
181     * properties that are defined in the passed in bean declaration. These
182     * properties will be set on the bean. If necessary, further beans will be
183     * created recursively.
184     *
185     * @param bean the bean to be initialized
186     * @param data the bean declaration
187     * @throws ConfigurationRuntimeException if a property cannot be set
188     */
189    public void initBean(final Object bean, final BeanDeclaration data)
190    {
191        initBeanProperties(bean, data);
192
193        final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
194        if (nestedBeans != null)
195        {
196            if (bean instanceof Collection)
197            {
198                // This is safe because the collection stores the values of the
199                // nested beans.
200                @SuppressWarnings("unchecked")
201                final
202                Collection<Object> coll = (Collection<Object>) bean;
203                if (nestedBeans.size() == 1)
204                {
205                    final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
206                    final String propName = e.getKey();
207                    final Class<?> defaultClass = getDefaultClass(bean, propName);
208                    if (e.getValue() instanceof List)
209                    {
210                        // This is safe, provided that the bean declaration is implemented
211                        // correctly.
212                        @SuppressWarnings("unchecked")
213                        final
214                        List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
215                        for (final BeanDeclaration decl : decls)
216                        {
217                            coll.add(createBean(decl, defaultClass));
218                        }
219                    }
220                    else
221                    {
222                        final BeanDeclaration decl = (BeanDeclaration) e.getValue();
223                        coll.add(createBean(decl, defaultClass));
224                    }
225                }
226            }
227            else
228            {
229                for (final Map.Entry<String, Object> e : nestedBeans.entrySet())
230                {
231                    final String propName = e.getKey();
232                    final Class<?> defaultClass = getDefaultClass(bean, propName);
233
234                    final Object prop = e.getValue();
235
236                    if (prop instanceof Collection)
237                    {
238                        final Collection<Object> beanCollection =
239                                createPropertyCollection(propName, defaultClass);
240
241                        for (final Object elemDef : (Collection<?>) prop)
242                        {
243                            beanCollection
244                                    .add(createBean((BeanDeclaration) elemDef));
245                        }
246
247                        initProperty(bean, propName, beanCollection);
248                    }
249                    else
250                    {
251                        initProperty(bean, propName, createBean(
252                            (BeanDeclaration) e.getValue(), defaultClass));
253                    }
254                }
255            }
256        }
257    }
258
259    /**
260     * Initializes the beans properties.
261     *
262     * @param bean the bean to be initialized
263     * @param data the bean declaration
264     * @throws ConfigurationRuntimeException if a property cannot be set
265     */
266    public static void initBeanProperties(final Object bean, final BeanDeclaration data)
267    {
268        final Map<String, Object> properties = data.getBeanProperties();
269        if (properties != null)
270        {
271            for (final Map.Entry<String, Object> e : properties.entrySet())
272            {
273                final String propName = e.getKey();
274                initProperty(bean, propName, e.getValue());
275            }
276        }
277    }
278
279    /**
280     * Creates a {@code DynaBean} instance which wraps the passed in bean.
281     *
282     * @param bean the bean to be wrapped (must not be <b>null</b>)
283     * @return a {@code DynaBean} wrapping the passed in bean
284     * @throws IllegalArgumentException if the bean is <b>null</b>
285     * @since 2.0
286     */
287    public static DynaBean createWrapDynaBean(final Object bean)
288    {
289        if (bean == null)
290        {
291            throw new IllegalArgumentException("Bean must not be null!");
292        }
293        final WrapDynaClass dynaClass =
294                WrapDynaClass.createDynaClass(bean.getClass(),
295                        BEAN_UTILS_BEAN.getPropertyUtils());
296        return new WrapDynaBean(bean, dynaClass);
297    }
298
299    /**
300     * Copies matching properties from the source bean to the destination bean
301     * using a specially configured {@code PropertyUtilsBean} instance. This
302     * method ensures that enhanced introspection is enabled when doing the copy
303     * operation.
304     *
305     * @param dest the destination bean
306     * @param orig the source bean
307     * @throws NoSuchMethodException exception thrown by
308     *         {@code PropertyUtilsBean}
309     * @throws InvocationTargetException exception thrown by
310     *         {@code PropertyUtilsBean}
311     * @throws IllegalAccessException exception thrown by
312     *         {@code PropertyUtilsBean}
313     * @since 2.0
314     */
315    public static void copyProperties(final Object dest, final Object orig)
316            throws IllegalAccessException, InvocationTargetException,
317            NoSuchMethodException
318    {
319        BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
320    }
321
322    /**
323     * Return the Class of the property if it can be determined.
324     * @param bean The bean containing the property.
325     * @param propName The name of the property.
326     * @return The class associated with the property or null.
327     */
328    private static Class<?> getDefaultClass(final Object bean, final String propName)
329    {
330        try
331        {
332            final PropertyDescriptor desc =
333                    BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(
334                            bean, propName);
335            if (desc == null)
336            {
337                return null;
338            }
339            return desc.getPropertyType();
340        }
341        catch (final Exception ex)
342        {
343            return null;
344        }
345    }
346
347    /**
348     * Sets a property on the given bean using Common Beanutils.
349     *
350     * @param bean the bean
351     * @param propName the name of the property
352     * @param value the property's value
353     * @throws ConfigurationRuntimeException if the property is not writeable or
354     * an error occurred
355     */
356    private static void initProperty(final Object bean, final String propName, final Object value)
357    {
358        if (!isPropertyWriteable(bean, propName))
359        {
360            throw new ConfigurationRuntimeException("Property " + propName
361                    + " cannot be set on " + bean.getClass().getName());
362        }
363
364        try
365        {
366            BEAN_UTILS_BEAN.setProperty(bean, propName, value);
367        }
368        catch (final IllegalAccessException iaex)
369        {
370            throw new ConfigurationRuntimeException(iaex);
371        }
372        catch (final InvocationTargetException itex)
373        {
374            throw new ConfigurationRuntimeException(itex);
375        }
376    }
377
378    /**
379     * Creates a concrete collection instance to populate a property of type
380     * collection. This method tries to guess an appropriate collection type.
381     * Mostly the type of the property will be one of the collection interfaces
382     * rather than a concrete class; so we have to create a concrete equivalent.
383     *
384     * @param propName the name of the collection property
385     * @param propertyClass the type of the property
386     * @return the newly created collection
387     */
388    private static Collection<Object> createPropertyCollection(final String propName,
389            final Class<?> propertyClass)
390    {
391        Collection<Object> beanCollection;
392
393        if (List.class.isAssignableFrom(propertyClass))
394        {
395            beanCollection = new ArrayList<>();
396        }
397        else if (Set.class.isAssignableFrom(propertyClass))
398        {
399            beanCollection = new TreeSet<>();
400        }
401        else
402        {
403            throw new UnsupportedOperationException(
404                    "Unable to handle collection of type : "
405                            + propertyClass.getName() + " for property "
406                            + propName);
407        }
408        return beanCollection;
409    }
410
411    /**
412     * Set a property on the bean only if the property exists
413     *
414     * @param bean the bean
415     * @param propName the name of the property
416     * @param value the property's value
417     * @throws ConfigurationRuntimeException if the property is not writeable or
418     *         an error occurred
419     */
420    public static void setProperty(final Object bean, final String propName, final Object value)
421    {
422        if (isPropertyWriteable(bean, propName))
423        {
424            initProperty(bean, propName, value);
425        }
426    }
427
428    /**
429     * The main method for creating and initializing beans from a configuration.
430     * This method will return an initialized instance of the bean class
431     * specified in the passed in bean declaration. If this declaration does not
432     * contain the class of the bean, the passed in default class will be used.
433     * From the bean declaration the factory to be used for creating the bean is
434     * queried. The declaration may here return <b>null</b>, then a default
435     * factory is used. This factory is then invoked to perform the create
436     * operation.
437     *
438     * @param data the bean declaration
439     * @param defaultClass the default class to use
440     * @param param an additional parameter that will be passed to the bean
441     * factory; some factories may support parameters and behave different
442     * depending on the value passed in here
443     * @return the new bean
444     * @throws ConfigurationRuntimeException if an error occurs
445     */
446    public Object createBean(final BeanDeclaration data, final Class<?> defaultClass,
447            final Object param)
448    {
449        if (data == null)
450        {
451            throw new IllegalArgumentException(
452                    "Bean declaration must not be null!");
453        }
454
455        final BeanFactory factory = fetchBeanFactory(data);
456        final BeanCreationContext bcc =
457                createBeanCreationContext(data, defaultClass, param, factory);
458        try
459        {
460            return factory.createBean(bcc);
461        }
462        catch (final Exception ex)
463        {
464            throw new ConfigurationRuntimeException(ex);
465        }
466    }
467
468    /**
469     * Returns a bean instance for the specified declaration. This method is a
470     * short cut for {@code createBean(data, null, null);}.
471     *
472     * @param data the bean declaration
473     * @param defaultClass the class to be used when in the declaration no class
474     * is specified
475     * @return the new bean
476     * @throws ConfigurationRuntimeException if an error occurs
477     */
478    public Object createBean(final BeanDeclaration data, final Class<?> defaultClass)
479    {
480        return createBean(data, defaultClass, null);
481    }
482
483    /**
484     * Returns a bean instance for the specified declaration. This method is a
485     * short cut for {@code createBean(data, null);}.
486     *
487     * @param data the bean declaration
488     * @return the new bean
489     * @throws ConfigurationRuntimeException if an error occurs
490     */
491    public Object createBean(final BeanDeclaration data)
492    {
493        return createBean(data, null);
494    }
495
496    /**
497     * Returns a {@code java.lang.Class} object for the specified name.
498     * Because class loading can be tricky in some environments the code for
499     * retrieving a class by its name was extracted into this helper method. So
500     * if changes are necessary, they can be made at a single place.
501     *
502     * @param name the name of the class to be loaded
503     * @return the class object for the specified name
504     * @throws ClassNotFoundException if the class cannot be loaded
505     */
506    static Class<?> loadClass(final String name) throws ClassNotFoundException
507    {
508        return ClassUtils.getClass(name);
509    }
510
511    /**
512     * Checks whether the specified property of the given bean instance supports
513     * write access.
514     *
515     * @param bean the bean instance
516     * @param propName the name of the property in question
517     * @return <b>true</b> if this property can be written, <b>false</b>
518     *         otherwise
519     */
520    private static boolean isPropertyWriteable(final Object bean, final String propName)
521    {
522        return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
523    }
524
525    /**
526     * Determines the class of the bean to be created. If the bean declaration
527     * contains a class name, this class is used. Otherwise it is checked
528     * whether a default class is provided. If this is not the case, the
529     * factory's default class is used. If this class is undefined, too, an
530     * exception is thrown.
531     *
532     * @param data the bean declaration
533     * @param defaultClass the default class
534     * @param factory the bean factory to use
535     * @return the class of the bean to be created
536     * @throws ConfigurationRuntimeException if the class cannot be determined
537     */
538    private static Class<?> fetchBeanClass(final BeanDeclaration data,
539            final Class<?> defaultClass, final BeanFactory factory)
540    {
541        final String clsName = data.getBeanClassName();
542        if (clsName != null)
543        {
544            try
545            {
546                return loadClass(clsName);
547            }
548            catch (final ClassNotFoundException cex)
549            {
550                throw new ConfigurationRuntimeException(cex);
551            }
552        }
553
554        if (defaultClass != null)
555        {
556            return defaultClass;
557        }
558
559        final Class<?> clazz = factory.getDefaultBeanClass();
560        if (clazz == null)
561        {
562            throw new ConfigurationRuntimeException(
563                    "Bean class is not specified!");
564        }
565        return clazz;
566    }
567
568    /**
569     * Obtains the bean factory to use for creating the specified bean. This
570     * method will check whether a factory is specified in the bean declaration.
571     * If this is not the case, the default bean factory will be used.
572     *
573     * @param data the bean declaration
574     * @return the bean factory to use
575     * @throws ConfigurationRuntimeException if the factory cannot be determined
576     */
577    private BeanFactory fetchBeanFactory(final BeanDeclaration data)
578    {
579        final String factoryName = data.getBeanFactoryName();
580        if (factoryName != null)
581        {
582            final BeanFactory factory = beanFactories.get(factoryName);
583            if (factory == null)
584            {
585                throw new ConfigurationRuntimeException(
586                        "Unknown bean factory: " + factoryName);
587            }
588            return factory;
589        }
590        return getDefaultBeanFactory();
591    }
592
593    /**
594     * Creates a {@code BeanCreationContext} object for the creation of the
595     * specified bean.
596     *
597     * @param data the bean declaration
598     * @param defaultClass the default class to use
599     * @param param an additional parameter that will be passed to the bean
600     *        factory; some factories may support parameters and behave
601     *        different depending on the value passed in here
602     * @param factory the current bean factory
603     * @return the {@code BeanCreationContext}
604     * @throws ConfigurationRuntimeException if the bean class cannot be
605     *         determined
606     */
607    private BeanCreationContext createBeanCreationContext(
608            final BeanDeclaration data, final Class<?> defaultClass,
609            final Object param, final BeanFactory factory)
610    {
611        final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
612        return new BeanCreationContextImpl(this, beanClass, data, param);
613    }
614
615    /**
616     * Initializes the shared {@code BeanUtilsBean} instance. This method sets
617     * up custom bean introspection in a way that fluent parameter interfaces
618     * are supported.
619     *
620     * @return the {@code BeanUtilsBean} instance to be used for all property
621     *         set operations
622     */
623    private static BeanUtilsBean initBeanUtilsBean()
624    {
625        final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
626        propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
627        return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
628    }
629
630    /**
631     * An implementation of the {@code BeanCreationContext} interface used by
632     * {@code BeanHelper} to communicate with a {@code BeanFactory}. This class
633     * contains all information required for the creation of a bean. The methods
634     * for creating and initializing bean instances are implemented by calling
635     * back to the provided {@code BeanHelper} instance (which is the instance
636     * that created this object).
637     */
638    private static final class BeanCreationContextImpl implements BeanCreationContext
639    {
640        /** The association BeanHelper instance. */
641        private final BeanHelper beanHelper;
642
643        /** The class of the bean to be created. */
644        private final Class<?> beanClass;
645
646        /** The underlying bean declaration. */
647        private final BeanDeclaration data;
648
649        /** The parameter for the bean factory. */
650        private final Object param;
651
652        private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass,
653                final BeanDeclaration data, final Object param)
654        {
655            beanHelper = helper;
656            this.beanClass = beanClass;
657            this.param = param;
658            this.data = data;
659        }
660
661        @Override
662        public void initBean(final Object bean, final BeanDeclaration data)
663        {
664            beanHelper.initBean(bean, data);
665        }
666
667        @Override
668        public Object getParameter()
669        {
670            return param;
671        }
672
673        @Override
674        public BeanDeclaration getBeanDeclaration()
675        {
676            return data;
677        }
678
679        @Override
680        public Class<?> getBeanClass()
681        {
682            return beanClass;
683        }
684
685        @Override
686        public Object createBean(final BeanDeclaration data)
687        {
688            return beanHelper.createBean(data);
689        }
690    }
691}