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 *     http://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     * A default instance of {@code BeanHelper} which can be shared between arbitrary components. If no special
068     * configuration is needed, this instance can be used throughout an application. Otherwise, new instances can be created
069     * with their own configuration.
070     */
071    public static final BeanHelper INSTANCE = new BeanHelper();
072
073    /**
074     * A special instance of {@code BeanUtilsBean} which is used for all property set and copy operations. This instance was
075     * initialized with {@code BeanIntrospector} objects which support fluent interfaces. This is required for handling
076     * builder parameter objects correctly.
077     */
078    private static final BeanUtilsBean BEAN_UTILS_BEAN = initBeanUtilsBean();
079
080    /** Stores a map with the registered bean factories. */
081    private final Map<String, BeanFactory> beanFactories = Collections.synchronizedMap(new HashMap<>());
082
083    /**
084     * Stores the default bean factory, which is used if no other factory is provided in a bean declaration.
085     */
086    private final BeanFactory defaultBeanFactory;
087
088    /**
089     * Constructs a new instance of {@code BeanHelper} with the default instance of {@link DefaultBeanFactory} as default
090     * {@link BeanFactory}.
091     */
092    public BeanHelper() {
093        this(null);
094    }
095
096    /**
097     * Constructs a new instance of {@code BeanHelper} and sets the specified default {@code BeanFactory}.
098     *
099     * @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                        for (final BeanDeclaration decl : decls) {
179                            coll.add(createBean(decl, defaultClass));
180                        }
181                    } else {
182                        final BeanDeclaration decl = (BeanDeclaration) e.getValue();
183                        coll.add(createBean(decl, defaultClass));
184                    }
185                }
186            } else {
187                for (final Map.Entry<String, Object> e : nestedBeans.entrySet()) {
188                    final String propName = e.getKey();
189                    final Class<?> defaultClass = getDefaultClass(bean, propName);
190
191                    final Object prop = e.getValue();
192
193                    if (prop instanceof Collection) {
194                        final Collection<Object> beanCollection = createPropertyCollection(propName, defaultClass);
195
196                        for (final Object elemDef : (Collection<?>) prop) {
197                            beanCollection.add(createBean((BeanDeclaration) elemDef));
198                        }
199
200                        initProperty(bean, propName, beanCollection);
201                    } else {
202                        initProperty(bean, propName, createBean((BeanDeclaration) e.getValue(), defaultClass));
203                    }
204                }
205            }
206        }
207    }
208
209    /**
210     * Initializes the beans properties.
211     *
212     * @param bean the bean to be initialized
213     * @param data the bean declaration
214     * @throws ConfigurationRuntimeException if a property cannot be set
215     */
216    public static void initBeanProperties(final Object bean, final BeanDeclaration data) {
217        final Map<String, Object> properties = data.getBeanProperties();
218        if (properties != null) {
219            for (final Map.Entry<String, Object> e : properties.entrySet()) {
220                final String propName = e.getKey();
221                initProperty(bean, propName, e.getValue());
222            }
223        }
224    }
225
226    /**
227     * Creates a {@code DynaBean} instance which wraps the passed in bean.
228     *
229     * @param bean the bean to be wrapped (must not be <b>null</b>)
230     * @return a {@code DynaBean} wrapping the passed in bean
231     * @throws IllegalArgumentException if the bean is <b>null</b>
232     * @since 2.0
233     */
234    public static DynaBean createWrapDynaBean(final Object bean) {
235        if (bean == null) {
236            throw new IllegalArgumentException("Bean must not be null!");
237        }
238        final WrapDynaClass dynaClass = WrapDynaClass.createDynaClass(bean.getClass(), BEAN_UTILS_BEAN.getPropertyUtils());
239        return new WrapDynaBean(bean, dynaClass);
240    }
241
242    /**
243     * Copies matching properties from the source bean to the destination bean using a specially configured
244     * {@code PropertyUtilsBean} instance. This method ensures that enhanced introspection is enabled when doing the copy
245     * operation.
246     *
247     * @param dest the destination bean
248     * @param orig the source bean
249     * @throws NoSuchMethodException exception thrown by {@code PropertyUtilsBean}
250     * @throws InvocationTargetException exception thrown by {@code PropertyUtilsBean}
251     * @throws IllegalAccessException exception thrown by {@code PropertyUtilsBean}
252     * @since 2.0
253     */
254    public static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
255        BEAN_UTILS_BEAN.getPropertyUtils().copyProperties(dest, orig);
256    }
257
258    /**
259     * Gets the Class of the property if it can be determined.
260     *
261     * @param bean The bean containing the property.
262     * @param propName The name of the property.
263     * @return The class associated with the property or null.
264     */
265    private static Class<?> getDefaultClass(final Object bean, final String propName) {
266        try {
267            final PropertyDescriptor desc = BEAN_UTILS_BEAN.getPropertyUtils().getPropertyDescriptor(bean, propName);
268            if (desc == null) {
269                return null;
270            }
271            return desc.getPropertyType();
272        } catch (final Exception ex) {
273            return null;
274        }
275    }
276
277    /**
278     * Sets a property on the given bean using Common Beanutils.
279     *
280     * @param bean the bean
281     * @param propName the name of the property
282     * @param value the property's value
283     * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
284     */
285    private static void initProperty(final Object bean, final String propName, final Object value) {
286        if (!isPropertyWriteable(bean, propName)) {
287            throw new ConfigurationRuntimeException("Property " + propName + " cannot be set on " + bean.getClass().getName());
288        }
289
290        try {
291            BEAN_UTILS_BEAN.setProperty(bean, propName, value);
292        } catch (final IllegalAccessException | InvocationTargetException itex) {
293            throw new ConfigurationRuntimeException(itex);
294        }
295    }
296
297    /**
298     * Creates a concrete collection instance to populate a property of type collection. This method tries to guess an
299     * appropriate collection type. Mostly the type of the property will be one of the collection interfaces rather than a
300     * concrete class; so we have to create a concrete equivalent.
301     *
302     * @param propName the name of the collection property
303     * @param propertyClass the type of the property
304     * @return the newly created collection
305     */
306    private static Collection<Object> createPropertyCollection(final String propName, final Class<?> propertyClass) {
307        final Collection<Object> beanCollection;
308
309        if (List.class.isAssignableFrom(propertyClass)) {
310            beanCollection = new ArrayList<>();
311        } else if (Set.class.isAssignableFrom(propertyClass)) {
312            beanCollection = new TreeSet<>();
313        } else {
314            throw new UnsupportedOperationException("Unable to handle collection of type : " + propertyClass.getName() + " for property " + propName);
315        }
316        return beanCollection;
317    }
318
319    /**
320     * Sets a property on the bean only if the property exists
321     *
322     * @param bean the bean
323     * @param propName the name of the property
324     * @param value the property's value
325     * @throws ConfigurationRuntimeException if the property is not writable or an error occurred
326     */
327    public static void setProperty(final Object bean, final String propName, final Object value) {
328        if (isPropertyWriteable(bean, propName)) {
329            initProperty(bean, propName, value);
330        }
331    }
332
333    /**
334     * The main method for creating and initializing beans from a configuration. This method will return an initialized
335     * instance of the bean class specified in the passed in bean declaration. If this declaration does not contain the
336     * class of the bean, the passed in default class will be used. From the bean declaration the factory to be used for
337     * creating the bean is queried. The declaration may here return <b>null</b>, then a default factory is used. This
338     * factory is then invoked to perform the create operation.
339     *
340     * @param data the bean declaration
341     * @param defaultClass the default class to use
342     * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
343     *        and behave different depending on the value passed in here
344     * @return the new bean
345     * @throws ConfigurationRuntimeException if an error occurs
346     */
347    public Object createBean(final BeanDeclaration data, final Class<?> defaultClass, final Object param) {
348        if (data == null) {
349            throw new IllegalArgumentException("Bean declaration must not be null!");
350        }
351
352        final BeanFactory factory = fetchBeanFactory(data);
353        final BeanCreationContext bcc = createBeanCreationContext(data, defaultClass, param, factory);
354        try {
355            return factory.createBean(bcc);
356        } catch (final Exception ex) {
357            throw new ConfigurationRuntimeException(ex);
358        }
359    }
360
361    /**
362     * Creates a bean instance for the specified declaration. This method is a short cut for
363     * {@code createBean(data, null, null);}.
364     *
365     * @param data the bean declaration
366     * @param defaultClass the class to be used when in the declaration no class is specified
367     * @return the new bean
368     * @throws ConfigurationRuntimeException if an error occurs
369     */
370    public Object createBean(final BeanDeclaration data, final Class<?> defaultClass) {
371        return createBean(data, defaultClass, null);
372    }
373
374    /**
375     * Creates a bean instance for the specified declaration. This method is a short cut for
376     * {@code createBean(data, null);}.
377     *
378     * @param data the bean declaration
379     * @return the new bean
380     * @throws ConfigurationRuntimeException if an error occurs
381     */
382    public Object createBean(final BeanDeclaration data) {
383        return createBean(data, null);
384    }
385
386    /**
387     * Loads a {@code java.lang.Class} object for the specified name. Because class loading can be tricky in some
388     * environments the code for retrieving a class by its name was extracted into this helper method. So if changes are
389     * necessary, they can be made at a single place.
390     *
391     * @param name the name of the class to be loaded
392     * @return the class object for the specified name
393     * @throws ClassNotFoundException if the class cannot be loaded
394     */
395    static Class<?> loadClass(final String name) throws ClassNotFoundException {
396        return ClassUtils.getClass(name);
397    }
398
399    /**
400     * Tests whether the specified property of the given bean instance supports write access.
401     *
402     * @param bean the bean instance
403     * @param propName the name of the property in question
404     * @return <b>true</b> if this property can be written, <b>false</b> otherwise
405     */
406    private static boolean isPropertyWriteable(final Object bean, final String propName) {
407        return BEAN_UTILS_BEAN.getPropertyUtils().isWriteable(bean, propName);
408    }
409
410    /**
411     * Determines the class of the bean to be created. If the bean declaration contains a class name, this class is used.
412     * Otherwise it is checked whether a default class is provided. If this is not the case, the factory's default class is
413     * used. If this class is undefined, too, an exception is thrown.
414     *
415     * @param data the bean declaration
416     * @param defaultClass the default class
417     * @param factory the bean factory to use
418     * @return the class of the bean to be created
419     * @throws ConfigurationRuntimeException if the class cannot be determined
420     */
421    private static Class<?> fetchBeanClass(final BeanDeclaration data, final Class<?> defaultClass, final BeanFactory factory) {
422        final String clsName = data.getBeanClassName();
423        if (clsName != null) {
424            try {
425                return loadClass(clsName);
426            } catch (final ClassNotFoundException cex) {
427                throw new ConfigurationRuntimeException(cex);
428            }
429        }
430
431        if (defaultClass != null) {
432            return defaultClass;
433        }
434
435        final Class<?> clazz = factory.getDefaultBeanClass();
436        if (clazz == null) {
437            throw new ConfigurationRuntimeException("Bean class is not specified!");
438        }
439        return clazz;
440    }
441
442    /**
443     * Obtains the bean factory to use for creating the specified bean. This method will check whether a factory is
444     * specified in the bean declaration. If this is not the case, the default bean factory will be used.
445     *
446     * @param data the bean declaration
447     * @return the bean factory to use
448     * @throws ConfigurationRuntimeException if the factory cannot be determined
449     */
450    private BeanFactory fetchBeanFactory(final BeanDeclaration data) {
451        final String factoryName = data.getBeanFactoryName();
452        if (factoryName != null) {
453            final BeanFactory factory = beanFactories.get(factoryName);
454            if (factory == null) {
455                throw new ConfigurationRuntimeException("Unknown bean factory: " + factoryName);
456            }
457            return factory;
458        }
459        return getDefaultBeanFactory();
460    }
461
462    /**
463     * Creates a {@code BeanCreationContext} object for the creation of the specified bean.
464     *
465     * @param data the bean declaration
466     * @param defaultClass the default class to use
467     * @param param an additional parameter that will be passed to the bean factory; some factories may support parameters
468     *        and behave different depending on the value passed in here
469     * @param factory the current bean factory
470     * @return the {@code BeanCreationContext}
471     * @throws ConfigurationRuntimeException if the bean class cannot be determined
472     */
473    private BeanCreationContext createBeanCreationContext(final BeanDeclaration data, final Class<?> defaultClass, final Object param,
474        final BeanFactory factory) {
475        final Class<?> beanClass = fetchBeanClass(data, defaultClass, factory);
476        return new BeanCreationContextImpl(this, beanClass, data, param);
477    }
478
479    /**
480     * Initializes the shared {@code BeanUtilsBean} instance. This method sets up custom bean introspection in a way that
481     * fluent parameter interfaces are supported.
482     *
483     * @return the {@code BeanUtilsBean} instance to be used for all property set operations
484     */
485    private static BeanUtilsBean initBeanUtilsBean() {
486        final PropertyUtilsBean propUtilsBean = new PropertyUtilsBean();
487        propUtilsBean.addBeanIntrospector(new FluentPropertyBeanIntrospector());
488        return new BeanUtilsBean(new ConvertUtilsBean(), propUtilsBean);
489    }
490
491    /**
492     * An implementation of the {@code BeanCreationContext} interface used by {@code BeanHelper} to communicate with a
493     * {@code BeanFactory}. This class contains all information required for the creation of a bean. The methods for
494     * creating and initializing bean instances are implemented by calling back to the provided {@code BeanHelper} instance
495     * (which is the instance that created this object).
496     */
497    private static final class BeanCreationContextImpl implements BeanCreationContext {
498        /** The association BeanHelper instance. */
499        private final BeanHelper beanHelper;
500
501        /** The class of the bean to be created. */
502        private final Class<?> beanClass;
503
504        /** The underlying bean declaration. */
505        private final BeanDeclaration data;
506
507        /** The parameter for the bean factory. */
508        private final Object param;
509
510        private BeanCreationContextImpl(final BeanHelper helper, final Class<?> beanClass, final BeanDeclaration data, final Object param) {
511            beanHelper = helper;
512            this.beanClass = beanClass;
513            this.param = param;
514            this.data = data;
515        }
516
517        @Override
518        public void initBean(final Object bean, final BeanDeclaration data) {
519            beanHelper.initBean(bean, data);
520        }
521
522        @Override
523        public Object getParameter() {
524            return param;
525        }
526
527        @Override
528        public BeanDeclaration getBeanDeclaration() {
529            return data;
530        }
531
532        @Override
533        public Class<?> getBeanClass() {
534            return beanClass;
535        }
536
537        @Override
538        public Object createBean(final BeanDeclaration data) {
539            return beanHelper.createBean(data);
540        }
541    }
542}