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