View Javadoc

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.configuration.beanutils;
18  
19  import java.beans.PropertyDescriptor;
20  import java.lang.reflect.InvocationTargetException;
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  
28  import org.apache.commons.beanutils.BeanUtils;
29  import org.apache.commons.beanutils.PropertyUtils;
30  import org.apache.commons.configuration.ConfigurationRuntimeException;
31  import org.apache.commons.lang.ClassUtils;
32  
33  /**
34   * <p>
35   * A helper class for creating bean instances that are defined in configuration
36   * files.
37   * </p>
38   * <p>
39   * This class provides static utility methods related to bean creation
40   * operations. These methods simplify such operations because a client need not
41   * deal with all involved interfaces. Usually, if a bean declaration has already
42   * been obtained, a single method call is necessary to create a new bean
43   * instance.
44   * </p>
45   * <p>
46   * This class also supports the registration of custom bean factories.
47   * Implementations of the {@link BeanFactory} interface can be
48   * registered under a symbolic name using the {@code registerBeanFactory()}
49   * method. In the configuration file the name of the bean factory can be
50   * specified in the bean declaration. Then this factory will be used to create
51   * the bean.
52   * </p>
53   *
54   * @since 1.3
55   * @author <a
56   * href="http://commons.apache.org/configuration/team-list.html">Commons
57   * Configuration team</a>
58   * @version $Id: BeanHelper.java 1366932 2012-07-29 20:06:31Z oheger $
59   */
60  public final class BeanHelper
61  {
62      /** Stores a map with the registered bean factories. */
63      private static final Map<String, BeanFactory> BEAN_FACTORIES = Collections
64              .synchronizedMap(new HashMap<String, BeanFactory>());
65  
66      /**
67       * Stores the default bean factory, which will be used if no other factory
68       * is provided.
69       */
70      private static BeanFactory defaultBeanFactory = DefaultBeanFactory.INSTANCE;
71  
72      /**
73       * Private constructor, so no instances can be created.
74       */
75      private BeanHelper()
76      {
77      }
78  
79      /**
80       * Register a bean factory under a symbolic name. This factory object can
81       * then be specified in bean declarations with the effect that this factory
82       * will be used to obtain an instance for the corresponding bean
83       * declaration.
84       *
85       * @param name the name of the factory
86       * @param factory the factory to be registered
87       */
88      public static void registerBeanFactory(String name, BeanFactory factory)
89      {
90          if (name == null)
91          {
92              throw new IllegalArgumentException(
93                      "Name for bean factory must not be null!");
94          }
95          if (factory == null)
96          {
97              throw new IllegalArgumentException("Bean factory must not be null!");
98          }
99  
100         BEAN_FACTORIES.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 BEAN_FACTORIES.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 BEAN_FACTORIES.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 = BEAN_FACTORIES.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 }