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 1790899 2017-04-10 21:56:46Z 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(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(String name, 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(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(Object bean, BeanDeclaration data)
191    {
192        initBeanProperties(bean, data);
193
194        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                Collection<Object> coll = (Collection<Object>) bean;
203                if (nestedBeans.size() == 1)
204                {
205                    Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
206                    String propName = e.getKey();
207                    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                        List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
214                        for (BeanDeclaration decl : decls)
215                        {
216                            coll.add(createBean(decl, defaultClass));
217                        }
218                    }
219                    else
220                    {
221                        BeanDeclaration decl = (BeanDeclaration) e.getValue();
222                        coll.add(createBean(decl, defaultClass));
223                    }
224                }
225            }
226            else
227            {
228                for (Map.Entry<String, Object> e : nestedBeans.entrySet())
229                {
230                    String propName = e.getKey();
231                    Class<?> defaultClass = getDefaultClass(bean, propName);
232
233                    Object prop = e.getValue();
234
235                    if (prop instanceof Collection)
236                    {
237                        Collection<Object> beanCollection =
238                                createPropertyCollection(propName, defaultClass);
239
240                        for (Object elemDef : (Collection<?>) prop)
241                        {
242                            beanCollection
243                                    .add(createBean((BeanDeclaration) elemDef));
244                        }
245
246                        initProperty(bean, propName, beanCollection);
247                    }
248                    else
249                    {
250                        initProperty(bean, propName, createBean(
251                            (BeanDeclaration) e.getValue(), defaultClass));
252                    }
253                }
254            }
255        }
256    }
257
258    /**
259     * Initializes the beans properties.
260     *
261     * @param bean the bean to be initialized
262     * @param data the bean declaration
263     * @throws ConfigurationRuntimeException if a property cannot be set
264     */
265    public static void initBeanProperties(Object bean, BeanDeclaration data)
266    {
267        Map<String, Object> properties = data.getBeanProperties();
268        if (properties != null)
269        {
270            for (Map.Entry<String, Object> e : properties.entrySet())
271            {
272                String propName = e.getKey();
273                initProperty(bean, propName, e.getValue());
274            }
275        }
276    }
277
278    /**
279     * Creates a {@code DynaBean} instance which wraps the passed in bean.
280     *
281     * @param bean the bean to be wrapped (must not be <b>null</b>)
282     * @return a {@code DynaBean} wrapping the passed in bean
283     * @throws IllegalArgumentException if the bean is <b>null</b>
284     * @since 2.0
285     */
286    public static DynaBean createWrapDynaBean(Object bean)
287    {
288        if (bean == null)
289        {
290            throw new IllegalArgumentException("Bean must not be null!");
291        }
292        WrapDynaClass dynaClass =
293                WrapDynaClass.createDynaClass(bean.getClass(),
294                        BEAN_UTILS_BEAN.getPropertyUtils());
295        return new WrapDynaBean(bean, dynaClass);
296    }
297
298    /**
299     * Copies matching properties from the source bean to the destination bean
300     * using a specially configured {@code PropertyUtilsBean} instance. This
301     * method ensures that enhanced introspection is enabled when doing the copy
302     * operation.
303     *
304     * @param dest the destination bean
305     * @param orig the source bean
306     * @throws NoSuchMethodException exception thrown by
307     *         {@code PropertyUtilsBean}
308     * @throws InvocationTargetException exception thrown by
309     *         {@code PropertyUtilsBean}
310     * @throws IllegalAccessException exception thrown by
311     *         {@code PropertyUtilsBean}
312     * @since 2.0
313     */
314    public static void copyProperties(Object dest, Object orig)
315            throws IllegalAccessException, InvocationTargetException,
316            NoSuchMethodException
317    {
318        BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
319    }
320
321    /**
322     * Return the Class of the property if it can be determined.
323     * @param bean The bean containing the property.
324     * @param propName The name of the property.
325     * @return The class associated with the property or null.
326     */
327    private static Class<?> getDefaultClass(Object bean, String propName)
328    {
329        try
330        {
331            PropertyDescriptor desc =
332                    BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(
333                            bean, propName);
334            if (desc == null)
335            {
336                return null;
337            }
338            return desc.getPropertyType();
339        }
340        catch (Exception ex)
341        {
342            return null;
343        }
344    }
345
346    /**
347     * Sets a property on the given bean using Common Beanutils.
348     *
349     * @param bean the bean
350     * @param propName the name of the property
351     * @param value the property's value
352     * @throws ConfigurationRuntimeException if the property is not writeable or
353     * an error occurred
354     */
355    private static void initProperty(Object bean, String propName, Object value)
356    {
357        if (!isPropertyWriteable(bean, propName))
358        {
359            throw new ConfigurationRuntimeException("Property " + propName
360                    + " cannot be set on " + bean.getClass().getName());
361        }
362
363        try
364        {
365            BEAN_UTILS_BEAN.setProperty(bean, propName, value);
366        }
367        catch (IllegalAccessException iaex)
368        {
369            throw new ConfigurationRuntimeException(iaex);
370        }
371        catch (InvocationTargetException itex)
372        {
373            throw new ConfigurationRuntimeException(itex);
374        }
375    }
376
377    /**
378     * Creates a concrete collection instance to populate a property of type
379     * collection. This method tries to guess an appropriate collection type.
380     * Mostly the type of the property will be one of the collection interfaces
381     * rather than a concrete class; so we have to create a concrete equivalent.
382     *
383     * @param propName the name of the collection property
384     * @param propertyClass the type of the property
385     * @return the newly created collection
386     */
387    private static Collection<Object> createPropertyCollection(String propName,
388            Class<?> propertyClass)
389    {
390        Collection<Object> beanCollection;
391
392        if (List.class.isAssignableFrom(propertyClass))
393        {
394            beanCollection = new ArrayList<>();
395        }
396        else if (Set.class.isAssignableFrom(propertyClass))
397        {
398            beanCollection = new TreeSet<>();
399        }
400        else
401        {
402            throw new UnsupportedOperationException(
403                    "Unable to handle collection of type : "
404                            + propertyClass.getName() + " for property "
405                            + propName);
406        }
407        return beanCollection;
408    }
409
410    /**
411     * Set a property on the bean only if the property exists
412     *
413     * @param bean the bean
414     * @param propName the name of the property
415     * @param value the property's value
416     * @throws ConfigurationRuntimeException if the property is not writeable or
417     *         an error occurred
418     */
419    public static void setProperty(Object bean, String propName, Object value)
420    {
421        if (isPropertyWriteable(bean, propName))
422        {
423            initProperty(bean, propName, value);
424        }
425    }
426
427    /**
428     * The main method for creating and initializing beans from a configuration.
429     * This method will return an initialized instance of the bean class
430     * specified in the passed in bean declaration. If this declaration does not
431     * contain the class of the bean, the passed in default class will be used.
432     * From the bean declaration the factory to be used for creating the bean is
433     * queried. The declaration may here return <b>null</b>, then a default
434     * factory is used. This factory is then invoked to perform the create
435     * operation.
436     *
437     * @param data the bean declaration
438     * @param defaultClass the default class to use
439     * @param param an additional parameter that will be passed to the bean
440     * factory; some factories may support parameters and behave different
441     * depending on the value passed in here
442     * @return the new bean
443     * @throws ConfigurationRuntimeException if an error occurs
444     */
445    public Object createBean(BeanDeclaration data, Class<?> defaultClass,
446            Object param)
447    {
448        if (data == null)
449        {
450            throw new IllegalArgumentException(
451                    "Bean declaration must not be null!");
452        }
453
454        BeanFactory factory = fetchBeanFactory(data);
455        BeanCreationContext bcc =
456                createBeanCreationContext(data, defaultClass, param, factory);
457        try
458        {
459            return factory.createBean(bcc);
460        }
461        catch (Exception ex)
462        {
463            throw new ConfigurationRuntimeException(ex);
464        }
465    }
466
467    /**
468     * Returns a bean instance for the specified declaration. This method is a
469     * short cut for {@code createBean(data, null, null);}.
470     *
471     * @param data the bean declaration
472     * @param defaultClass the class to be used when in the declaration no class
473     * is specified
474     * @return the new bean
475     * @throws ConfigurationRuntimeException if an error occurs
476     */
477    public Object createBean(BeanDeclaration data, Class<?> defaultClass)
478    {
479        return createBean(data, defaultClass, null);
480    }
481
482    /**
483     * Returns a bean instance for the specified declaration. This method is a
484     * short cut for {@code createBean(data, null);}.
485     *
486     * @param data the bean declaration
487     * @return the new bean
488     * @throws ConfigurationRuntimeException if an error occurs
489     */
490    public Object createBean(BeanDeclaration data)
491    {
492        return createBean(data, null);
493    }
494
495    /**
496     * Returns a {@code java.lang.Class} object for the specified name.
497     * Because class loading can be tricky in some environments the code for
498     * retrieving a class by its name was extracted into this helper method. So
499     * if changes are necessary, they can be made at a single place.
500     *
501     * @param name the name of the class to be loaded
502     * @return the class object for the specified name
503     * @throws ClassNotFoundException if the class cannot be loaded
504     */
505    static Class<?> loadClass(String name) throws ClassNotFoundException
506    {
507        return ClassUtils.getClass(name);
508    }
509
510    /**
511     * Checks whether the specified property of the given bean instance supports
512     * write access.
513     *
514     * @param bean the bean instance
515     * @param propName the name of the property in question
516     * @return <b>true</b> if this property can be written, <b>false</b>
517     *         otherwise
518     */
519    private static boolean isPropertyWriteable(Object bean, String propName)
520    {
521        return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
522    }
523
524    /**
525     * Determines the class of the bean to be created. If the bean declaration
526     * contains a class name, this class is used. Otherwise it is checked
527     * whether a default class is provided. If this is not the case, the
528     * factory's default class is used. If this class is undefined, too, an
529     * exception is thrown.
530     *
531     * @param data the bean declaration
532     * @param defaultClass the default class
533     * @param factory the bean factory to use
534     * @return the class of the bean to be created
535     * @throws ConfigurationRuntimeException if the class cannot be determined
536     */
537    private static Class<?> fetchBeanClass(BeanDeclaration data,
538            Class<?> defaultClass, BeanFactory factory)
539    {
540        String clsName = data.getBeanClassName();
541        if (clsName != null)
542        {
543            try
544            {
545                return loadClass(clsName);
546            }
547            catch (ClassNotFoundException cex)
548            {
549                throw new ConfigurationRuntimeException(cex);
550            }
551        }
552
553        if (defaultClass != null)
554        {
555            return defaultClass;
556        }
557
558        Class<?> clazz = factory.getDefaultBeanClass();
559        if (clazz == null)
560        {
561            throw new ConfigurationRuntimeException(
562                    "Bean class is not specified!");
563        }
564        return clazz;
565    }
566
567    /**
568     * Obtains the bean factory to use for creating the specified bean. This
569     * method will check whether a factory is specified in the bean declaration.
570     * If this is not the case, the default bean factory will be used.
571     *
572     * @param data the bean declaration
573     * @return the bean factory to use
574     * @throws ConfigurationRuntimeException if the factory cannot be determined
575     */
576    private BeanFactory fetchBeanFactory(BeanDeclaration data)
577    {
578        String factoryName = data.getBeanFactoryName();
579        if (factoryName != null)
580        {
581            BeanFactory factory = beanFactories.get(factoryName);
582            if (factory == null)
583            {
584                throw new ConfigurationRuntimeException(
585                        "Unknown bean factory: " + factoryName);
586            }
587            else
588            {
589                return factory;
590            }
591        }
592        else
593        {
594            return getDefaultBeanFactory();
595        }
596    }
597
598    /**
599     * Creates a {@code BeanCreationContext} object for the creation of the
600     * specified bean.
601     *
602     * @param data the bean declaration
603     * @param defaultClass the default class to use
604     * @param param an additional parameter that will be passed to the bean
605     *        factory; some factories may support parameters and behave
606     *        different depending on the value passed in here
607     * @param factory the current bean factory
608     * @return the {@code BeanCreationContext}
609     * @throws ConfigurationRuntimeException if the bean class cannot be
610     *         determined
611     */
612    private BeanCreationContext createBeanCreationContext(
613            final BeanDeclaration data, Class<?> defaultClass,
614            final Object param, BeanFactory factory)
615    {
616        final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
617        return new BeanCreationContextImpl(this, beanClass, data, param);
618    }
619
620    /**
621     * Initializes the shared {@code BeanUtilsBean} instance. This method sets
622     * up custom bean introspection in a way that fluent parameter interfaces
623     * are supported.
624     *
625     * @return the {@code BeanUtilsBean} instance to be used for all property
626     *         set operations
627     */
628    private static BeanUtilsBean initBeanUtilsBean()
629    {
630        PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
631        propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
632        return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
633    }
634
635    /**
636     * An implementation of the {@code BeanCreationContext} interface used by
637     * {@code BeanHelper} to communicate with a {@code BeanFactory}. This class
638     * contains all information required for the creation of a bean. The methods
639     * for creating and initializing bean instances are implemented by calling
640     * back to the provided {@code BeanHelper} instance (which is the instance
641     * that created this object).
642     */
643    private static final class BeanCreationContextImpl implements BeanCreationContext
644    {
645        /** The association BeanHelper instance. */
646        private final BeanHelper beanHelper;
647
648        /** The class of the bean to be created. */
649        private final Class<?> beanClass;
650
651        /** The underlying bean declaration. */
652        private final BeanDeclaration data;
653
654        /** The parameter for the bean factory. */
655        private final Object param;
656
657        private BeanCreationContextImpl(BeanHelper helper, Class<?> beanClass,
658                BeanDeclaration data, Object param)
659        {
660            beanHelper = helper;
661            this.beanClass = beanClass;
662            this.param = param;
663            this.data = data;
664        }
665
666        @Override
667        public void initBean(Object bean, BeanDeclaration data)
668        {
669            beanHelper.initBean(bean, data);
670        }
671
672        @Override
673        public Object getParameter()
674        {
675            return param;
676        }
677
678        @Override
679        public BeanDeclaration getBeanDeclaration()
680        {
681            return data;
682        }
683
684        @Override
685        public Class<?> getBeanClass()
686        {
687            return beanClass;
688        }
689
690        @Override
691        public Object createBean(BeanDeclaration data)
692        {
693            return beanHelper.createBean(data);
694        }
695    }
696}