All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.configuration2.beanutils.BeanHelper Maven / Gradle / Ivy

Go to download

Tools to assist in the reading of configuration/preferences files in various formats

There is a newer version: 2.10.1
Show newest version
/*
 * 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;

/**
 * 

* A helper class for creating bean instances that are defined in configuration * files. *

*

* 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. *

*

* 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. *

*

* 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. *

* * @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 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; /** * 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 null, * 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; null 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 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 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 coll = (Collection) bean; if (nestedBeans.size() == 1) { final Map.Entry 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 decls = (List) 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 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 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 properties = data.getBeanProperties(); if (properties != null) { for (final Map.Entry 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 null) * @return a {@code DynaBean} wrapping the passed in bean * @throws IllegalArgumentException if the bean is null * @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 iaex) { throw new ConfigurationRuntimeException(iaex); } catch (final 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 createPropertyCollection(final String propName, final Class propertyClass) { Collection 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 null, 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 true if this property can be written, false * 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); } } }