    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     */
    017    package org.apache.commons.configuration.beanutils;
    018
    019    import java.beans.PropertyDescriptor;
    020    import java.lang.reflect.InvocationTargetException;
    021    import java.util.Collection;
    022    import java.util.Collections;
    023    import java.util.HashMap;
    024    import java.util.Iterator;
    025    import java.util.List;
    026    import java.util.Map;
    027    import java.util.Set;
    028
    029    import org.apache.commons.beanutils.BeanUtils;
    030    import org.apache.commons.beanutils.PropertyUtils;
    031    import org.apache.commons.configuration.ConfigurationRuntimeException;
    032    import org.apache.commons.lang.ClassUtils;
    033
    034    /**
    035     * <p>
    036     * A helper class for creating bean instances that are defined in configuration
    037     * files.
    038     * </p>
    039     * <p>
    040     * This class provides static utility methods related to bean creation
    041     * operations. These methods simplify such operations because a client need not
    042     * deal with all involved interfaces. Usually, if a bean declaration has already
    043     * been obtained, a single method call is necessary to create a new bean
    044     * instance.
    045     * </p>
    046     * <p>
    047     * This class also supports the registration of custom bean factories.
    048     * Implementations of the <code>{@link BeanFactory}</code> interface can be
    049     * registered under a symbolic name using the <code>registerBeanFactory()</code>
    050     * method. In the configuration file the name of the bean factory can be
    051     * specified in the bean declaration. Then this factory will be used to create
    052     * the bean.
    053     * </p>
    054     *
    055     * @since 1.3
    056     * @author Oliver Heger
    057     * @version $Id: BeanHelper.java 1089793 2011-04-07 09:45:51Z oheger $
    058     */
    059    public final class BeanHelper
    060    {
    061        /** Stores a map with the registered bean factories. */
    062        private static Map beanFactories = Collections.synchronizedMap(new HashMap());
    063
    064        /**
    065         * Stores the default bean factory, which will be used if no other factory
    066         * is provided.
    067         */
    068        private static BeanFactory defaultBeanFactory = DefaultBeanFactory.INSTANCE;
    069
    070        /**
    071         * Private constructor, so no instances can be created.
    072         */
    073        private BeanHelper()
    074        {
    075        }
    076
    077        /**
    078         * Register a bean factory under a symbolic name. This factory object can
    079         * then be specified in bean declarations with the effect that this factory
    080         * will be used to obtain an instance for the corresponding bean
    081         * declaration.
    082         *
    083         * @param name the name of the factory
    084         * @param factory the factory to be registered
    085         */
    086        public static void registerBeanFactory(String name, BeanFactory factory)
    087        {
    088            if (name == null)
    089            {
    090                throw new IllegalArgumentException(
    091                        "Name for bean factory must not be null!");
    092            }
    093            if (factory == null)
    094            {
    095                throw new IllegalArgumentException("Bean factory must not be null!");
    096            }
    097
    098            beanFactories.put(name, factory);
    099        }
    100
    101        /**
    102         * Deregisters the bean factory with the given name. After that this factory
    103         * cannot be used any longer.
    104         *
    105         * @param name the name of the factory to be deregistered
    106         * @return the factory that was registered under this name; <b>null</b> if
    107         * there was no such factory
    108         */
    109        public static BeanFactory deregisterBeanFactory(String name)
    110        {
    111            return (BeanFactory) beanFactories.remove(name);
    112        }
    113
    114        /**
    115         * Returns a set with the names of all currently registered bean factories.
    116         *
    117         * @return a set with the names of the registered bean factories
    118         */
    119        public static Set registeredFactoryNames()
    120        {
    121            return beanFactories.keySet();
    122        }
    123
    124        /**
    125         * Returns the default bean factory.
    126         *
    127         * @return the default bean factory
    128         */
    129        public static BeanFactory getDefaultBeanFactory()
    130        {
    131            return defaultBeanFactory;
    132        }
    133
    134        /**
    135         * Sets the default bean factory. This factory will be used for all create
    136         * operations, for which no special factory is provided in the bean
    137         * declaration.
    138         *
    139         * @param factory the default bean factory (must not be <b>null</b>)
    140         */
    141        public static void setDefaultBeanFactory(BeanFactory factory)
    142        {
    143            if (factory == null)
    144            {
    145                throw new IllegalArgumentException(
    146                        "Default bean factory must not be null!");
    147            }
    148            defaultBeanFactory = factory;
    149        }
    150
    151        /**
    152         * Initializes the passed in bean. This method will obtain all the bean's
    153         * properties that are defined in the passed in bean declaration. These
    154         * properties will be set on the bean. If necessary, further beans will be
    155         * created recursively.
    156         *
    157         * @param bean the bean to be initialized
    158         * @param data the bean declaration
    159         * @throws ConfigurationRuntimeException if a property cannot be set
    160         */
    161        public static void initBean(Object bean, BeanDeclaration data)
    162                throws ConfigurationRuntimeException
    163        {
    164            initBeanProperties(bean, data);
    165
    166            Map nestedBeans = data.getNestedBeanDeclarations();
    167            if (nestedBeans != null)
    168            {
    169                if (bean instanceof Collection)
    170                {
    171                    Collection coll = (Collection) bean;
    172                    if (nestedBeans.size() == 1)
    173                    {
    174                        Map.Entry e = (Map.Entry) nestedBeans.entrySet().iterator().next();
    175                        String propName = (String) e.getKey();
    176                        Class defaultClass = getDefaultClass(bean, propName);
    177                        if (e.getValue() instanceof List)
    178                        {
    179                            Iterator iter = ((List) e.getValue()).iterator();
    180                            while (iter.hasNext())
    181                            {
    182                                coll.add(createBean((BeanDeclaration) iter.next(), defaultClass));
    183                            }
    184                        }
    185                        else
    186                        {
    187                            BeanDeclaration decl = (BeanDeclaration) e.getValue();
    188                            coll.add(createBean(decl, defaultClass));
    189                        }
    190                    }
    191                }
    192                else
    193                {
    194                    for (Iterator it = nestedBeans.entrySet().iterator(); it.hasNext();)
    195                    {
    196                        Map.Entry e = (Map.Entry) it.next();
    197                        String propName = (String) e.getKey();
    198                        Class defaultClass = getDefaultClass(bean, propName);
    199                        initProperty(bean, propName, createBean(
    200                            (BeanDeclaration) e.getValue(), defaultClass));
    201                    }
    202                }
    203            }
    204        }
    205
    206        /**
    207         * Initializes the beans properties.
    208         *
    209         * @param bean the bean to be initialized
    210         * @param data the bean declaration
    211         * @throws ConfigurationRuntimeException if a property cannot be set
    212         */
    213        public static void initBeanProperties(Object bean, BeanDeclaration data)
    214                throws ConfigurationRuntimeException
    215        {
    216            Map properties = data.getBeanProperties();
    217            if (properties != null)
    218            {
    219                for (Iterator it = properties.entrySet().iterator(); it.hasNext();)
    220                {
    221                    Map.Entry e = (Map.Entry) it.next();
    222                    String propName = (String) e.getKey();
    223                    initProperty(bean, propName, e.getValue());
    224                }
    225            }
    226        }
    227
    228        /**
    229         * Return the Class of the property if it can be determined.
    230         * @param bean The bean containing the property.
    231         * @param propName The name of the property.
    232         * @return The class associated with the property or null.
    233         */
    234        private static Class getDefaultClass(Object bean, String propName)
    235        {
    236            try
    237            {
    238                PropertyDescriptor desc = PropertyUtils.getPropertyDescriptor(bean, propName);
    239                if (desc == null)
    240                {
    241                    return null;
    242                }
    243                return desc.getPropertyType();
    244            }
    245            catch (Exception ex)
    246            {
    247                return null;
    248            }
    249        }
    250
    251        /**
    252         * Sets a property on the given bean using Common Beanutils.
    253         *
    254         * @param bean the bean
    255         * @param propName the name of the property
    256         * @param value the property's value
    257         * @throws ConfigurationRuntimeException if the property is not writeable or
    258         * an error occurred
    259         */
    260        private static void initProperty(Object bean, String propName, Object value)
    261                throws ConfigurationRuntimeException
    262        {
    263            if (!PropertyUtils.isWriteable(bean, propName))
    264            {
    265                throw new ConfigurationRuntimeException("Property " + propName
    266                        + " cannot be set on " + bean.getClass().getName());
    267            }
    268
    269            try
    270            {
    271                BeanUtils.setProperty(bean, propName, value);
    272            }
    273            catch (IllegalAccessException iaex)
    274            {
    275                throw new ConfigurationRuntimeException(iaex);
    276            }
    277            catch (InvocationTargetException itex)
    278            {
    279                throw new ConfigurationRuntimeException(itex);
    280            }
    281        }
    282
    283        /**
    284         * Set a property on the bean only if the property exists
    285         *
    286         * @param bean the bean
    287         * @param propName the name of the property
    288         * @param value the property's value
    289         * @throws ConfigurationRuntimeException if the property is not writeable or
    290         *         an error occurred
    291         */
    292        public static void setProperty(Object bean, String propName, Object value)
    293        {
    294            if (PropertyUtils.isWriteable(bean, propName))
    295            {
    296                initProperty(bean, propName, value);
    297            }
    298        }
    299
    300        /**
    301         * The main method for creating and initializing beans from a configuration.
    302         * This method will return an initialized instance of the bean class
    303         * specified in the passed in bean declaration. If this declaration does not
    304         * contain the class of the bean, the passed in default class will be used.
    305         * From the bean declaration the factory to be used for creating the bean is
    306         * queried. The declaration may here return <b>null</b>, then a default
    307         * factory is used. This factory is then invoked to perform the create
    308         * operation.
    309         *
    310         * @param data the bean declaration
    311         * @param defaultClass the default class to use
    312         * @param param an additional parameter that will be passed to the bean
    313         * factory; some factories may support parameters and behave different
    314         * depending on the value passed in here
    315         * @return the new bean
    316         * @throws ConfigurationRuntimeException if an error occurs
    317         */
    318        public static Object createBean(BeanDeclaration data, Class defaultClass,
    319                Object param) throws ConfigurationRuntimeException
    320        {
    321            if (data == null)
    322            {
    323                throw new IllegalArgumentException(
    324                        "Bean declaration must not be null!");
    325            }
    326
    327            BeanFactory factory = fetchBeanFactory(data);
    328            try
    329            {
    330                return factory.createBean(fetchBeanClass(data, defaultClass,
    331                        factory), data, param);
    332            }
    333            catch (Exception ex)
    334            {
    335                throw new ConfigurationRuntimeException(ex);
    336            }
    337        }
    338
    339        /**
    340         * Returns a bean instance for the specified declaration. This method is a
    341         * short cut for <code>createBean(data, null, null);</code>.
    342         *
    343         * @param data the bean declaration
    344         * @param defaultClass the class to be used when in the declation no class
    345         * is specified
    346         * @return the new bean
    347         * @throws ConfigurationRuntimeException if an error occurs
    348         */
    349        public static Object createBean(BeanDeclaration data, Class defaultClass)
    350                throws ConfigurationRuntimeException
    351        {
    352            return createBean(data, defaultClass, null);
    353        }
    354
    355        /**
    356         * Returns a bean instance for the specified declaration. This method is a
    357         * short cut for <code>createBean(data, null);</code>.
    358         *
    359         * @param data the bean declaration
    360         * @return the new bean
    361         * @throws ConfigurationRuntimeException if an error occurs
    362         */
    363        public static Object createBean(BeanDeclaration data)
    364                throws ConfigurationRuntimeException
    365        {
    366            return createBean(data, null);
    367        }
    368
    369        /**
    370         * Returns a <code>java.lang.Class</code> object for the specified name.
    371         * Because class loading can be tricky in some environments the code for
    372         * retrieving a class by its name was extracted into this helper method. So
    373         * if changes are necessary, they can be made at a single place.
    374         *
    375         * @param name the name of the class to be loaded
    376         * @param callingClass the calling class
    377         * @return the class object for the specified name
    378         * @throws ClassNotFoundException if the class cannot be loaded
    379         */
    380        static Class loadClass(String name, Class callingClass)
    381                throws ClassNotFoundException
    382        {
    383            return ClassUtils.getClass(name);
    384        }
    385
    386        /**
    387         * Determines the class of the bean to be created. If the bean declaration
    388         * contains a class name, this class is used. Otherwise it is checked
    389         * whether a default class is provided. If this is not the case, the
    390         * factory's default class is used. If this class is undefined, too, an
    391         * exception is thrown.
    392         *
    393         * @param data the bean declaration
    394         * @param defaultClass the default class
    395         * @param factory the bean factory to use
    396         * @return the class of the bean to be created
    397         * @throws ConfigurationRuntimeException if the class cannot be determined
    398         */
    399        private static Class fetchBeanClass(BeanDeclaration data,
    400                Class defaultClass, BeanFactory factory)
    401                throws ConfigurationRuntimeException
    402        {
    403            String clsName = data.getBeanClassName();
    404            if (clsName != null)
    405            {
    406                try
    407                {
    408                    return loadClass(clsName, factory.getClass());
    409                }
    410                catch (ClassNotFoundException cex)
    411                {
    412                    throw new ConfigurationRuntimeException(cex);
    413                }
    414            }
    415
    416            if (defaultClass != null)
    417            {
    418                return defaultClass;
    419            }
    420
    421            Class clazz = factory.getDefaultBeanClass();
    422            if (clazz == null)
    423            {
    424                throw new ConfigurationRuntimeException(
    425                        "Bean class is not specified!");
    426            }
    427            return clazz;
    428        }
    429
    430        /**
    431         * Obtains the bean factory to use for creating the specified bean. This
    432         * method will check whether a factory is specified in the bean declaration.
    433         * If this is not the case, the default bean factory will be used.
    434         *
    435         * @param data the bean declaration
    436         * @return the bean factory to use
    437         * @throws ConfigurationRuntimeException if the factory cannot be determined
    438         */
    439        private static BeanFactory fetchBeanFactory(BeanDeclaration data)
    440                throws ConfigurationRuntimeException
    441        {
    442            String factoryName = data.getBeanFactoryName();
    443            if (factoryName != null)
    444            {
    445                BeanFactory factory = (BeanFactory) beanFactories.get(factoryName);
    446                if (factory == null)
    447                {
    448                    throw new ConfigurationRuntimeException(
    449                            "Unknown bean factory: " + factoryName);
    450                }
    451                else
    452                {
    453                    return factory;
    454                }
    455            }
    456            else
    457            {
    458                return getDefaultBeanFactory();
    459            }
    460        }
    461    }