BeanHelper.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.configuration2.beanutils;

  18. import java.beans.PropertyDescriptor;
  19. import java.lang.reflect.InvocationTargetException;
  20. import java.util.ArrayList;
  21. import java.util.Collection;
  22. import java.util.Collections;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.TreeSet;

  28. import org.apache.commons.beanutils.BeanUtilsBean;
  29. import org.apache.commons.beanutils.ConvertUtilsBean;
  30. import org.apache.commons.beanutils.DynaBean;
  31. import org.apache.commons.beanutils.FluentPropertyBeanIntrospector;
  32. import org.apache.commons.beanutils.PropertyUtilsBean;
  33. import org.apache.commons.beanutils.WrapDynaBean;
  34. import org.apache.commons.beanutils.WrapDynaClass;
  35. import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
  36. import org.apache.commons.lang3.ClassUtils;

  37. /**
  38.  * <p>
  39.  * A helper class for creating bean instances that are defined in configuration files.
  40.  * </p>
  41.  * <p>
  42.  * This class provides utility methods related to bean creation operations. These methods simplify such operations
  43.  * because a client need not deal with all involved interfaces. Usually, if a bean declaration has already been
  44.  * obtained, a single method call is necessary to create a new bean instance.
  45.  * </p>
  46.  * <p>
  47.  * This class also supports the registration of custom bean factories. Implementations of the {@link BeanFactory}
  48.  * interface can be registered under a symbolic name using the {@code registerBeanFactory()} method. In the
  49.  * configuration file the name of the bean factory can be specified in the bean declaration. Then this factory will be
  50.  * used to create the bean.
  51.  * </p>
  52.  * <p>
  53.  * In order to create beans using {@code BeanHelper}, create and instance of this class and initialize it accordingly -
  54.  * a default {@link BeanFactory} can be passed to the constructor, and additional bean factories can be registered (see
  55.  * above). Then this instance can be used to create beans from {@link BeanDeclaration} objects. {@code BeanHelper} is
  56.  * thread-safe. So an instance can be passed around in an application and shared between multiple components.
  57.  * </p>
  58.  *
  59.  * @since 1.3
  60.  */
  61. public final class BeanHelper {

  62.     /**
  63.      * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a
  64.      * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for
  65.      * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance
  66.      * (which is the instance that created this object).
  67.      */
  68.     private static final class BeanCreationContextImpl implements BeanCreationContext {
  69.         /** The association BeanHelper instance. */
  70.         private final BeanHelper beanHelper;

  71.         /** The class of the bean to be created. */
  72.         private final Class<?> beanClass;

  73.         /** The underlying bean declaration. */
  74.         private final BeanDeclaration data;

  75.         /** The parameter for the bean factory. */
  76.         private final Object param;

  77.         private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) {
  78.             beanHelper = helper;
  79.             this.beanClass = beanClass;
  80.             this.param = param;
  81.             this.data = data;
  82.         }

  83.         @Override
  84.         public Object createBean(final BeanDeclaration data) {
  85.             return beanHelper.createBean(data);
  86.         }

  87.         @Override
  88.         public Class<?> getBeanClass() {
  89.             return beanClass;
  90.         }

  91.         @Override
  92.         public BeanDeclaration getBeanDeclaration() {
  93.             return data;
  94.         }

  95.         @Override
  96.         public Object getParameter() {
  97.             return param;
  98.         }

  99.         @Override
  100.         public void initBean(final Object bean, final BeanDeclaration data) {
  101.             beanHelper.initBean(bean, data);
  102.         }
  103.     }

  104.     /**
  105.      * A default instance of {@code BeanHelper} which can be shared between arbitrary components. If no special
  106.      * configuration is needed, this instance can be used throughout an application. Otherwise, new instances can be created
  107.      * with their own configuration.
  108.      */
  109.     public static final BeanHelper INSTANCE = new BeanHelper();

  110.     /**
  111.      * A special instance of {@code BeanUtilsBean} which is used for all property set and copy operations. This instance was
  112.      * initialized with {@code BeanIntrospector} objects which support fluent interfaces. This is required for handling
  113.      * builder parameter objects correctly.
  114.      */
  115.     private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean();

  116.     /**
  117.      * Copies matching properties from the source bean to the destination bean using a specially configured
  118.      * {@code PropertyUtilsBean} instance. This method ensures that enhanced introspection is enabled when doing the copy
  119.      * operation.
  120.      *
  121.      * @param dest the destination bean
  122.      * @param orig the source bean
  123.      * @throws NoSuchMethodException exception thrown by {@code PropertyUtilsBean}
  124.      * @throws InvocationTargetException exception thrown by {@code PropertyUtilsBean}
  125.      * @throws IllegalAccessException exception thrown by {@code PropertyUtilsBean}
  126.      * @since 2.0
  127.      */
  128.     public static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  129.         BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
  130.     }

  131.     /**
  132.      * Creates a concrete collection instance to populate a property of type collection. This method tries to guess an
  133.      * appropriate collection type. Mostly the type of the property will be one of the collection interfaces rather than a
  134.      * concrete class; so we have to create a concrete equivalent.
  135.      *
  136.      * @param propName the name of the collection property
  137.      * @param propertyClass the type of the property
  138.      * @return the newly created collection
  139.      */
  140.     private static Collection<Object> createPropertyCollection(final String propName, final Class<?> propertyClass) {
  141.         final Collection<Object> beanCollection;

  142.         if (List.class.isAssignableFrom(propertyClass)) {
  143.             beanCollection = new ArrayList<>();
  144.         } else if (Set.class.isAssignableFrom(propertyClass)) {
  145.             beanCollection = new TreeSet<>();
  146.         } else {
  147.             throw new UnsupportedOperationException("Unable to handle collection of type : " + propertyClass.getName() + " for property " + propName);
  148.         }
  149.         return beanCollection;
  150.     }

  151.     /**
  152.      * Creates a {@code DynaBean} instance which wraps the passed in bean.
  153.      *
  154.      * @param bean the bean to be wrapped (must not be <strong>null</strong>)
  155.      * @return a {@code DynaBean} wrapping the passed in bean
  156.      * @throws IllegalArgumentException if the bean is <strong>null</strong>
  157.      * @since 2.0
  158.      */
  159.     public static DynaBean createWrapDynaBean(final Object bean) {
  160.         if (bean == null) {
  161.             throw new IllegalArgumentException("Bean must not be null!");
  162.         }
  163.         final WrapDynaClass dynaClass = WrapDynaClass.createDynaClass(bean.getClass(), BEAN_UTILS_BEAN.getPropertyUtils());
  164.         return new WrapDynaBean(bean, dynaClass);
  165.     }

  166.     /**
  167.      * Determines the class of the bean to be created. If the bean declaration contains a class name, this class is used.
  168.      * Otherwise it is checked whether a default class is provided. If this is not the case, the factory's default class is
  169.      * used. If this class is undefined, too, an exception is thrown.
  170.      *
  171.      * @param data the bean declaration
  172.      * @param defaultClass the default class
  173.      * @param factory the bean factory to use
  174.      * @return the class of the bean to be created
  175.      * @throws ConfigurationRuntimeException if the class cannot be determined
  176.      */
  177.     private static Class<?> fetchBeanClass(final BeanDeclaration data, final Class<?> defaultClass, final BeanFactory factory) {
  178.         final String clsName = data.getBeanClassName();
  179.         if (clsName != null) {
  180.             try {
  181.                 return loadClass(clsName);
  182.             } catch (final ClassNotFoundException cex) {
  183.                 throw new ConfigurationRuntimeException(cex);
  184.             }
  185.         }

  186.         if (defaultClass != null) {
  187.             return defaultClass;
  188.         }

  189.         final Class<?> clazz = factory.getDefaultBeanClass();
  190.         if (clazz == null) {
  191.             throw new ConfigurationRuntimeException("Bean class is not specified!");
  192.         }
  193.         return clazz;
  194.     }

  195.     /**
  196.      * Gets the Class of the property if it can be determined.
  197.      *
  198.      * @param bean The bean containing the property.
  199.      * @param propName The name of the property.
  200.      * @return The class associated with the property or null.
  201.      */
  202.     private static Class<?> getDefaultClass(final Object bean, final String propName) {
  203.         try {
  204.             final PropertyDescriptor desc = BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(bean, propName);
  205.             if (desc == null) {
  206.                 return null;
  207.             }
  208.             return desc.getPropertyType();
  209.         } catch (final Exception ex) {
  210.             return null;
  211.         }
  212.     }

  213.     /**
  214.      * Initializes the beans properties.
  215.      *
  216.      * @param bean the bean to be initialized
  217.      * @param data the bean declaration
  218.      * @throws ConfigurationRuntimeException if a property cannot be set
  219.      */
  220.     public static void initBeanProperties(final Object bean, final BeanDeclaration data) {
  221.         final Map<String, Object> properties = data.getBeanProperties();
  222.         if (properties != null) {
  223.             properties.forEach((k, v) -> initProperty(bean, k, v));
  224.         }
  225.     }

  226.     /**
  227.      * Initializes the shared {@code BeanUtilsBean} instance. This method sets up custom bean introspection in a way that
  228.      * fluent parameter interfaces are supported.
  229.      *
  230.      * @return the {@code BeanUtilsBean} instance to be used for all property set operations
  231.      */
  232.     private static BeanUtilsBean initBeanUtilsBean() {
  233.         final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
  234.         propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
  235.         return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
  236.     }

  237.     /**
  238.      * Sets a property on the given bean using Common Beanutils.
  239.      *
  240.      * @param bean the bean
  241.      * @param propName the name of the property
  242.      * @param value the property's value
  243.      * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
  244.      */
  245.     private static void initProperty(final Object bean, final String propName, final Object value) {
  246.         if (!isPropertyWriteable(bean, propName)) {
  247.             throw new ConfigurationRuntimeException("Property " + propName + " cannot be set on " + bean.getClass().getName());
  248.         }

  249.         try {
  250.             BEAN_UTILS_BEAN.setProperty(bean, propName, value);
  251.         } catch (final IllegalAccessException | InvocationTargetException itex) {
  252.             throw new ConfigurationRuntimeException(itex);
  253.         }
  254.     }

  255.     /**
  256.      * Tests whether the specified property of the given bean instance supports write access.
  257.      *
  258.      * @param bean the bean instance
  259.      * @param propName the name of the property in question
  260.      * @return <strong>true</strong> if this property can be written, <strong>false</strong> otherwise
  261.      */
  262.     private static boolean isPropertyWriteable(final Object bean, final String propName) {
  263.         return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
  264.     }

  265.     /**
  266.      * Loads a {@code Class} object for the specified name. Because class loading can be tricky in some
  267.      * environments the code for retrieving a class by its name was extracted into this helper method. So if changes are
  268.      * necessary, they can be made at a single place.
  269.      *
  270.      * @param name the name of the class to be loaded
  271.      * @return the class object for the specified name
  272.      * @throws ClassNotFoundException if the class cannot be loaded
  273.      */
  274.     static Class<?> loadClass(final String name) throws ClassNotFoundException {
  275.         return ClassUtils.getClass(name);
  276.     }

  277.     /**
  278.      * Sets a property on the bean only if the property exists
  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.     public static void setProperty(final Object bean, final String propName, final Object value) {
  286.         if (isPropertyWriteable(bean, propName)) {
  287.             initProperty(bean, propName, value);
  288.         }
  289.     }

  290.     /** Stores a map with the registered bean factories. */
  291.     private final Map<String, BeanFactory> beanFactories = Collections.synchronizedMap(new HashMap<>());

  292.     /**
  293.      * Stores the default bean factory, which is used if no other factory is provided in a bean declaration.
  294.      */
  295.     private final BeanFactory defaultBeanFactory;

  296.     /**
  297.      * Constructs a new instance of {@code BeanHelper} with the default instance of {@link DefaultBeanFactory} as default
  298.      * {@link BeanFactory}.
  299.      */
  300.     public BeanHelper() {
  301.         this(null);
  302.     }

  303.     /**
  304.      * Constructs a new instance of {@code BeanHelper} and sets the specified default {@code BeanFactory}.
  305.      *
  306.      * @param defaultBeanFactory the default {@code BeanFactory} (can be <strong>null</strong>, then a default instance is used)
  307.      */
  308.     public BeanHelper(final BeanFactory defaultBeanFactory) {
  309.         this.defaultBeanFactory = defaultBeanFactory != null ? defaultBeanFactory : DefaultBeanFactory.INSTANCE;
  310.     }

  311.     /**
  312.      * Creates a bean instance for the specified declaration. This method is a short cut for
  313.      * {@code createBean(data, null);}.
  314.      *
  315.      * @param data the bean declaration
  316.      * @return the new bean
  317.      * @throws ConfigurationRuntimeException if an error occurs
  318.      */
  319.     public Object createBean(final BeanDeclaration data) {
  320.         return createBean(data, null);
  321.     }

  322.     /**
  323.      * Creates a bean instance for the specified declaration. This method is a short cut for
  324.      * {@code createBean(data, null, null);}.
  325.      *
  326.      * @param data the bean declaration
  327.      * @param defaultClass the class to be used when in the declaration no class is specified
  328.      * @return the new bean
  329.      * @throws ConfigurationRuntimeException if an error occurs
  330.      */
  331.     public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) {
  332.         return createBean(data, defaultClass, null);
  333.     }

  334.     /**
  335.      * The main method for creating and initializing beans from a configuration. This method will return an initialized
  336.      * instance of the bean class specified in the passed in bean declaration. If this declaration does not contain the
  337.      * class of the bean, the passed in default class will be used. From the bean declaration the factory to be used for
  338.      * creating the bean is queried. The declaration may here return <strong>null</strong>, then a default factory is used. This
  339.      * factory is then invoked to perform the create operation.
  340.      *
  341.      * @param data the bean declaration
  342.      * @param defaultClass the default class to use
  343.      * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
  344.      *        and behave different depending on the value passed in here
  345.      * @return the new bean
  346.      * @throws ConfigurationRuntimeException if an error occurs
  347.      */
  348.     public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, final Object param) {
  349.         if (data == null) {
  350.             throw new IllegalArgumentException("Bean declaration must not be null!");
  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.      * Creates a {@code BeanCreationContext} object for the creation of the specified bean.
  362.      *
  363.      * @param data the bean declaration
  364.      * @param defaultClass the default class to use
  365.      * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
  366.      *        and behave different depending on the value passed in here
  367.      * @param factory the current bean factory
  368.      * @return the {@code BeanCreationContext}
  369.      * @throws ConfigurationRuntimeException if the bean class cannot be determined
  370.      */
  371.     private BeanCreationContext createBeanCreationContext(final BeanDeclaration data, final Class<?> defaultClass, final Object param,
  372.         final BeanFactory factory) {
  373.         final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
  374.         return new BeanCreationContextImpl(this, beanClass, data, param);
  375.     }

  376.     /**
  377.      * Deregisters the bean factory with the given name. After that this factory cannot be used any longer.
  378.      *
  379.      * @param name the name of the factory to be deregistered
  380.      * @return the factory that was registered under this name; <strong>null</strong> if there was no such factory
  381.      */
  382.     public BeanFactory deregisterBeanFactory(final String name) {
  383.         return beanFactories.remove(name);
  384.     }

  385.     /**
  386.      * Obtains the bean factory to use for creating the specified bean. This method will check whether a factory is
  387.      * specified in the bean declaration. If this is not the case, the default bean factory will be used.
  388.      *
  389.      * @param data the bean declaration
  390.      * @return the bean factory to use
  391.      * @throws ConfigurationRuntimeException if the factory cannot be determined
  392.      */
  393.     private BeanFactory fetchBeanFactory(final BeanDeclaration data) {
  394.         final String factoryName = data.getBeanFactoryName();
  395.         if (factoryName != null) {
  396.             final BeanFactory factory = beanFactories.get(factoryName);
  397.             if (factory == null) {
  398.                 throw new ConfigurationRuntimeException("Unknown bean factory: " + factoryName);
  399.             }
  400.             return factory;
  401.         }
  402.         return getDefaultBeanFactory();
  403.     }

  404.     /**
  405.      * Gets the default bean factory.
  406.      *
  407.      * @return the default bean factory
  408.      */
  409.     public BeanFactory getDefaultBeanFactory() {
  410.         return defaultBeanFactory;
  411.     }

  412.     /**
  413.      * Initializes the passed in bean. This method will obtain all the bean's properties that are defined in the passed in
  414.      * bean declaration. These properties will be set on the bean. If necessary, further beans will be created recursively.
  415.      *
  416.      * @param bean the bean to be initialized
  417.      * @param data the bean declaration
  418.      * @throws ConfigurationRuntimeException if a property cannot be set
  419.      */
  420.     public void initBean(final Object bean, final BeanDeclaration data) {
  421.         initBeanProperties(bean, data);

  422.         final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
  423.         if (nestedBeans != null) {
  424.             if (bean instanceof Collection) {
  425.                 // This is safe because the collection stores the values of the
  426.                 // nested beans.
  427.                 @SuppressWarnings("unchecked")
  428.                 final Collection<Object> coll = (Collection<Object>) bean;
  429.                 if (nestedBeans.size() == 1) {
  430.                     final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
  431.                     final String propName = e.getKey();
  432.                     final Class<?> defaultClass = getDefaultClass(bean, propName);
  433.                     if (e.getValue() instanceof List) {
  434.                         // This is safe, provided that the bean declaration is implemented
  435.                         // correctly.
  436.                         @SuppressWarnings("unchecked")
  437.                         final List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
  438.                         decls.forEach(decl -> coll.add(createBean(decl, defaultClass)));
  439.                     } else {
  440.                         coll.add(createBean((BeanDeclaration) e.getValue(), defaultClass));
  441.                     }
  442.                 }
  443.             } else {
  444.                 nestedBeans.forEach((propName, prop) -> {
  445.                     final Class<?> defaultClass = getDefaultClass(bean, propName);
  446.                     if (prop instanceof Collection) {
  447.                         final Collection<Object> beanCollection = createPropertyCollection(propName, defaultClass);
  448.                         ((Collection<BeanDeclaration>) prop).forEach(elemDef -> beanCollection.add(createBean(elemDef)));
  449.                         initProperty(bean, propName, beanCollection);
  450.                     } else {
  451.                         initProperty(bean, propName, createBean((BeanDeclaration) prop, defaultClass));
  452.                     }
  453.                 });
  454.             }
  455.         }
  456.     }

  457.     /**
  458.      * Registers a bean factory under a symbolic name. This factory object can then be specified in bean declarations with
  459.      * the effect that this factory will be used to obtain an instance for the corresponding bean declaration.
  460.      *
  461.      * @param name the name of the factory
  462.      * @param factory the factory to be registered
  463.      */
  464.     public void registerBeanFactory(final String name, final BeanFactory factory) {
  465.         if (name == null) {
  466.             throw new IllegalArgumentException("Name for bean factory must not be null!");
  467.         }
  468.         if (factory == null) {
  469.             throw new IllegalArgumentException("Bean factory must not be null!");
  470.         }

  471.         beanFactories.put(name, factory);
  472.     }

  473.     /**
  474.      * Gets a set with the names of all currently registered bean factories.
  475.      *
  476.      * @return a set with the names of the registered bean factories
  477.      */
  478.     public Set<String> registeredFactoryNames() {
  479.         return beanFactories.keySet();
  480.     }
  481. }