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