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}