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 * https://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 * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a
68 * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for
69 * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance
70 * (which is the instance that created this object).
71 */
72 private static final class BeanCreationContextImpl implements BeanCreationContext {
73
74 /** The association BeanHelper instance. */
75 private final BeanHelper beanHelper;
76
77 /** The class of the bean to be created. */
78 private final Class<?> beanClass;
79
80 /** The underlying bean declaration. */
81 private final BeanDeclaration data;
82
83 /** The parameter for the bean factory. */
84 private final Object param;
85
86 private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) {
87 beanHelper = helper;
88 this.beanClass = beanClass;
89 this.param = param;
90 this.data = data;
91 }
92
93 @Override
94 public Object createBean(final BeanDeclaration data) {
95 return beanHelper.createBean(data);
96 }
97
98 @Override
99 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 }