BeanHelper.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.configuration2.beanutils;
- import java.beans.PropertyDescriptor;
- import java.lang.reflect.InvocationTargetException;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.TreeSet;
- import org.apache.commons.beanutils.BeanUtilsBean;
- import org.apache.commons.beanutils.ConvertUtilsBean;
- import org.apache.commons.beanutils.DynaBean;
- import org.apache.commons.beanutils.FluentPropertyBeanIntrospector;
- import org.apache.commons.beanutils.PropertyUtilsBean;
- import org.apache.commons.beanutils.WrapDynaBean;
- import org.apache.commons.beanutils.WrapDynaClass;
- import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
- import org.apache.commons.lang3.ClassUtils;
- /**
- * <p>
- * A helper class for creating bean instances that are defined in configuration files.
- * </p>
- * <p>
- * This class provides utility methods related to bean creation operations. These methods simplify such operations
- * because a client need not deal with all involved interfaces. Usually, if a bean declaration has already been
- * obtained, a single method call is necessary to create a new bean instance.
- * </p>
- * <p>
- * This class also supports the registration of custom bean factories. Implementations of the {@link BeanFactory}
- * interface can be registered under a symbolic name using the {@code registerBeanFactory()} method. In the
- * configuration file the name of the bean factory can be specified in the bean declaration. Then this factory will be
- * used to create the bean.
- * </p>
- * <p>
- * In order to create beans using {@code BeanHelper}, create and instance of this class and initialize it accordingly -
- * a default {@link BeanFactory} can be passed to the constructor, and additional bean factories can be registered (see
- * above). Then this instance can be used to create beans from {@link BeanDeclaration} objects. {@code BeanHelper} is
- * thread-safe. So an instance can be passed around in an application and shared between multiple components.
- * </p>
- *
- * @since 1.3
- */
- public final class BeanHelper {
- /**
- * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a
- * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for
- * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance
- * (which is the instance that created this object).
- */
- private static final class BeanCreationContextImpl implements BeanCreationContext {
- /** The association BeanHelper instance. */
- private final BeanHelper beanHelper;
- /** The class of the bean to be created. */
- private final Class<?> beanClass;
- /** The underlying bean declaration. */
- private final BeanDeclaration data;
- /** The parameter for the bean factory. */
- private final Object param;
- private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) {
- beanHelper = helper;
- this.beanClass = beanClass;
- this.param = param;
- this.data = data;
- }
- @Override
- public Object createBean(final BeanDeclaration data) {
- return beanHelper.createBean(data);
- }
- @Override
- public Class<?> getBeanClass() {
- return beanClass;
- }
- @Override
- public BeanDeclaration getBeanDeclaration() {
- return data;
- }
- @Override
- public Object getParameter() {
- return param;
- }
- @Override
- public void initBean(final Object bean, final BeanDeclaration data) {
- beanHelper.initBean(bean, data);
- }
- }
- /**
- * A default instance of {@code BeanHelper} which can be shared between arbitrary components. If no special
- * configuration is needed, this instance can be used throughout an application. Otherwise, new instances can be created
- * with their own configuration.
- */
- public static final BeanHelper INSTANCE = new BeanHelper();
- /**
- * A special instance of {@code BeanUtilsBean} which is used for all property set and copy operations. This instance was
- * initialized with {@code BeanIntrospector} objects which support fluent interfaces. This is required for handling
- * builder parameter objects correctly.
- */
- private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean();
- /**
- * Copies matching properties from the source bean to the destination bean using a specially configured
- * {@code PropertyUtilsBean} instance. This method ensures that enhanced introspection is enabled when doing the copy
- * operation.
- *
- * @param dest the destination bean
- * @param orig the source bean
- * @throws NoSuchMethodException exception thrown by {@code PropertyUtilsBean}
- * @throws InvocationTargetException exception thrown by {@code PropertyUtilsBean}
- * @throws IllegalAccessException exception thrown by {@code PropertyUtilsBean}
- * @since 2.0
- */
- public static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
- BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
- }
- /**
- * Creates a concrete collection instance to populate a property of type collection. This method tries to guess an
- * appropriate collection type. Mostly the type of the property will be one of the collection interfaces rather than a
- * concrete class; so we have to create a concrete equivalent.
- *
- * @param propName the name of the collection property
- * @param propertyClass the type of the property
- * @return the newly created collection
- */
- private static Collection<Object> createPropertyCollection(final String propName, final Class<?> propertyClass) {
- final Collection<Object> beanCollection;
- if (List.class.isAssignableFrom(propertyClass)) {
- beanCollection = new ArrayList<>();
- } else if (Set.class.isAssignableFrom(propertyClass)) {
- beanCollection = new TreeSet<>();
- } else {
- throw new UnsupportedOperationException("Unable to handle collection of type : " + propertyClass.getName() + " for property " + propName);
- }
- return beanCollection;
- }
- /**
- * Creates a {@code DynaBean} instance which wraps the passed in bean.
- *
- * @param bean the bean to be wrapped (must not be <strong>null</strong>)
- * @return a {@code DynaBean} wrapping the passed in bean
- * @throws IllegalArgumentException if the bean is <strong>null</strong>
- * @since 2.0
- */
- public static DynaBean createWrapDynaBean(final Object bean) {
- if (bean == null) {
- throw new IllegalArgumentException("Bean must not be null!");
- }
- final WrapDynaClass dynaClass = WrapDynaClass.createDynaClass(bean.getClass(), BEAN_UTILS_BEAN.getPropertyUtils());
- return new WrapDynaBean(bean, dynaClass);
- }
- /**
- * Determines the class of the bean to be created. If the bean declaration contains a class name, this class is used.
- * Otherwise it is checked whether a default class is provided. If this is not the case, the factory's default class is
- * used. If this class is undefined, too, an exception is thrown.
- *
- * @param data the bean declaration
- * @param defaultClass the default class
- * @param factory the bean factory to use
- * @return the class of the bean to be created
- * @throws ConfigurationRuntimeException if the class cannot be determined
- */
- private static Class<?> fetchBeanClass(final BeanDeclaration data, final Class<?> defaultClass, final BeanFactory factory) {
- final String clsName = data.getBeanClassName();
- if (clsName != null) {
- try {
- return loadClass(clsName);
- } catch (final ClassNotFoundException cex) {
- throw new ConfigurationRuntimeException(cex);
- }
- }
- if (defaultClass != null) {
- return defaultClass;
- }
- final Class<?> clazz = factory.getDefaultBeanClass();
- if (clazz == null) {
- throw new ConfigurationRuntimeException("Bean class is not specified!");
- }
- return clazz;
- }
- /**
- * Gets the Class of the property if it can be determined.
- *
- * @param bean The bean containing the property.
- * @param propName The name of the property.
- * @return The class associated with the property or null.
- */
- private static Class<?> getDefaultClass(final Object bean, final String propName) {
- try {
- final PropertyDescriptor desc = BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(bean, propName);
- if (desc == null) {
- return null;
- }
- return desc.getPropertyType();
- } catch (final Exception ex) {
- return null;
- }
- }
- /**
- * Initializes the beans properties.
- *
- * @param bean the bean to be initialized
- * @param data the bean declaration
- * @throws ConfigurationRuntimeException if a property cannot be set
- */
- public static void initBeanProperties(final Object bean, final BeanDeclaration data) {
- final Map<String, Object> properties = data.getBeanProperties();
- if (properties != null) {
- properties.forEach((k, v) -> initProperty(bean, k, v));
- }
- }
- /**
- * Initializes the shared {@code BeanUtilsBean} instance. This method sets up custom bean introspection in a way that
- * fluent parameter interfaces are supported.
- *
- * @return the {@code BeanUtilsBean} instance to be used for all property set operations
- */
- private static BeanUtilsBean initBeanUtilsBean() {
- final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
- propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
- return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
- }
- /**
- * Sets a property on the given bean using Common Beanutils.
- *
- * @param bean the bean
- * @param propName the name of the property
- * @param value the property's value
- * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
- */
- private static void initProperty(final Object bean, final String propName, final Object value) {
- if (!isPropertyWriteable(bean, propName)) {
- throw new ConfigurationRuntimeException("Property " + propName + " cannot be set on " + bean.getClass().getName());
- }
- try {
- BEAN_UTILS_BEAN.setProperty(bean, propName, value);
- } catch (final IllegalAccessException | InvocationTargetException itex) {
- throw new ConfigurationRuntimeException(itex);
- }
- }
- /**
- * Tests whether the specified property of the given bean instance supports write access.
- *
- * @param bean the bean instance
- * @param propName the name of the property in question
- * @return <strong>true</strong> if this property can be written, <strong>false</strong> otherwise
- */
- private static boolean isPropertyWriteable(final Object bean, final String propName) {
- return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
- }
- /**
- * Loads a {@code Class} object for the specified name. Because class loading can be tricky in some
- * environments the code for retrieving a class by its name was extracted into this helper method. So if changes are
- * necessary, they can be made at a single place.
- *
- * @param name the name of the class to be loaded
- * @return the class object for the specified name
- * @throws ClassNotFoundException if the class cannot be loaded
- */
- static Class<?> loadClass(final String name) throws ClassNotFoundException {
- return ClassUtils.getClass(name);
- }
- /**
- * Sets a property on the bean only if the property exists
- *
- * @param bean the bean
- * @param propName the name of the property
- * @param value the property's value
- * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
- */
- public static void setProperty(final Object bean, final String propName, final Object value) {
- if (isPropertyWriteable(bean, propName)) {
- initProperty(bean, propName, value);
- }
- }
- /** Stores a map with the registered bean factories. */
- private final Map<String, BeanFactory> beanFactories = Collections.synchronizedMap(new HashMap<>());
- /**
- * Stores the default bean factory, which is used if no other factory is provided in a bean declaration.
- */
- private final BeanFactory defaultBeanFactory;
- /**
- * Constructs a new instance of {@code BeanHelper} with the default instance of {@link DefaultBeanFactory} as default
- * {@link BeanFactory}.
- */
- public BeanHelper() {
- this(null);
- }
- /**
- * Constructs a new instance of {@code BeanHelper} and sets the specified default {@code BeanFactory}.
- *
- * @param defaultBeanFactory the default {@code BeanFactory} (can be <strong>null</strong>, then a default instance is used)
- */
- public BeanHelper(final BeanFactory defaultBeanFactory) {
- this.defaultBeanFactory = defaultBeanFactory != null ? defaultBeanFactory : DefaultBeanFactory.INSTANCE;
- }
- /**
- * Creates a bean instance for the specified declaration. This method is a short cut for
- * {@code createBean(data, null);}.
- *
- * @param data the bean declaration
- * @return the new bean
- * @throws ConfigurationRuntimeException if an error occurs
- */
- public Object createBean(final BeanDeclaration data) {
- return createBean(data, null);
- }
- /**
- * Creates a bean instance for the specified declaration. This method is a short cut for
- * {@code createBean(data, null, null);}.
- *
- * @param data the bean declaration
- * @param defaultClass the class to be used when in the declaration no class is specified
- * @return the new bean
- * @throws ConfigurationRuntimeException if an error occurs
- */
- public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) {
- return createBean(data, defaultClass, null);
- }
- /**
- * The main method for creating and initializing beans from a configuration. This method will return an initialized
- * instance of the bean class specified in the passed in bean declaration. If this declaration does not contain the
- * class of the bean, the passed in default class will be used. From the bean declaration the factory to be used for
- * creating the bean is queried. The declaration may here return <strong>null</strong>, then a default factory is used. This
- * factory is then invoked to perform the create operation.
- *
- * @param data the bean declaration
- * @param defaultClass the default class to use
- * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
- * and behave different depending on the value passed in here
- * @return the new bean
- * @throws ConfigurationRuntimeException if an error occurs
- */
- public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, final Object param) {
- if (data == null) {
- throw new IllegalArgumentException("Bean declaration must not be null!");
- }
- final BeanFactory factory = fetchBeanFactory(data);
- final BeanCreationContext bcc = createBeanCreationContext(data, defaultClass, param, factory);
- try {
- return factory.createBean(bcc);
- } catch (final Exception ex) {
- throw new ConfigurationRuntimeException(ex);
- }
- }
- /**
- * Creates a {@code BeanCreationContext} object for the creation of the specified bean.
- *
- * @param data the bean declaration
- * @param defaultClass the default class to use
- * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
- * and behave different depending on the value passed in here
- * @param factory the current bean factory
- * @return the {@code BeanCreationContext}
- * @throws ConfigurationRuntimeException if the bean class cannot be determined
- */
- private BeanCreationContext createBeanCreationContext(final BeanDeclaration data, final Class<?> defaultClass, final Object param,
- final BeanFactory factory) {
- final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
- return new BeanCreationContextImpl(this, beanClass, data, param);
- }
- /**
- * Deregisters the bean factory with the given name. After that this factory cannot be used any longer.
- *
- * @param name the name of the factory to be deregistered
- * @return the factory that was registered under this name; <strong>null</strong> if there was no such factory
- */
- public BeanFactory deregisterBeanFactory(final String name) {
- return beanFactories.remove(name);
- }
- /**
- * Obtains the bean factory to use for creating the specified bean. This method will check whether a factory is
- * specified in the bean declaration. If this is not the case, the default bean factory will be used.
- *
- * @param data the bean declaration
- * @return the bean factory to use
- * @throws ConfigurationRuntimeException if the factory cannot be determined
- */
- private BeanFactory fetchBeanFactory(final BeanDeclaration data) {
- final String factoryName = data.getBeanFactoryName();
- if (factoryName != null) {
- final BeanFactory factory = beanFactories.get(factoryName);
- if (factory == null) {
- throw new ConfigurationRuntimeException("Unknown bean factory: " + factoryName);
- }
- return factory;
- }
- return getDefaultBeanFactory();
- }
- /**
- * Gets the default bean factory.
- *
- * @return the default bean factory
- */
- public BeanFactory getDefaultBeanFactory() {
- return defaultBeanFactory;
- }
- /**
- * Initializes the passed in bean. This method will obtain all the bean's properties that are defined in the passed in
- * bean declaration. These properties will be set on the bean. If necessary, further beans will be created recursively.
- *
- * @param bean the bean to be initialized
- * @param data the bean declaration
- * @throws ConfigurationRuntimeException if a property cannot be set
- */
- public void initBean(final Object bean, final BeanDeclaration data) {
- initBeanProperties(bean, data);
- final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
- if (nestedBeans != null) {
- if (bean instanceof Collection) {
- // This is safe because the collection stores the values of the
- // nested beans.
- @SuppressWarnings("unchecked")
- final Collection<Object> coll = (Collection<Object>) bean;
- if (nestedBeans.size() == 1) {
- final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
- final String propName = e.getKey();
- final Class<?> defaultClass = getDefaultClass(bean, propName);
- if (e.getValue() instanceof List) {
- // This is safe, provided that the bean declaration is implemented
- // correctly.
- @SuppressWarnings("unchecked")
- final List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
- decls.forEach(decl -> coll.add(createBean(decl, defaultClass)));
- } else {
- coll.add(createBean((BeanDeclaration) e.getValue(), defaultClass));
- }
- }
- } else {
- nestedBeans.forEach((propName, prop) -> {
- final Class<?> defaultClass = getDefaultClass(bean, propName);
- if (prop instanceof Collection) {
- final Collection<Object> beanCollection = createPropertyCollection(propName, defaultClass);
- ((Collection<BeanDeclaration>) prop).forEach(elemDef -> beanCollection.add(createBean(elemDef)));
- initProperty(bean, propName, beanCollection);
- } else {
- initProperty(bean, propName, createBean((BeanDeclaration) prop, defaultClass));
- }
- });
- }
- }
- }
- /**
- * Registers a bean factory under a symbolic name. This factory object can then be specified in bean declarations with
- * the effect that this factory will be used to obtain an instance for the corresponding bean declaration.
- *
- * @param name the name of the factory
- * @param factory the factory to be registered
- */
- public void registerBeanFactory(final String name, final BeanFactory factory) {
- if (name == null) {
- throw new IllegalArgumentException("Name for bean factory must not be null!");
- }
- if (factory == null) {
- throw new IllegalArgumentException("Bean factory must not be null!");
- }
- beanFactories.put(name, factory);
- }
- /**
- * Gets a set with the names of all currently registered bean factories.
- *
- * @return a set with the names of the registered bean factories
- */
- public Set<String> registeredFactoryNames() {
- return beanFactories.keySet();
- }
- }