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