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