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.configuration2.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.BeanUtilsBean; 031import org.apache.commons.beanutils.ConvertUtilsBean; 032import org.apache.commons.beanutils.DynaBean; 033import org.apache.commons.beanutils.FluentPropertyBeanIntrospector; 034import org.apache.commons.beanutils.PropertyUtilsBean; 035import org.apache.commons.beanutils.WrapDynaBean; 036import org.apache.commons.beanutils.WrapDynaClass; 037import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 038import org.apache.commons.lang3.ClassUtils; 039 040/** 041 * <p> 042 * A helper class for creating bean instances that are defined in configuration 043 * files. 044 * </p> 045 * <p> 046 * This class provides utility methods related to bean creation 047 * operations. These methods simplify such operations because a client need not 048 * deal with all involved interfaces. Usually, if a bean declaration has already 049 * been obtained, a single method call is necessary to create a new bean 050 * instance. 051 * </p> 052 * <p> 053 * This class also supports the registration of custom bean factories. 054 * Implementations of the {@link BeanFactory} interface can be 055 * registered under a symbolic name using the {@code registerBeanFactory()} 056 * method. In the configuration file the name of the bean factory can be 057 * specified in the bean declaration. Then this factory will be used to create 058 * the bean. 059 * </p> 060 * <p> 061 * In order to create beans using {@code BeanHelper}, create and instance of 062 * this class and initialize it accordingly - a default {@link BeanFactory} 063 * can be passed to the constructor, and additional bean factories can be 064 * registered (see above). Then this instance can be used to create beans from 065 * {@link BeanDeclaration} objects. {@code BeanHelper} is thread-safe. So an 066 * instance can be passed around in an application and shared between multiple 067 * components. 068 * </p> 069 * 070 * @since 1.3 071 */ 072public final class BeanHelper 073{ 074 /** 075 * A default instance of {@code BeanHelper} which can be shared between 076 * arbitrary components. If no special configuration is needed, this 077 * instance can be used throughout an application. Otherwise, new instances 078 * can be created with their own configuration. 079 */ 080 public static final BeanHelper INSTANCE = new BeanHelper(); 081 082 /** 083 * A special instance of {@code BeanUtilsBean} which is used for all 084 * property set and copy operations. This instance was initialized with 085 * {@code BeanIntrospector} objects which support fluent interfaces. This is 086 * required for handling builder parameter objects correctly. 087 */ 088 private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean(); 089 090 /** Stores a map with the registered bean factories. */ 091 private final Map<String, BeanFactory> beanFactories = Collections 092 .synchronizedMap(new HashMap<String, BeanFactory>()); 093 094 /** 095 * Stores the default bean factory, which is used if no other factory 096 * is provided in a bean declaration. 097 */ 098 private final BeanFactory defaultBeanFactory; 099 100 /** 101 * Creates a new instance of {@code BeanHelper} with the default instance of 102 * {@link DefaultBeanFactory} as default {@link BeanFactory}. 103 */ 104 public BeanHelper() 105 { 106 this(null); 107 } 108 109 /** 110 * Creates a new instance of {@code BeanHelper} and sets the specified 111 * default {@code BeanFactory}. 112 * 113 * @param defFactory the default {@code BeanFactory} (can be <b>null</b>, 114 * then a default instance is used) 115 */ 116 public BeanHelper(final BeanFactory defFactory) 117 { 118 defaultBeanFactory = 119 defFactory != null ? defFactory : DefaultBeanFactory.INSTANCE; 120 } 121 122 /** 123 * Register a bean factory under a symbolic name. This factory object can 124 * then be specified in bean declarations with the effect that this factory 125 * will be used to obtain an instance for the corresponding bean 126 * declaration. 127 * 128 * @param name the name of the factory 129 * @param factory the factory to be registered 130 */ 131 public void registerBeanFactory(final String name, final BeanFactory factory) 132 { 133 if (name == null) 134 { 135 throw new IllegalArgumentException( 136 "Name for bean factory must not be null!"); 137 } 138 if (factory == null) 139 { 140 throw new IllegalArgumentException("Bean factory must not be null!"); 141 } 142 143 beanFactories.put(name, factory); 144 } 145 146 /** 147 * Deregisters the bean factory with the given name. After that this factory 148 * cannot be used any longer. 149 * 150 * @param name the name of the factory to be deregistered 151 * @return the factory that was registered under this name; <b>null</b> if 152 * there was no such factory 153 */ 154 public BeanFactory deregisterBeanFactory(final String name) 155 { 156 return beanFactories.remove(name); 157 } 158 159 /** 160 * Returns a set with the names of all currently registered bean factories. 161 * 162 * @return a set with the names of the registered bean factories 163 */ 164 public Set<String> registeredFactoryNames() 165 { 166 return beanFactories.keySet(); 167 } 168 169 /** 170 * Returns the default bean factory. 171 * 172 * @return the default bean factory 173 */ 174 public BeanFactory getDefaultBeanFactory() 175 { 176 return defaultBeanFactory; 177 } 178 179 /** 180 * Initializes the passed in bean. This method will obtain all the bean's 181 * properties that are defined in the passed in bean declaration. These 182 * properties will be set on the bean. If necessary, further beans will be 183 * created recursively. 184 * 185 * @param bean the bean to be initialized 186 * @param data the bean declaration 187 * @throws ConfigurationRuntimeException if a property cannot be set 188 */ 189 public void initBean(final Object bean, final BeanDeclaration data) 190 { 191 initBeanProperties(bean, data); 192 193 final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations(); 194 if (nestedBeans != null) 195 { 196 if (bean instanceof Collection) 197 { 198 // This is safe because the collection stores the values of the 199 // nested beans. 200 @SuppressWarnings("unchecked") 201 final 202 Collection<Object> coll = (Collection<Object>) bean; 203 if (nestedBeans.size() == 1) 204 { 205 final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next(); 206 final String propName = e.getKey(); 207 final Class<?> defaultClass = getDefaultClass(bean, propName); 208 if (e.getValue() instanceof List) 209 { 210 // This is safe, provided that the bean declaration is implemented 211 // correctly. 212 @SuppressWarnings("unchecked") 213 final 214 List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue(); 215 for (final BeanDeclaration decl : decls) 216 { 217 coll.add(createBean(decl, defaultClass)); 218 } 219 } 220 else 221 { 222 final BeanDeclaration decl = (BeanDeclaration) e.getValue(); 223 coll.add(createBean(decl, defaultClass)); 224 } 225 } 226 } 227 else 228 { 229 for (final Map.Entry<String, Object> e : nestedBeans.entrySet()) 230 { 231 final String propName = e.getKey(); 232 final Class<?> defaultClass = getDefaultClass(bean, propName); 233 234 final Object prop = e.getValue(); 235 236 if (prop instanceof Collection) 237 { 238 final Collection<Object> beanCollection = 239 createPropertyCollection(propName, defaultClass); 240 241 for (final Object elemDef : (Collection<?>) prop) 242 { 243 beanCollection 244 .add(createBean((BeanDeclaration) elemDef)); 245 } 246 247 initProperty(bean, propName, beanCollection); 248 } 249 else 250 { 251 initProperty(bean, propName, createBean( 252 (BeanDeclaration) e.getValue(), defaultClass)); 253 } 254 } 255 } 256 } 257 } 258 259 /** 260 * Initializes the beans properties. 261 * 262 * @param bean the bean to be initialized 263 * @param data the bean declaration 264 * @throws ConfigurationRuntimeException if a property cannot be set 265 */ 266 public static void initBeanProperties(final Object bean, final BeanDeclaration data) 267 { 268 final Map<String, Object> properties = data.getBeanProperties(); 269 if (properties != null) 270 { 271 for (final Map.Entry<String, Object> e : properties.entrySet()) 272 { 273 final String propName = e.getKey(); 274 initProperty(bean, propName, e.getValue()); 275 } 276 } 277 } 278 279 /** 280 * Creates a {@code DynaBean} instance which wraps the passed in bean. 281 * 282 * @param bean the bean to be wrapped (must not be <b>null</b>) 283 * @return a {@code DynaBean} wrapping the passed in bean 284 * @throws IllegalArgumentException if the bean is <b>null</b> 285 * @since 2.0 286 */ 287 public static DynaBean createWrapDynaBean(final Object bean) 288 { 289 if (bean == null) 290 { 291 throw new IllegalArgumentException("Bean must not be null!"); 292 } 293 final WrapDynaClass dynaClass = 294 WrapDynaClass.createDynaClass(bean.getClass(), 295 BEAN_UTILS_BEAN.getPropertyUtils()); 296 return new WrapDynaBean(bean, dynaClass); 297 } 298 299 /** 300 * Copies matching properties from the source bean to the destination bean 301 * using a specially configured {@code PropertyUtilsBean} instance. This 302 * method ensures that enhanced introspection is enabled when doing the copy 303 * operation. 304 * 305 * @param dest the destination bean 306 * @param orig the source bean 307 * @throws NoSuchMethodException exception thrown by 308 * {@code PropertyUtilsBean} 309 * @throws InvocationTargetException exception thrown by 310 * {@code PropertyUtilsBean} 311 * @throws IllegalAccessException exception thrown by 312 * {@code PropertyUtilsBean} 313 * @since 2.0 314 */ 315 public static void copyProperties(final Object dest, final Object orig) 316 throws IllegalAccessException, InvocationTargetException, 317 NoSuchMethodException 318 { 319 BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig); 320 } 321 322 /** 323 * Return the Class of the property if it can be determined. 324 * @param bean The bean containing the property. 325 * @param propName The name of the property. 326 * @return The class associated with the property or null. 327 */ 328 private static Class<?> getDefaultClass(final Object bean, final String propName) 329 { 330 try 331 { 332 final PropertyDescriptor desc = 333 BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor( 334 bean, propName); 335 if (desc == null) 336 { 337 return null; 338 } 339 return desc.getPropertyType(); 340 } 341 catch (final Exception ex) 342 { 343 return null; 344 } 345 } 346 347 /** 348 * Sets a property on the given bean using Common Beanutils. 349 * 350 * @param bean the bean 351 * @param propName the name of the property 352 * @param value the property's value 353 * @throws ConfigurationRuntimeException if the property is not writeable or 354 * an error occurred 355 */ 356 private static void initProperty(final Object bean, final String propName, final Object value) 357 { 358 if (!isPropertyWriteable(bean, propName)) 359 { 360 throw new ConfigurationRuntimeException("Property " + propName 361 + " cannot be set on " + bean.getClass().getName()); 362 } 363 364 try 365 { 366 BEAN_UTILS_BEAN.setProperty(bean, propName, value); 367 } 368 catch (final IllegalAccessException iaex) 369 { 370 throw new ConfigurationRuntimeException(iaex); 371 } 372 catch (final InvocationTargetException itex) 373 { 374 throw new ConfigurationRuntimeException(itex); 375 } 376 } 377 378 /** 379 * Creates a concrete collection instance to populate a property of type 380 * collection. This method tries to guess an appropriate collection type. 381 * Mostly the type of the property will be one of the collection interfaces 382 * rather than a concrete class; so we have to create a concrete equivalent. 383 * 384 * @param propName the name of the collection property 385 * @param propertyClass the type of the property 386 * @return the newly created collection 387 */ 388 private static Collection<Object> createPropertyCollection(final String propName, 389 final Class<?> propertyClass) 390 { 391 Collection<Object> beanCollection; 392 393 if (List.class.isAssignableFrom(propertyClass)) 394 { 395 beanCollection = new ArrayList<>(); 396 } 397 else if (Set.class.isAssignableFrom(propertyClass)) 398 { 399 beanCollection = new TreeSet<>(); 400 } 401 else 402 { 403 throw new UnsupportedOperationException( 404 "Unable to handle collection of type : " 405 + propertyClass.getName() + " for property " 406 + propName); 407 } 408 return beanCollection; 409 } 410 411 /** 412 * Set a property on the bean only if the property exists 413 * 414 * @param bean the bean 415 * @param propName the name of the property 416 * @param value the property's value 417 * @throws ConfigurationRuntimeException if the property is not writeable or 418 * an error occurred 419 */ 420 public static void setProperty(final Object bean, final String propName, final Object value) 421 { 422 if (isPropertyWriteable(bean, propName)) 423 { 424 initProperty(bean, propName, value); 425 } 426 } 427 428 /** 429 * The main method for creating and initializing beans from a configuration. 430 * This method will return an initialized instance of the bean class 431 * specified in the passed in bean declaration. If this declaration does not 432 * contain the class of the bean, the passed in default class will be used. 433 * From the bean declaration the factory to be used for creating the bean is 434 * queried. The declaration may here return <b>null</b>, then a default 435 * factory is used. This factory is then invoked to perform the create 436 * operation. 437 * 438 * @param data the bean declaration 439 * @param defaultClass the default class to use 440 * @param param an additional parameter that will be passed to the bean 441 * factory; some factories may support parameters and behave different 442 * depending on the value passed in here 443 * @return the new bean 444 * @throws ConfigurationRuntimeException if an error occurs 445 */ 446 public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, 447 final Object param) 448 { 449 if (data == null) 450 { 451 throw new IllegalArgumentException( 452 "Bean declaration must not be null!"); 453 } 454 455 final BeanFactory factory = fetchBeanFactory(data); 456 final BeanCreationContext bcc = 457 createBeanCreationContext(data, defaultClass, param, factory); 458 try 459 { 460 return factory.createBean(bcc); 461 } 462 catch (final Exception ex) 463 { 464 throw new ConfigurationRuntimeException(ex); 465 } 466 } 467 468 /** 469 * Returns a bean instance for the specified declaration. This method is a 470 * short cut for {@code createBean(data, null, null);}. 471 * 472 * @param data the bean declaration 473 * @param defaultClass the class to be used when in the declaration no class 474 * is specified 475 * @return the new bean 476 * @throws ConfigurationRuntimeException if an error occurs 477 */ 478 public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) 479 { 480 return createBean(data, defaultClass, null); 481 } 482 483 /** 484 * Returns a bean instance for the specified declaration. This method is a 485 * short cut for {@code createBean(data, null);}. 486 * 487 * @param data the bean declaration 488 * @return the new bean 489 * @throws ConfigurationRuntimeException if an error occurs 490 */ 491 public Object createBean(final BeanDeclaration data) 492 { 493 return createBean(data, null); 494 } 495 496 /** 497 * Returns a {@code java.lang.Class} object for the specified name. 498 * Because class loading can be tricky in some environments the code for 499 * retrieving a class by its name was extracted into this helper method. So 500 * if changes are necessary, they can be made at a single place. 501 * 502 * @param name the name of the class to be loaded 503 * @return the class object for the specified name 504 * @throws ClassNotFoundException if the class cannot be loaded 505 */ 506 static Class<?> loadClass(final String name) throws ClassNotFoundException 507 { 508 return ClassUtils.getClass(name); 509 } 510 511 /** 512 * Checks whether the specified property of the given bean instance supports 513 * write access. 514 * 515 * @param bean the bean instance 516 * @param propName the name of the property in question 517 * @return <b>true</b> if this property can be written, <b>false</b> 518 * otherwise 519 */ 520 private static boolean isPropertyWriteable(final Object bean, final String propName) 521 { 522 return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName); 523 } 524 525 /** 526 * Determines the class of the bean to be created. If the bean declaration 527 * contains a class name, this class is used. Otherwise it is checked 528 * whether a default class is provided. If this is not the case, the 529 * factory's default class is used. If this class is undefined, too, an 530 * exception is thrown. 531 * 532 * @param data the bean declaration 533 * @param defaultClass the default class 534 * @param factory the bean factory to use 535 * @return the class of the bean to be created 536 * @throws ConfigurationRuntimeException if the class cannot be determined 537 */ 538 private static Class<?> fetchBeanClass(final BeanDeclaration data, 539 final Class<?> defaultClass, final BeanFactory factory) 540 { 541 final String clsName = data.getBeanClassName(); 542 if (clsName != null) 543 { 544 try 545 { 546 return loadClass(clsName); 547 } 548 catch (final ClassNotFoundException cex) 549 { 550 throw new ConfigurationRuntimeException(cex); 551 } 552 } 553 554 if (defaultClass != null) 555 { 556 return defaultClass; 557 } 558 559 final Class<?> clazz = factory.getDefaultBeanClass(); 560 if (clazz == null) 561 { 562 throw new ConfigurationRuntimeException( 563 "Bean class is not specified!"); 564 } 565 return clazz; 566 } 567 568 /** 569 * Obtains the bean factory to use for creating the specified bean. This 570 * method will check whether a factory is specified in the bean declaration. 571 * If this is not the case, the default bean factory will be used. 572 * 573 * @param data the bean declaration 574 * @return the bean factory to use 575 * @throws ConfigurationRuntimeException if the factory cannot be determined 576 */ 577 private BeanFactory fetchBeanFactory(final BeanDeclaration data) 578 { 579 final String factoryName = data.getBeanFactoryName(); 580 if (factoryName != null) 581 { 582 final BeanFactory factory = beanFactories.get(factoryName); 583 if (factory == null) 584 { 585 throw new ConfigurationRuntimeException( 586 "Unknown bean factory: " + factoryName); 587 } 588 return factory; 589 } 590 return getDefaultBeanFactory(); 591 } 592 593 /** 594 * Creates a {@code BeanCreationContext} object for the creation of the 595 * specified bean. 596 * 597 * @param data the bean declaration 598 * @param defaultClass the default class to use 599 * @param param an additional parameter that will be passed to the bean 600 * factory; some factories may support parameters and behave 601 * different depending on the value passed in here 602 * @param factory the current bean factory 603 * @return the {@code BeanCreationContext} 604 * @throws ConfigurationRuntimeException if the bean class cannot be 605 * determined 606 */ 607 private BeanCreationContext createBeanCreationContext( 608 final BeanDeclaration data, final Class<?> defaultClass, 609 final Object param, final BeanFactory factory) 610 { 611 final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory); 612 return new BeanCreationContextImpl(this, beanClass, data, param); 613 } 614 615 /** 616 * Initializes the shared {@code BeanUtilsBean} instance. This method sets 617 * up custom bean introspection in a way that fluent parameter interfaces 618 * are supported. 619 * 620 * @return the {@code BeanUtilsBean} instance to be used for all property 621 * set operations 622 */ 623 private static BeanUtilsBean initBeanUtilsBean() 624 { 625 final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean(); 626 propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector()); 627 return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean); 628 } 629 630 /** 631 * An implementation of the {@code BeanCreationContext} interface used by 632 * {@code BeanHelper} to communicate with a {@code BeanFactory}. This class 633 * contains all information required for the creation of a bean. The methods 634 * for creating and initializing bean instances are implemented by calling 635 * back to the provided {@code BeanHelper} instance (which is the instance 636 * that created this object). 637 */ 638 private static final class BeanCreationContextImpl implements BeanCreationContext 639 { 640 /** The association BeanHelper instance. */ 641 private final BeanHelper beanHelper; 642 643 /** The class of the bean to be created. */ 644 private final Class<?> beanClass; 645 646 /** The underlying bean declaration. */ 647 private final BeanDeclaration data; 648 649 /** The parameter for the bean factory. */ 650 private final Object param; 651 652 private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, 653 final BeanDeclaration data, final Object param) 654 { 655 beanHelper = helper; 656 this.beanClass = beanClass; 657 this.param = param; 658 this.data = data; 659 } 660 661 @Override 662 public void initBean(final Object bean, final BeanDeclaration data) 663 { 664 beanHelper.initBean(bean, data); 665 } 666 667 @Override 668 public Object getParameter() 669 { 670 return param; 671 } 672 673 @Override 674 public BeanDeclaration getBeanDeclaration() 675 { 676 return data; 677 } 678 679 @Override 680 public Class<?> getBeanClass() 681 { 682 return beanClass; 683 } 684 685 @Override 686 public Object createBean(final BeanDeclaration data) 687 { 688 return beanHelper.createBean(data); 689 } 690 } 691}