    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}