blob: a34d6e6da1b66fba24b38e6a3fe00a73a9f002db [file] [log] [blame]
/*
* 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
{
/**
* 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();
/** Stores a map with the registered bean factories. */
private final Map<String, BeanFactory> beanFactories = Collections
.synchronizedMap(new HashMap<String, BeanFactory>());
/**
* Stores the default bean factory, which is used if no other factory
* is provided in a bean declaration.
*/
private final BeanFactory defaultBeanFactory;
/**
* Creates a new instance of {@code BeanHelper} with the default instance of
* {@link DefaultBeanFactory} as default {@link BeanFactory}.
*/
public BeanHelper()
{
this(null);
}
/**
* Creates a new instance of {@code BeanHelper} and sets the specified
* default {@code BeanFactory}.
*
* @param defFactory the default {@code BeanFactory} (can be <b>null</b>,
* then a default instance is used)
*/
public BeanHelper(final BeanFactory defFactory)
{
defaultBeanFactory =
defFactory != null ? defFactory : DefaultBeanFactory.INSTANCE;
}
/**
* Register 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);
}
/**
* 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; <b>null</b> if
* there was no such factory
*/
public BeanFactory deregisterBeanFactory(final String name)
{
return beanFactories.remove(name);
}
/**
* Returns 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();
}
/**
* Returns 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();
for (final BeanDeclaration decl : decls)
{
coll.add(createBean(decl, defaultClass));
}
}
else
{
final BeanDeclaration decl = (BeanDeclaration) e.getValue();
coll.add(createBean(decl, defaultClass));
}
}
}
else
{
for (final Map.Entry<String, Object> e : nestedBeans.entrySet())
{
final String propName = e.getKey();
final Class<?> defaultClass = getDefaultClass(bean, propName);
final Object prop = e.getValue();
if (prop instanceof Collection)
{
final Collection<Object> beanCollection =
createPropertyCollection(propName, defaultClass);
for (final Object elemDef : (Collection<?>) prop)
{
beanCollection
.add(createBean((BeanDeclaration) elemDef));
}
initProperty(bean, propName, beanCollection);
}
else
{
initProperty(bean, propName, createBean(
(BeanDeclaration) e.getValue(), defaultClass));
}
}
}
}
}
/**
* 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)
{
for (final Map.Entry<String, Object> e : properties.entrySet())
{
final String propName = e.getKey();
initProperty(bean, propName, e.getValue());
}
}
}
/**
* Creates a {@code DynaBean} instance which wraps the passed in bean.
*
* @param bean the bean to be wrapped (must not be <b>null</b>)
* @return a {@code DynaBean} wrapping the passed in bean
* @throws IllegalArgumentException if the bean is <b>null</b>
* @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);
}
/**
* 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);
}
/**
* Return 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;
}
}
/**
* 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 writeable 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);
}
}
/**
* 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)
{
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;
}
/**
* Set 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 writeable 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);
}
}
/**
* 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 <b>null</b>, 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);
}
}
/**
* Returns 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);
}
/**
* Returns 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);
}
/**
* Returns a {@code java.lang.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);
}
/**
* Checks 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 <b>true</b> if this property can be written, <b>false</b>
* otherwise
*/
private static boolean isPropertyWriteable(final Object bean, final String propName)
{
return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
}
/**
* 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;
}
/**
* 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();
}
/**
* 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);
}
/**
* 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);
}
/**
* 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 void initBean(final Object bean, final BeanDeclaration data)
{
beanHelper.initBean(bean, data);
}
@Override
public Object getParameter()
{
return param;
}
@Override
public BeanDeclaration getBeanDeclaration()
{
return data;
}
@Override
public Class<?> getBeanClass()
{
return beanClass;
}
@Override
public Object createBean(final BeanDeclaration data)
{
return beanHelper.createBean(data);
}
}
}