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 }