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 }