1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.configuration2.beanutils; 18 19 import java.beans.PropertyDescriptor; 20 import java.lang.reflect.InvocationTargetException; 21 import java.util.ArrayList; 22 import java.util.Collection; 23 import java.util.Collections; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Set; 28 import java.util.TreeSet; 29 30 import org.apache.commons.beanutils.BeanUtilsBean; 31 import org.apache.commons.beanutils.ConvertUtilsBean; 32 import org.apache.commons.beanutils.DynaBean; 33 import org.apache.commons.beanutils.FluentPropertyBeanIntrospector; 34 import org.apache.commons.beanutils.PropertyUtilsBean; 35 import org.apache.commons.beanutils.WrapDynaBean; 36 import org.apache.commons.beanutils.WrapDynaClass; 37 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 38 import org.apache.commons.lang3.ClassUtils; 39 40 /** 41 * <p> 42 * A helper class for creating bean instances that are defined in configuration files. 43 * </p> 44 * <p> 45 * This class provides utility methods related to bean creation operations. These methods simplify such operations 46 * because a client need not deal with all involved interfaces. Usually, if a bean declaration has already been 47 * obtained, a single method call is necessary to create a new bean instance. 48 * </p> 49 * <p> 50 * This class also supports the registration of custom bean factories. Implementations of the {@link BeanFactory} 51 * interface can be registered under a symbolic name using the {@code registerBeanFactory()} method. In the 52 * configuration file the name of the bean factory can be specified in the bean declaration. Then this factory will be 53 * used to create the bean. 54 * </p> 55 * <p> 56 * In order to create beans using {@code BeanHelper}, create and instance of this class and initialize it accordingly - 57 * a default {@link BeanFactory} can be passed to the constructor, and additional bean factories can be registered (see 58 * above). Then this instance can be used to create beans from {@link BeanDeclaration} objects. {@code BeanHelper} is 59 * thread-safe. So an instance can be passed around in an application and shared between multiple components. 60 * </p> 61 * 62 * @since 1.3 63 */ 64 public final class BeanHelper { 65 66 /** 67 * A default instance of {@code BeanHelper} which can be shared between arbitrary components. If no special 68 * configuration is needed, this instance can be used throughout an application. Otherwise, new instances can be created 69 * with their own configuration. 70 */ 71 public static final BeanHelper INSTANCE = new BeanHelper(); 72 73 /** 74 * A special instance of {@code BeanUtilsBean} which is used for all property set and copy operations. This instance was 75 * initialized with {@code BeanIntrospector} objects which support fluent interfaces. This is required for handling 76 * builder parameter objects correctly. 77 */ 78 private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean(); 79 80 /** Stores a map with the registered bean factories. */ 81 private final Map<String, BeanFactory> beanFactories = Collections.synchronizedMap(new HashMap<>()); 82 83 /** 84 * Stores the default bean factory, which is used if no other factory is provided in a bean declaration. 85 */ 86 private final BeanFactory defaultBeanFactory; 87 88 /** 89 * Constructs a new instance of {@code BeanHelper} with the default instance of {@link DefaultBeanFactory} as default 90 * {@link BeanFactory}. 91 */ 92 public BeanHelper() { 93 this(null); 94 } 95 96 /** 97 * Constructs a new instance of {@code BeanHelper} and sets the specified default {@code BeanFactory}. 98 * 99 * @param defaultBeanFactory the default {@code BeanFactory} (can be <b>null</b>, then a default instance is used) 100 */ 101 public BeanHelper(final BeanFactory defaultBeanFactory) { 102 this.defaultBeanFactory = defaultBeanFactory != null ? defaultBeanFactory : DefaultBeanFactory.INSTANCE; 103 } 104 105 /** 106 * Registers a bean factory under a symbolic name. This factory object can then be specified in bean declarations with 107 * the effect that this factory will be used to obtain an instance for the corresponding bean declaration. 108 * 109 * @param name the name of the factory 110 * @param factory the factory to be registered 111 */ 112 public void registerBeanFactory(final String name, final BeanFactory factory) { 113 if (name == null) { 114 throw new IllegalArgumentException("Name for bean factory must not be null!"); 115 } 116 if (factory == null) { 117 throw new IllegalArgumentException("Bean factory must not be null!"); 118 } 119 120 beanFactories.put(name, factory); 121 } 122 123 /** 124 * Deregisters the bean factory with the given name. After that this factory cannot be used any longer. 125 * 126 * @param name the name of the factory to be deregistered 127 * @return the factory that was registered under this name; <b>null</b> if there was no such factory 128 */ 129 public BeanFactory deregisterBeanFactory(final String name) { 130 return beanFactories.remove(name); 131 } 132 133 /** 134 * Gets a set with the names of all currently registered bean factories. 135 * 136 * @return a set with the names of the registered bean factories 137 */ 138 public Set<String> registeredFactoryNames() { 139 return beanFactories.keySet(); 140 } 141 142 /** 143 * Gets the default bean factory. 144 * 145 * @return the default bean factory 146 */ 147 public BeanFactory getDefaultBeanFactory() { 148 return defaultBeanFactory; 149 } 150 151 /** 152 * Initializes the passed in bean. This method will obtain all the bean's properties that are defined in the passed in 153 * bean declaration. These properties will be set on the bean. If necessary, further beans will be created recursively. 154 * 155 * @param bean the bean to be initialized 156 * @param data the bean declaration 157 * @throws ConfigurationRuntimeException if a property cannot be set 158 */ 159 public void initBean(final Object bean, final BeanDeclaration data) { 160 initBeanProperties(bean, data); 161 162 final Map<String, Object> nestedBeans = data.getNestedBeanDeclarations(); 163 if (nestedBeans != null) { 164 if (bean instanceof Collection) { 165 // This is safe because the collection stores the values of the 166 // nested beans. 167 @SuppressWarnings("unchecked") 168 final Collection<Object> coll = (Collection<Object>) bean; 169 if (nestedBeans.size() == 1) { 170 final Map.Entry<String, Object> e = nestedBeans.entrySet().iterator().next(); 171 final String propName = e.getKey(); 172 final Class<?> defaultClass = getDefaultClass(bean, propName); 173 if (e.getValue() instanceof List) { 174 // This is safe, provided that the bean declaration is implemented 175 // correctly. 176 @SuppressWarnings("unchecked") 177 final List<BeanDeclaration> decls = (List<BeanDeclaration>) e.getValue(); 178 decls.forEach(decl -> coll.add(createBean(decl, defaultClass))); 179 } else { 180 coll.add(createBean((BeanDeclaration) e.getValue(), defaultClass)); 181 } 182 } 183 } else { 184 nestedBeans.forEach((propName, prop) -> { 185 final Class<?> defaultClass = getDefaultClass(bean, propName); 186 if (prop instanceof Collection) { 187 final Collection<Object> beanCollection = createPropertyCollection(propName, defaultClass); 188 ((Collection<BeanDeclaration>) prop).forEach(elemDef -> beanCollection.add(createBean(elemDef))); 189 initProperty(bean, propName, beanCollection); 190 } else { 191 initProperty(bean, propName, createBean((BeanDeclaration) prop, defaultClass)); 192 } 193 }); 194 } 195 } 196 } 197 198 /** 199 * Initializes the beans properties. 200 * 201 * @param bean the bean to be initialized 202 * @param data the bean declaration 203 * @throws ConfigurationRuntimeException if a property cannot be set 204 */ 205 public static void initBeanProperties(final Object bean, final BeanDeclaration data) { 206 final Map<String, Object> properties = data.getBeanProperties(); 207 if (properties != null) { 208 properties.forEach((k, v) -> initProperty(bean, k, v)); 209 } 210 } 211 212 /** 213 * Creates a {@code DynaBean} instance which wraps the passed in bean. 214 * 215 * @param bean the bean to be wrapped (must not be <b>null</b>) 216 * @return a {@code DynaBean} wrapping the passed in bean 217 * @throws IllegalArgumentException if the bean is <b>null</b> 218 * @since 2.0 219 */ 220 public static DynaBean createWrapDynaBean(final Object bean) { 221 if (bean == null) { 222 throw new IllegalArgumentException("Bean must not be null!"); 223 } 224 final WrapDynaClass dynaClass = WrapDynaClass.createDynaClass(bean.getClass(), BEAN_UTILS_BEAN.getPropertyUtils()); 225 return new WrapDynaBean(bean, dynaClass); 226 } 227 228 /** 229 * Copies matching properties from the source bean to the destination bean using a specially configured 230 * {@code PropertyUtilsBean} instance. This method ensures that enhanced introspection is enabled when doing the copy 231 * operation. 232 * 233 * @param dest the destination bean 234 * @param orig the source bean 235 * @throws NoSuchMethodException exception thrown by {@code PropertyUtilsBean} 236 * @throws InvocationTargetException exception thrown by {@code PropertyUtilsBean} 237 * @throws IllegalAccessException exception thrown by {@code PropertyUtilsBean} 238 * @since 2.0 239 */ 240 public static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 241 BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig); 242 } 243 244 /** 245 * Gets the Class of the property if it can be determined. 246 * 247 * @param bean The bean containing the property. 248 * @param propName The name of the property. 249 * @return The class associated with the property or null. 250 */ 251 private static Class<?> getDefaultClass(final Object bean, final String propName) { 252 try { 253 final PropertyDescriptor desc = BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(bean, propName); 254 if (desc == null) { 255 return null; 256 } 257 return desc.getPropertyType(); 258 } catch (final Exception ex) { 259 return null; 260 } 261 } 262 263 /** 264 * Sets a property on the given bean using Common Beanutils. 265 * 266 * @param bean the bean 267 * @param propName the name of the property 268 * @param value the property's value 269 * @throws ConfigurationRuntimeException if the property is not writable or an error occurred 270 */ 271 private static void initProperty(final Object bean, final String propName, final Object value) { 272 if (!isPropertyWriteable(bean, propName)) { 273 throw new ConfigurationRuntimeException("Property " + propName + " cannot be set on " + bean.getClass().getName()); 274 } 275 276 try { 277 BEAN_UTILS_BEAN.setProperty(bean, propName, value); 278 } catch (final IllegalAccessException | InvocationTargetException itex) { 279 throw new ConfigurationRuntimeException(itex); 280 } 281 } 282 283 /** 284 * Creates a concrete collection instance to populate a property of type collection. This method tries to guess an 285 * appropriate collection type. Mostly the type of the property will be one of the collection interfaces rather than a 286 * concrete class; so we have to create a concrete equivalent. 287 * 288 * @param propName the name of the collection property 289 * @param propertyClass the type of the property 290 * @return the newly created collection 291 */ 292 private static Collection<Object> createPropertyCollection(final String propName, final Class<?> propertyClass) { 293 final Collection<Object> beanCollection; 294 295 if (List.class.isAssignableFrom(propertyClass)) { 296 beanCollection = new ArrayList<>(); 297 } else if (Set.class.isAssignableFrom(propertyClass)) { 298 beanCollection = new TreeSet<>(); 299 } else { 300 throw new UnsupportedOperationException("Unable to handle collection of type : " + propertyClass.getName() + " for property " + propName); 301 } 302 return beanCollection; 303 } 304 305 /** 306 * Sets a property on the bean only if the property exists 307 * 308 * @param bean the bean 309 * @param propName the name of the property 310 * @param value the property's value 311 * @throws ConfigurationRuntimeException if the property is not writable or an error occurred 312 */ 313 public static void setProperty(final Object bean, final String propName, final Object value) { 314 if (isPropertyWriteable(bean, propName)) { 315 initProperty(bean, propName, value); 316 } 317 } 318 319 /** 320 * The main method for creating and initializing beans from a configuration. This method will return an initialized 321 * instance of the bean class specified in the passed in bean declaration. If this declaration does not contain the 322 * class of the bean, the passed in default class will be used. From the bean declaration the factory to be used for 323 * creating the bean is queried. The declaration may here return <b>null</b>, then a default factory is used. This 324 * factory is then invoked to perform the create operation. 325 * 326 * @param data the bean declaration 327 * @param defaultClass the default class to use 328 * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters 329 * and behave different depending on the value passed in here 330 * @return the new bean 331 * @throws ConfigurationRuntimeException if an error occurs 332 */ 333 public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, final Object param) { 334 if (data == null) { 335 throw new IllegalArgumentException("Bean declaration must not be null!"); 336 } 337 338 final BeanFactory factory = fetchBeanFactory(data); 339 final BeanCreationContext bcc = createBeanCreationContext(data, defaultClass, param, factory); 340 try { 341 return factory.createBean(bcc); 342 } catch (final Exception ex) { 343 throw new ConfigurationRuntimeException(ex); 344 } 345 } 346 347 /** 348 * Creates a bean instance for the specified declaration. This method is a short cut for 349 * {@code createBean(data, null, null);}. 350 * 351 * @param data the bean declaration 352 * @param defaultClass the class to be used when in the declaration no class is specified 353 * @return the new bean 354 * @throws ConfigurationRuntimeException if an error occurs 355 */ 356 public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) { 357 return createBean(data, defaultClass, null); 358 } 359 360 /** 361 * Creates a bean instance for the specified declaration. This method is a short cut for 362 * {@code createBean(data, null);}. 363 * 364 * @param data the bean declaration 365 * @return the new bean 366 * @throws ConfigurationRuntimeException if an error occurs 367 */ 368 public Object createBean(final BeanDeclaration data) { 369 return createBean(data, null); 370 } 371 372 /** 373 * Loads a {@code Class} object for the specified name. Because class loading can be tricky in some 374 * environments the code for retrieving a class by its name was extracted into this helper method. So if changes are 375 * necessary, they can be made at a single place. 376 * 377 * @param name the name of the class to be loaded 378 * @return the class object for the specified name 379 * @throws ClassNotFoundException if the class cannot be loaded 380 */ 381 static Class<?> loadClass(final String name) throws ClassNotFoundException { 382 return ClassUtils.getClass(name); 383 } 384 385 /** 386 * Tests whether the specified property of the given bean instance supports write access. 387 * 388 * @param bean the bean instance 389 * @param propName the name of the property in question 390 * @return <b>true</b> if this property can be written, <b>false</b> otherwise 391 */ 392 private static boolean isPropertyWriteable(final Object bean, final String propName) { 393 return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName); 394 } 395 396 /** 397 * Determines the class of the bean to be created. If the bean declaration contains a class name, this class is used. 398 * Otherwise it is checked whether a default class is provided. If this is not the case, the factory's default class is 399 * used. If this class is undefined, too, an exception is thrown. 400 * 401 * @param data the bean declaration 402 * @param defaultClass the default class 403 * @param factory the bean factory to use 404 * @return the class of the bean to be created 405 * @throws ConfigurationRuntimeException if the class cannot be determined 406 */ 407 private static Class<?> fetchBeanClass(final BeanDeclaration data, final Class<?> defaultClass, final BeanFactory factory) { 408 final String clsName = data.getBeanClassName(); 409 if (clsName != null) { 410 try { 411 return loadClass(clsName); 412 } catch (final ClassNotFoundException cex) { 413 throw new ConfigurationRuntimeException(cex); 414 } 415 } 416 417 if (defaultClass != null) { 418 return defaultClass; 419 } 420 421 final Class<?> clazz = factory.getDefaultBeanClass(); 422 if (clazz == null) { 423 throw new ConfigurationRuntimeException("Bean class is not specified!"); 424 } 425 return clazz; 426 } 427 428 /** 429 * Obtains the bean factory to use for creating the specified bean. This method will check whether a factory is 430 * specified in the bean declaration. If this is not the case, the default bean factory will be used. 431 * 432 * @param data the bean declaration 433 * @return the bean factory to use 434 * @throws ConfigurationRuntimeException if the factory cannot be determined 435 */ 436 private BeanFactory fetchBeanFactory(final BeanDeclaration data) { 437 final String factoryName = data.getBeanFactoryName(); 438 if (factoryName != null) { 439 final BeanFactory factory = beanFactories.get(factoryName); 440 if (factory == null) { 441 throw new ConfigurationRuntimeException("Unknown bean factory: " + factoryName); 442 } 443 return factory; 444 } 445 return getDefaultBeanFactory(); 446 } 447 448 /** 449 * Creates a {@code BeanCreationContext} object for the creation of the specified bean. 450 * 451 * @param data the bean declaration 452 * @param defaultClass the default class to use 453 * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters 454 * and behave different depending on the value passed in here 455 * @param factory the current bean factory 456 * @return the {@code BeanCreationContext} 457 * @throws ConfigurationRuntimeException if the bean class cannot be determined 458 */ 459 private BeanCreationContext createBeanCreationContext(final BeanDeclaration data, final Class<?> defaultClass, final Object param, 460 final BeanFactory factory) { 461 final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory); 462 return new BeanCreationContextImpl(this, beanClass, data, param); 463 } 464 465 /** 466 * Initializes the shared {@code BeanUtilsBean} instance. This method sets up custom bean introspection in a way that 467 * fluent parameter interfaces are supported. 468 * 469 * @return the {@code BeanUtilsBean} instance to be used for all property set operations 470 */ 471 private static BeanUtilsBean initBeanUtilsBean() { 472 final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean(); 473 propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector()); 474 return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean); 475 } 476 477 /** 478 * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a 479 * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for 480 * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance 481 * (which is the instance that created this object). 482 */ 483 private static final class BeanCreationContextImpl implements BeanCreationContext { 484 /** The association BeanHelper instance. */ 485 private final BeanHelper beanHelper; 486 487 /** The class of the bean to be created. */ 488 private final Class<?> beanClass; 489 490 /** The underlying bean declaration. */ 491 private final BeanDeclaration data; 492 493 /** The parameter for the bean factory. */ 494 private final Object param; 495 496 private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) { 497 beanHelper = helper; 498 this.beanClass = beanClass; 499 this.param = param; 500 this.data = data; 501 } 502 503 @Override 504 public void initBean(final Object bean, final BeanDeclaration data) { 505 beanHelper.initBean(bean, data); 506 } 507 508 @Override 509 public Object getParameter() { 510 return param; 511 } 512 513 @Override 514 public BeanDeclaration getBeanDeclaration() { 515 return data; 516 } 517 518 @Override 519 public Class<?> getBeanClass() { 520 return beanClass; 521 } 522 523 @Override 524 public Object createBean(final BeanDeclaration data) { 525 return beanHelper.createBean(data); 526 } 527 } 528 }