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