    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}