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