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