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 }