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 }