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