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