    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 files.
    043 * </p>
    044 * <p>
    045 * This class provides utility methods related to bean creation operations. These methods simplify such operations
    046 * because a client need not deal with all involved interfaces. Usually, if a bean declaration has already been
    047 * obtained, a single method call is necessary to create a new bean instance.
    048 * </p>
    049 * <p>
    050 * This class also supports the registration of custom bean factories. Implementations of the {@link BeanFactory}
    051 * interface can be registered under a symbolic name using the {@code registerBeanFactory()} method. In the
    052 * configuration file the name of the bean factory can be specified in the bean declaration. Then this factory will be
    053 * used to create the bean.
    054 * </p>
    055 * <p>
    056 * In order to create beans using {@code BeanHelper}, create and instance of this class and initialize it accordingly -
    057 * a default {@link BeanFactory} can be passed to the constructor, and additional bean factories can be registered (see
    058 * above). Then this instance can be used to create beans from {@link BeanDeclaration} objects. {@code BeanHelper} is
    059 * thread-safe. So an instance can be passed around in an application and shared between multiple components.
    060 * </p>
    061 *
    062 * @since 1.3
    063 */
    064public final class BeanHelper {
    065
    066    /**
    067     * A default instance of {@code BeanHelper} which can be shared between arbitrary components. If no special
    068     * configuration is needed, this instance can be used throughout an application. Otherwise, new instances can be created
    069     * with their own configuration.
    070     */
    071    public static final BeanHelper INSTANCE = new BeanHelper();
    072
    073    /**
    074     * A special instance of {@code BeanUtilsBean} which is used for all property set and copy operations. This instance was
    075     * initialized with {@code BeanIntrospector} objects which support fluent interfaces. This is required for handling
    076     * builder parameter objects correctly.
    077     */
    078    private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean();
    079
    080    /** Stores a map with the registered bean factories. */
    081    private final Map<String, BeanFactory> beanFactories = Collections.synchronizedMap(new HashMap<>());
    082
    083    /**
    084     * Stores the default bean factory, which is used if no other factory is provided in a bean declaration.
    085     */
    086    private final BeanFactory defaultBeanFactory;
    087
    088    /**
    089     * Constructs a new instance of {@code BeanHelper} with the default instance of {@link DefaultBeanFactory} as default
    090     * {@link BeanFactory}.
    091     */
    092    public BeanHelper() {
    093        this(null);
    094    }
    095
    096    /**
    097     * Constructs a new instance of {@code BeanHelper} and sets the specified default {@code BeanFactory}.
    098     *
    099     * @param defaultBeanFactory the default {@code BeanFactory} (can be <b>null</b>, then a default instance is used)
    100     */
    101    public BeanHelper(final BeanFactory defaultBeanFactory) {
    102        this.defaultBeanFactory = defaultBeanFactory != null ? defaultBeanFactory : DefaultBeanFactory.INSTANCE;
    103    }
    104
    105    /**
    106     * Registers a bean factory under a symbolic name. This factory object can then be specified in bean declarations with
    107     * the effect that this factory will be used to obtain an instance for the corresponding bean declaration.
    108     *
    109     * @param name the name of the factory
    110     * @param factory the factory to be registered
    111     */
    112    public void registerBeanFactory(final String name, final BeanFactory factory) {
    113        if (name == null) {
    114            throw new IllegalArgumentException("Name for bean factory must not be null!");
    115        }
    116        if (factory == null) {
    117            throw new IllegalArgumentException("Bean factory must not be null!");
    118        }
    119
    120        beanFactories.put(name, factory);
    121    }
    122
    123    /**
    124     * Deregisters the bean factory with the given name. After that this factory cannot be used any longer.
    125     *
    126     * @param name the name of the factory to be deregistered
    127     * @return the factory that was registered under this name; <b>null</b> if there was no such factory
    128     */
    129    public BeanFactory deregisterBeanFactory(final String name) {
    130        return beanFactories.remove(name);
    131    }
    132
    133    /**
    134     * Gets a set with the names of all currently registered bean factories.
    135     *
    136     * @return a set with the names of the registered bean factories
    137     */
    138    public Set<String> registeredFactoryNames() {
    139        return beanFactories.keySet();
    140    }
    141
    142    /**
    143     * Gets the default bean factory.
    144     *
    145     * @return the default bean factory
    146     */
    147    public BeanFactory getDefaultBeanFactory() {
    148        return defaultBeanFactory;
    149    }
    150
    151    /**
    152     * Initializes the passed in bean. This method will obtain all the bean's properties that are defined in the passed in
    153     * bean declaration. These properties will be set on the bean. If necessary, further beans will be created recursively.
    154     *
    155     * @param bean the bean to be initialized
    156     * @param data the bean declaration
    157     * @throws ConfigurationRuntimeException if a property cannot be set
    158     */
    159    public void initBean(final Object bean, final BeanDeclaration data) {
    160        initBeanProperties(bean, data);
    161
    162        final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
    163        if (nestedBeans != null) {
    164            if (bean instanceof Collection) {
    165                // This is safe because the collection stores the values of the
    166                // nested beans.
    167                @SuppressWarnings("unchecked")
    168                final Collection<Object> coll = (Collection<Object>) bean;
    169                if (nestedBeans.size() == 1) {
    170                    final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
    171                    final String propName = e.getKey();
    172                    final Class<?> defaultClass = getDefaultClass(bean, propName);
    173                    if (e.getValue() instanceof List) {
    174                        // This is safe, provided that the bean declaration is implemented
    175                        // correctly.
    176                        @SuppressWarnings("unchecked")
    177                        final List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
    178                        for (final BeanDeclaration decl : decls) {
    179                            coll.add(createBean(decl, defaultClass));
    180                        }
    181                    } else {
    182                        final BeanDeclaration decl = (BeanDeclaration) e.getValue();
    183                        coll.add(createBean(decl, defaultClass));
    184                    }
    185                }
    186            } else {
    187                for (final Map.Entry<String, Object> e : nestedBeans.entrySet()) {
    188                    final String propName = e.getKey();
    189                    final Class<?> defaultClass = getDefaultClass(bean, propName);
    190
    191                    final Object prop = e.getValue();
    192
    193                    if (prop instanceof Collection) {
    194                        final Collection<Object> beanCollection = createPropertyCollection(propName, defaultClass);
    195
    196                        for (final Object elemDef : (Collection<?>) prop) {
    197                            beanCollection.add(createBean((BeanDeclaration) elemDef));
    198                        }
    199
    200                        initProperty(bean, propName, beanCollection);
    201                    } else {
    202                        initProperty(bean, propName, createBean((BeanDeclaration) e.getValue(), defaultClass));
    203                    }
    204                }
    205            }
    206        }
    207    }
    208
    209    /**
    210     * Initializes the beans properties.
    211     *
    212     * @param bean the bean to be initialized
    213     * @param data the bean declaration
    214     * @throws ConfigurationRuntimeException if a property cannot be set
    215     */
    216    public static void initBeanProperties(final Object bean, final BeanDeclaration data) {
    217        final Map<String, Object> properties = data.getBeanProperties();
    218        if (properties != null) {
    219            for (final Map.Entry<String, Object> e : properties.entrySet()) {
    220                final String propName = e.getKey();
    221                initProperty(bean, propName, e.getValue());
    222            }
    223        }
    224    }
    225
    226    /**
    227     * Creates a {@code DynaBean} instance which wraps the passed in bean.
    228     *
    229     * @param bean the bean to be wrapped (must not be <b>null</b>)
    230     * @return a {@code DynaBean} wrapping the passed in bean
    231     * @throws IllegalArgumentException if the bean is <b>null</b>
    232     * @since 2.0
    233     */
    234    public static DynaBean createWrapDynaBean(final Object bean) {
    235        if (bean == null) {
    236            throw new IllegalArgumentException("Bean must not be null!");
    237        }
    238        final WrapDynaClass dynaClass = WrapDynaClass.createDynaClass(bean.getClass(), BEAN_UTILS_BEAN.getPropertyUtils());
    239        return new WrapDynaBean(bean, dynaClass);
    240    }
    241
    242    /**
    243     * Copies matching properties from the source bean to the destination bean using a specially configured
    244     * {@code PropertyUtilsBean} instance. This method ensures that enhanced introspection is enabled when doing the copy
    245     * operation.
    246     *
    247     * @param dest the destination bean
    248     * @param orig the source bean
    249     * @throws NoSuchMethodException exception thrown by {@code PropertyUtilsBean}
    250     * @throws InvocationTargetException exception thrown by {@code PropertyUtilsBean}
    251     * @throws IllegalAccessException exception thrown by {@code PropertyUtilsBean}
    252     * @since 2.0
    253     */
    254    public static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    255        BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
    256    }
    257
    258    /**
    259     * Gets the Class of the property if it can be determined.
    260     *
    261     * @param bean The bean containing the property.
    262     * @param propName The name of the property.
    263     * @return The class associated with the property or null.
    264     */
    265    private static Class<?> getDefaultClass(final Object bean, final String propName) {
    266        try {
    267            final PropertyDescriptor desc = BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(bean, propName);
    268            if (desc == null) {
    269                return null;
    270            }
    271            return desc.getPropertyType();
    272        } catch (final Exception ex) {
    273            return null;
    274        }
    275    }
    276
    277    /**
    278     * Sets a property on the given bean using Common Beanutils.
    279     *
    280     * @param bean the bean
    281     * @param propName the name of the property
    282     * @param value the property's value
    283     * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
    284     */
    285    private static void initProperty(final Object bean, final String propName, final Object value) {
    286        if (!isPropertyWriteable(bean, propName)) {
    287            throw new ConfigurationRuntimeException("Property " + propName + " cannot be set on " + bean.getClass().getName());
    288        }
    289
    290        try {
    291            BEAN_UTILS_BEAN.setProperty(bean, propName, value);
    292        } catch (final IllegalAccessException | InvocationTargetException itex) {
    293            throw new ConfigurationRuntimeException(itex);
    294        }
    295    }
    296
    297    /**
    298     * Creates a concrete collection instance to populate a property of type collection. This method tries to guess an
    299     * appropriate collection type. Mostly the type of the property will be one of the collection interfaces rather than a
    300     * concrete class; so we have to create a concrete equivalent.
    301     *
    302     * @param propName the name of the collection property
    303     * @param propertyClass the type of the property
    304     * @return the newly created collection
    305     */
    306    private static Collection<Object> createPropertyCollection(final String propName, final Class<?> propertyClass) {
    307        final Collection<Object> beanCollection;
    308
    309        if (List.class.isAssignableFrom(propertyClass)) {
    310            beanCollection = new ArrayList<>();
    311        } else if (Set.class.isAssignableFrom(propertyClass)) {
    312            beanCollection = new TreeSet<>();
    313        } else {
    314            throw new UnsupportedOperationException("Unable to handle collection of type : " + propertyClass.getName() + " for property " + propName);
    315        }
    316        return beanCollection;
    317    }
    318
    319    /**
    320     * Sets a property on the bean only if the property exists
    321     *
    322     * @param bean the bean
    323     * @param propName the name of the property
    324     * @param value the property's value
    325     * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
    326     */
    327    public static void setProperty(final Object bean, final String propName, final Object value) {
    328        if (isPropertyWriteable(bean, propName)) {
    329            initProperty(bean, propName, value);
    330        }
    331    }
    332
    333    /**
    334     * The main method for creating and initializing beans from a configuration. This method will return an initialized
    335     * instance of the bean class specified in the passed in bean declaration. If this declaration does not contain the
    336     * class of the bean, the passed in default class will be used. From the bean declaration the factory to be used for
    337     * creating the bean is queried. The declaration may here return <b>null</b>, then a default factory is used. This
    338     * factory is then invoked to perform the create operation.
    339     *
    340     * @param data the bean declaration
    341     * @param defaultClass the default class to use
    342     * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
    343     *        and behave different depending on the value passed in here
    344     * @return the new bean
    345     * @throws ConfigurationRuntimeException if an error occurs
    346     */
    347    public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, final Object param) {
    348        if (data == null) {
    349            throw new IllegalArgumentException("Bean declaration must not be null!");
    350        }
    351
    352        final BeanFactory factory = fetchBeanFactory(data);
    353        final BeanCreationContext bcc = createBeanCreationContext(data, defaultClass, param, factory);
    354        try {
    355            return factory.createBean(bcc);
    356        } catch (final Exception ex) {
    357            throw new ConfigurationRuntimeException(ex);
    358        }
    359    }
    360
    361    /**
    362     * Creates a bean instance for the specified declaration. This method is a short cut for
    363     * {@code createBean(data, null, null);}.
    364     *
    365     * @param data the bean declaration
    366     * @param defaultClass the class to be used when in the declaration no class is specified
    367     * @return the new bean
    368     * @throws ConfigurationRuntimeException if an error occurs
    369     */
    370    public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) {
    371        return createBean(data, defaultClass, null);
    372    }
    373
    374    /**
    375     * Creates a bean instance for the specified declaration. This method is a short cut for
    376     * {@code createBean(data, null);}.
    377     *
    378     * @param data the bean declaration
    379     * @return the new bean
    380     * @throws ConfigurationRuntimeException if an error occurs
    381     */
    382    public Object createBean(final BeanDeclaration data) {
    383        return createBean(data, null);
    384    }
    385
    386    /**
    387     * Loads a {@code java.lang.Class} object for the specified name. Because class loading can be tricky in some
    388     * environments the code for retrieving a class by its name was extracted into this helper method. So if changes are
    389     * necessary, they can be made at a single place.
    390     *
    391     * @param name the name of the class to be loaded
    392     * @return the class object for the specified name
    393     * @throws ClassNotFoundException if the class cannot be loaded
    394     */
    395    static Class<?> loadClass(final String name) throws ClassNotFoundException {
    396        return ClassUtils.getClass(name);
    397    }
    398
    399    /**
    400     * Tests whether the specified property of the given bean instance supports write access.
    401     *
    402     * @param bean the bean instance
    403     * @param propName the name of the property in question
    404     * @return <b>true</b> if this property can be written, <b>false</b> otherwise
    405     */
    406    private static boolean isPropertyWriteable(final Object bean, final String propName) {
    407        return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
    408    }
    409
    410    /**
    411     * Determines the class of the bean to be created. If the bean declaration contains a class name, this class is used.
    412     * Otherwise it is checked whether a default class is provided. If this is not the case, the factory's default class is
    413     * used. If this class is undefined, too, an exception is thrown.
    414     *
    415     * @param data the bean declaration
    416     * @param defaultClass the default class
    417     * @param factory the bean factory to use
    418     * @return the class of the bean to be created
    419     * @throws ConfigurationRuntimeException if the class cannot be determined
    420     */
    421    private static Class<?> fetchBeanClass(final BeanDeclaration data, final Class<?> defaultClass, final BeanFactory factory) {
    422        final String clsName = data.getBeanClassName();
    423        if (clsName != null) {
    424            try {
    425                return loadClass(clsName);
    426            } catch (final ClassNotFoundException cex) {
    427                throw new ConfigurationRuntimeException(cex);
    428            }
    429        }
    430
    431        if (defaultClass != null) {
    432            return defaultClass;
    433        }
    434
    435        final Class<?> clazz = factory.getDefaultBeanClass();
    436        if (clazz == null) {
    437            throw new ConfigurationRuntimeException("Bean class is not specified!");
    438        }
    439        return clazz;
    440    }
    441
    442    /**
    443     * Obtains the bean factory to use for creating the specified bean. This method will check whether a factory is
    444     * specified in the bean declaration. If this is not the case, the default bean factory will be used.
    445     *
    446     * @param data the bean declaration
    447     * @return the bean factory to use
    448     * @throws ConfigurationRuntimeException if the factory cannot be determined
    449     */
    450    private BeanFactory fetchBeanFactory(final BeanDeclaration data) {
    451        final String factoryName = data.getBeanFactoryName();
    452        if (factoryName != null) {
    453            final BeanFactory factory = beanFactories.get(factoryName);
    454            if (factory == null) {
    455                throw new ConfigurationRuntimeException("Unknown bean factory: " + factoryName);
    456            }
    457            return factory;
    458        }
    459        return getDefaultBeanFactory();
    460    }
    461
    462    /**
    463     * Creates a {@code BeanCreationContext} object for the creation of the specified bean.
    464     *
    465     * @param data the bean declaration
    466     * @param defaultClass the default class to use
    467     * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
    468     *        and behave different depending on the value passed in here
    469     * @param factory the current bean factory
    470     * @return the {@code BeanCreationContext}
    471     * @throws ConfigurationRuntimeException if the bean class cannot be determined
    472     */
    473    private BeanCreationContext createBeanCreationContext(final BeanDeclaration data, final Class<?> defaultClass, final Object param,
    474        final BeanFactory factory) {
    475        final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
    476        return new BeanCreationContextImpl(this, beanClass, data, param);
    477    }
    478
    479    /**
    480     * Initializes the shared {@code BeanUtilsBean} instance. This method sets up custom bean introspection in a way that
    481     * fluent parameter interfaces are supported.
    482     *
    483     * @return the {@code BeanUtilsBean} instance to be used for all property set operations
    484     */
    485    private static BeanUtilsBean initBeanUtilsBean() {
    486        final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
    487        propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
    488        return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
    489    }
    490
    491    /**
    492     * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a
    493     * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for
    494     * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance
    495     * (which is the instance that created this object).
    496     */
    497    private static final class BeanCreationContextImpl implements BeanCreationContext {
    498        /** The association BeanHelper instance. */
    499        private final BeanHelper beanHelper;
    500
    501        /** The class of the bean to be created. */
    502        private final Class<?> beanClass;
    503
    504        /** The underlying bean declaration. */
    505        private final BeanDeclaration data;
    506
    507        /** The parameter for the bean factory. */
    508        private final Object param;
    509
    510        private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) {
    511            beanHelper = helper;
    512            this.beanClass = beanClass;
    513            this.param = param;
    514            this.data = data;
    515        }
    516
    517        @Override
    518        public void initBean(final Object bean, final BeanDeclaration data) {
    519            beanHelper.initBean(bean, data);
    520        }
    521
    522        @Override
    523        public Object getParameter() {
    524            return param;
    525        }
    526
    527        @Override
    528        public BeanDeclaration getBeanDeclaration() {
    529            return data;
    530        }
    531
    532        @Override
    533        public Class<?> getBeanClass() {
    534            return beanClass;
    535        }
    536
    537        @Override
    538        public Object createBean(final BeanDeclaration data) {
    539            return beanHelper.createBean(data);
    540        }
    541    }
    542}