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 *     http://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.configuration.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.BeanUtils;
031import org.apache.commons.beanutils.PropertyUtils;
032import org.apache.commons.configuration.ConfigurationRuntimeException;
033import org.apache.commons.lang.ClassUtils;
034
035/**
036 * <p>
037 * A helper class for creating bean instances that are defined in configuration
038 * files.
039 * </p>
040 * <p>
041 * This class provides static utility methods related to bean creation
042 * operations. These methods simplify such operations because a client need not
043 * deal with all involved interfaces. Usually, if a bean declaration has already
044 * been obtained, a single method call is necessary to create a new bean
045 * instance.
046 * </p>
047 * <p>
048 * This class also supports the registration of custom bean factories.
049 * Implementations of the {@link BeanFactory} interface can be
050 * registered under a symbolic name using the {@code registerBeanFactory()}
051 * method. In the configuration file the name of the bean factory can be
052 * specified in the bean declaration. Then this factory will be used to create
053 * the bean.
054 * </p>
055 *
056 * @since 1.3
057 * @author <a
058 * href="http://commons.apache.org/configuration/team-list.html">Commons
059 * Configuration team</a>
060 * @version $Id: BeanHelper.java 1534393 2013-10-21 22:02:27Z henning $
061 */
062public final class BeanHelper
063{
064    /** Stores a map with the registered bean factories. */
065    private static final Map<String, BeanFactory> BEAN_FACTORIES = Collections
066            .synchronizedMap(new HashMap<String, BeanFactory>());
067
068    /**
069     * Stores the default bean factory, which will be used if no other factory
070     * is provided.
071     */
072    private static BeanFactory defaultBeanFactory = DefaultBeanFactory.INSTANCE;
073
074    /**
075     * Private constructor, so no instances can be created.
076     */
077    private BeanHelper()
078    {
079    }
080
081    /**
082     * Register a bean factory under a symbolic name. This factory object can
083     * then be specified in bean declarations with the effect that this factory
084     * will be used to obtain an instance for the corresponding bean
085     * declaration.
086     *
087     * @param name the name of the factory
088     * @param factory the factory to be registered
089     */
090    public static void registerBeanFactory(String name, BeanFactory factory)
091    {
092        if (name == null)
093        {
094            throw new IllegalArgumentException(
095                    "Name for bean factory must not be null!");
096        }
097        if (factory == null)
098        {
099            throw new IllegalArgumentException("Bean factory must not be null!");
100        }
101
102        BEAN_FACTORIES.put(name, factory);
103    }
104
105    /**
106     * Deregisters the bean factory with the given name. After that this factory
107     * cannot be used any longer.
108     *
109     * @param name the name of the factory to be deregistered
110     * @return the factory that was registered under this name; <b>null</b> if
111     * there was no such factory
112     */
113    public static BeanFactory deregisterBeanFactory(String name)
114    {
115        return BEAN_FACTORIES.remove(name);
116    }
117
118    /**
119     * Returns a set with the names of all currently registered bean factories.
120     *
121     * @return a set with the names of the registered bean factories
122     */
123    public static Set<String> registeredFactoryNames()
124    {
125        return BEAN_FACTORIES.keySet();
126    }
127
128    /**
129     * Returns the default bean factory.
130     *
131     * @return the default bean factory
132     */
133    public static BeanFactory getDefaultBeanFactory()
134    {
135        return defaultBeanFactory;
136    }
137
138    /**
139     * Sets the default bean factory. This factory will be used for all create
140     * operations, for which no special factory is provided in the bean
141     * declaration.
142     *
143     * @param factory the default bean factory (must not be <b>null</b>)
144     */
145    public static void setDefaultBeanFactory(BeanFactory factory)
146    {
147        if (factory == null)
148        {
149            throw new IllegalArgumentException(
150                    "Default bean factory must not be null!");
151        }
152        defaultBeanFactory = factory;
153    }
154
155    /**
156     * Initializes the passed in bean. This method will obtain all the bean's
157     * properties that are defined in the passed in bean declaration. These
158     * properties will be set on the bean. If necessary, further beans will be
159     * created recursively.
160     *
161     * @param bean the bean to be initialized
162     * @param data the bean declaration
163     * @throws ConfigurationRuntimeException if a property cannot be set
164     */
165    public static void initBean(Object bean, BeanDeclaration data)
166            throws ConfigurationRuntimeException
167    {
168        initBeanProperties(bean, data);
169
170        Map<String, Object> nestedBeans = data.getNestedBeanDeclarations();
171        if (nestedBeans != null)
172        {
173            if (bean instanceof Collection)
174            {
175                // This is safe because the collection stores the values of the
176                // nested beans.
177                @SuppressWarnings("unchecked")
178                Collection<Object> coll = (Collection<Object>) bean;
179                if (nestedBeans.size() == 1)
180                {
181                    Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next();
182                    String propName = e.getKey();
183                    Class<?> defaultClass = getDefaultClass(bean, propName);
184                    if (e.getValue() instanceof List)
185                    {
186                        // This is safe, provided that the bean declaration is implemented
187                        // correctly.
188                        @SuppressWarnings("unchecked")
189                        List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue();
190                        for (BeanDeclaration decl : decls)
191                        {
192                            coll.add(createBean(decl, defaultClass));
193                        }
194                    }
195                    else
196                    {
197                        BeanDeclaration decl = (BeanDeclaration) e.getValue();
198                        coll.add(createBean(decl, defaultClass));
199                    }
200                }
201            }
202            else
203            {
204                for (Map.Entry<String, Object> e : nestedBeans.entrySet())
205                {
206                    String propName = e.getKey();
207                    Class<?> defaultClass = getDefaultClass(bean, propName);
208
209                    Object prop = e.getValue();
210
211                    if (prop instanceof Collection)
212                    {
213                        Collection<Object> beanCollection =
214                                createPropertyCollection(propName, defaultClass);
215
216                        for (Object elemDef : (Collection<?>) prop)
217                        {
218                            beanCollection
219                                    .add(createBean((BeanDeclaration) elemDef));
220                        }
221
222                        initProperty(bean, propName, beanCollection);
223                    }
224                    else
225                    {
226                        initProperty(bean, propName, createBean(
227                            (BeanDeclaration) e.getValue(), defaultClass));
228                    }
229                }
230            }
231        }
232    }
233
234    /**
235     * Initializes the beans properties.
236     *
237     * @param bean the bean to be initialized
238     * @param data the bean declaration
239     * @throws ConfigurationRuntimeException if a property cannot be set
240     */
241    public static void initBeanProperties(Object bean, BeanDeclaration data)
242            throws ConfigurationRuntimeException
243    {
244        Map<String, Object> properties = data.getBeanProperties();
245        if (properties != null)
246        {
247            for (Map.Entry<String, Object> e : properties.entrySet())
248            {
249                String propName = e.getKey();
250                initProperty(bean, propName, e.getValue());
251            }
252        }
253    }
254
255    /**
256     * Return the Class of the property if it can be determined.
257     * @param bean The bean containing the property.
258     * @param propName The name of the property.
259     * @return The class associated with the property or null.
260     */
261    private static Class<?> getDefaultClass(Object bean, String propName)
262    {
263        try
264        {
265            PropertyDescriptor desc = PropertyUtils.getPropertyDescriptor(bean, propName);
266            if (desc == null)
267            {
268                return null;
269            }
270            return desc.getPropertyType();
271        }
272        catch (Exception ex)
273        {
274            return null;
275        }
276    }
277
278    /**
279     * Sets a property on the given bean using Common Beanutils.
280     *
281     * @param bean the bean
282     * @param propName the name of the property
283     * @param value the property's value
284     * @throws ConfigurationRuntimeException if the property is not writeable or
285     * an error occurred
286     */
287    private static void initProperty(Object bean, String propName, Object value)
288            throws ConfigurationRuntimeException
289    {
290        if (!PropertyUtils.isWriteable(bean, propName))
291        {
292            throw new ConfigurationRuntimeException("Property " + propName
293                    + " cannot be set on " + bean.getClass().getName());
294        }
295
296        try
297        {
298            BeanUtils.setProperty(bean, propName, value);
299        }
300        catch (IllegalAccessException iaex)
301        {
302            throw new ConfigurationRuntimeException(iaex);
303        }
304        catch (InvocationTargetException itex)
305        {
306            throw new ConfigurationRuntimeException(itex);
307        }
308    }
309
310    /**
311     * Creates a concrete collection instance to populate a property of type
312     * collection. This method tries to guess an appropriate collection type.
313     * Mostly the type of the property will be one of the collection interfaces
314     * rather than a concrete class; so we have to create a concrete equivalent.
315     *
316     * @param propName the name of the collection property
317     * @param propertyClass the type of the property
318     * @return the newly created collection
319     */
320    private static Collection<Object> createPropertyCollection(String propName,
321            Class<?> propertyClass)
322    {
323        Collection<Object> beanCollection = null;
324
325        if (List.class.isAssignableFrom(propertyClass))
326        {
327            beanCollection = new ArrayList<Object>();
328        }
329        else if (Set.class.isAssignableFrom(propertyClass))
330        {
331            beanCollection = new TreeSet<Object>();
332        }
333        else
334        {
335            throw new UnsupportedOperationException(
336                    "Unable to handle collection of type : "
337                            + propertyClass.getName() + " for property "
338                            + propName);
339        }
340        return beanCollection;
341    }
342
343    /**
344     * Set a property on the bean only if the property exists
345     *
346     * @param bean the bean
347     * @param propName the name of the property
348     * @param value the property's value
349     * @throws ConfigurationRuntimeException if the property is not writeable or
350     *         an error occurred
351     */
352    public static void setProperty(Object bean, String propName, Object value)
353    {
354        if (PropertyUtils.isWriteable(bean, propName))
355        {
356            initProperty(bean, propName, value);
357        }
358    }
359
360    /**
361     * The main method for creating and initializing beans from a configuration.
362     * This method will return an initialized instance of the bean class
363     * specified in the passed in bean declaration. If this declaration does not
364     * contain the class of the bean, the passed in default class will be used.
365     * From the bean declaration the factory to be used for creating the bean is
366     * queried. The declaration may here return <b>null</b>, then a default
367     * factory is used. This factory is then invoked to perform the create
368     * operation.
369     *
370     * @param data the bean declaration
371     * @param defaultClass the default class to use
372     * @param param an additional parameter that will be passed to the bean
373     * factory; some factories may support parameters and behave different
374     * depending on the value passed in here
375     * @return the new bean
376     * @throws ConfigurationRuntimeException if an error occurs
377     */
378    public static Object createBean(BeanDeclaration data, Class<?> defaultClass,
379            Object param) throws ConfigurationRuntimeException
380    {
381        if (data == null)
382        {
383            throw new IllegalArgumentException(
384                    "Bean declaration must not be null!");
385        }
386
387        BeanFactory factory = fetchBeanFactory(data);
388        try
389        {
390            return factory.createBean(fetchBeanClass(data, defaultClass,
391                    factory), data, param);
392        }
393        catch (Exception ex)
394        {
395            throw new ConfigurationRuntimeException(ex);
396        }
397    }
398
399    /**
400     * Returns a bean instance for the specified declaration. This method is a
401     * short cut for {@code createBean(data, null, null);}.
402     *
403     * @param data the bean declaration
404     * @param defaultClass the class to be used when in the declaration no class
405     * is specified
406     * @return the new bean
407     * @throws ConfigurationRuntimeException if an error occurs
408     */
409    public static Object createBean(BeanDeclaration data, Class<?> defaultClass)
410            throws ConfigurationRuntimeException
411    {
412        return createBean(data, defaultClass, null);
413    }
414
415    /**
416     * Returns a bean instance for the specified declaration. This method is a
417     * short cut for {@code createBean(data, null);}.
418     *
419     * @param data the bean declaration
420     * @return the new bean
421     * @throws ConfigurationRuntimeException if an error occurs
422     */
423    public static Object createBean(BeanDeclaration data)
424            throws ConfigurationRuntimeException
425    {
426        return createBean(data, null);
427    }
428
429    /**
430     * Returns a {@code java.lang.Class} object for the specified name.
431     * Because class loading can be tricky in some environments the code for
432     * retrieving a class by its name was extracted into this helper method. So
433     * if changes are necessary, they can be made at a single place.
434     *
435     * @param name the name of the class to be loaded
436     * @param callingClass the calling class
437     * @return the class object for the specified name
438     * @throws ClassNotFoundException if the class cannot be loaded
439     */
440    static Class<?> loadClass(String name, Class<?> callingClass)
441            throws ClassNotFoundException
442    {
443        return ClassUtils.getClass(name);
444    }
445
446    /**
447     * Determines the class of the bean to be created. If the bean declaration
448     * contains a class name, this class is used. Otherwise it is checked
449     * whether a default class is provided. If this is not the case, the
450     * factory's default class is used. If this class is undefined, too, an
451     * exception is thrown.
452     *
453     * @param data the bean declaration
454     * @param defaultClass the default class
455     * @param factory the bean factory to use
456     * @return the class of the bean to be created
457     * @throws ConfigurationRuntimeException if the class cannot be determined
458     */
459    private static Class<?> fetchBeanClass(BeanDeclaration data,
460            Class<?> defaultClass, BeanFactory factory)
461            throws ConfigurationRuntimeException
462    {
463        String clsName = data.getBeanClassName();
464        if (clsName != null)
465        {
466            try
467            {
468                return loadClass(clsName, factory.getClass());
469            }
470            catch (ClassNotFoundException cex)
471            {
472                throw new ConfigurationRuntimeException(cex);
473            }
474        }
475
476        if (defaultClass != null)
477        {
478            return defaultClass;
479        }
480
481        Class<?> clazz = factory.getDefaultBeanClass();
482        if (clazz == null)
483        {
484            throw new ConfigurationRuntimeException(
485                    "Bean class is not specified!");
486        }
487        return clazz;
488    }
489
490    /**
491     * Obtains the bean factory to use for creating the specified bean. This
492     * method will check whether a factory is specified in the bean declaration.
493     * If this is not the case, the default bean factory will be used.
494     *
495     * @param data the bean declaration
496     * @return the bean factory to use
497     * @throws ConfigurationRuntimeException if the factory cannot be determined
498     */
499    private static BeanFactory fetchBeanFactory(BeanDeclaration data)
500            throws ConfigurationRuntimeException
501    {
502        String factoryName = data.getBeanFactoryName();
503        if (factoryName != null)
504        {
505            BeanFactory factory = BEAN_FACTORIES.get(factoryName);
506            if (factory == null)
507            {
508                throw new ConfigurationRuntimeException(
509                        "Unknown bean factory: " + factoryName);
510            }
511            else
512            {
513                return factory;
514            }
515        }
516        else
517        {
518            return getDefaultBeanFactory();
519        }
520    }
521}