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