    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.builder.combined;
    018
    019import java.lang.reflect.Constructor;
    020import java.util.ArrayList;
    021import java.util.Collection;
    022import java.util.Collections;
    023import java.util.Map;
    024
    025import org.apache.commons.configuration2.Configuration;
    026import org.apache.commons.configuration2.ConfigurationUtils;
    027import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
    028import org.apache.commons.configuration2.builder.BuilderParameters;
    029import org.apache.commons.configuration2.builder.ConfigurationBuilder;
    030import org.apache.commons.configuration2.ex.ConfigurationException;
    031
    032/**
    033 * <p>
    034 * A fully-functional, reflection-based implementation of the {@code ConfigurationBuilderProvider} interface which can
    035 * deal with the default tags defining configuration sources.
    036 * </p>
    037 * <p>
    038 * An instance of this class is initialized with the names of the {@code ConfigurationBuilder} class used by this
    039 * provider and the concrete {@code Configuration} class. The {@code ConfigurationBuilder} class must be derived from
    040 * {@link BasicConfigurationBuilder}. When asked for the builder object, an instance of the builder class is created and
    041 * initialized from the bean declaration associated with the current configuration source.
    042 * </p>
    043 * <p>
    044 * {@code ConfigurationBuilder} objects are configured using parameter objects. When declaring configuration sources in
    045 * XML it should not be necessary to define the single parameter objects. Rather, simple and complex properties are set
    046 * in the typical way of a bean declaration (i.e. as attributes of the current XML element or as child elements). This
    047 * class creates all supported parameter objects (whose names also must be provided at construction time) and takes care
    048 * that their properties are initialized according to the current bean declaration.
    049 * </p>
    050 * <p>
    051 * The use of reflection to create builder instances allows a generic implementation supporting many concrete builder
    052 * classes. Another reason for this approach is that builder classes are only loaded if actually needed. Some
    053 * specialized {@code Configuration} implementations require specific external dependencies which should not be
    054 * mandatory for the use of {@code CombinedConfigurationBuilder}. Because such classes are lazily loaded, an application
    055 * only has to include the dependencies it actually uses.
    056 * </p>
    057 *
    058 * @since 2.0
    059 */
    060public class BaseConfigurationBuilderProvider implements ConfigurationBuilderProvider {
    061    /** The types of the constructor parameters for a basic builder. */
    062    private static final Class<?>[] CTOR_PARAM_TYPES = {Class.class, Map.class, Boolean.TYPE};
    063
    064    /** The name of the builder class. */
    065    private final String builderClass;
    066
    067    /** The name of a builder class with reloading support. */
    068    private final String reloadingBuilderClass;
    069
    070    /** Stores the name of the configuration class to be created. */
    071    private final String configurationClass;
    072
    073    /** A collection with the names of parameter classes. */
    074    private final Collection<String> parameterClasses;
    075
    076    /**
    077     * Creates a new instance of {@code BaseConfigurationBuilderProvider} and initializes all its properties.
    078     *
    079     * @param bldrCls the name of the builder class (must not be <b>null</b>)
    080     * @param reloadBldrCls the name of a builder class to be used if reloading support is required (<b>null</b> if
    081     *        reloading is not supported)
    082     * @param configCls the name of the configuration class (must not be <b>null</b>)
    083     * @param paramCls a collection with the names of parameters classes
    084     * @throws IllegalArgumentException if a required parameter is missing
    085     */
    086    public BaseConfigurationBuilderProvider(final String bldrCls, final String reloadBldrCls, final String configCls, final Collection<String> paramCls) {
    087        if (bldrCls == null) {
    088            throw new IllegalArgumentException("Builder class must not be null!");
    089        }
    090        if (configCls == null) {
    091            throw new IllegalArgumentException("Configuration class must not be null!");
    092        }
    093
    094        builderClass = bldrCls;
    095        reloadingBuilderClass = reloadBldrCls;
    096        configurationClass = configCls;
    097        parameterClasses = initParameterClasses(paramCls);
    098    }
    099
    100    /**
    101     * Returns the name of the class of the builder created by this provider.
    102     *
    103     * @return the builder class
    104     */
    105    public String getBuilderClass() {
    106        return builderClass;
    107    }
    108
    109    /**
    110     * Returns the name of the class of the builder created by this provider if the reload flag is set. If this method
    111     * returns <b>null</b>, reloading builders are not supported by this provider.
    112     *
    113     * @return the reloading builder class
    114     */
    115    public String getReloadingBuilderClass() {
    116        return reloadingBuilderClass;
    117    }
    118
    119    /**
    120     * Returns the name of the configuration class created by the builder produced by this provider.
    121     *
    122     * @return the configuration class
    123     */
    124    public String getConfigurationClass() {
    125        return configurationClass;
    126    }
    127
    128    /**
    129     * Returns an unmodifiable collection with the names of parameter classes supported by this provider.
    130     *
    131     * @return the parameter classes
    132     */
    133    public Collection<String> getParameterClasses() {
    134        return parameterClasses;
    135    }
    136
    137    /**
    138     * {@inheritDoc} This implementation delegates to some protected methods to create a new builder instance using
    139     * reflection and to configure it with parameter values defined by the passed in {@code BeanDeclaration}.
    140     */
    141    @Override
    142    public ConfigurationBuilder<? extends Configuration> getConfigurationBuilder(final ConfigurationDeclaration decl) throws ConfigurationException {
    143        try {
    144            final Collection<BuilderParameters> params = createParameterObjects();
    145            initializeParameterObjects(decl, params);
    146            final BasicConfigurationBuilder<? extends Configuration> builder = createBuilder(decl, params);
    147            configureBuilder(builder, decl, params);
    148            return builder;
    149        } catch (final ConfigurationException cex) {
    150            throw cex;
    151        } catch (final Exception ex) {
    152            throw new ConfigurationException(ex);
    153        }
    154    }
    155
    156    /**
    157     * Determines the <em>allowFailOnInit</em> flag for the newly created builder based on the given
    158     * {@code ConfigurationDeclaration}. Some combinations of flags in the declaration say that a configuration source is
    159     * optional, but an empty instance should be created if its creation fail.
    160     *
    161     * @param decl the current {@code ConfigurationDeclaration}
    162     * @return the value of the <em>allowFailOnInit</em> flag
    163     */
    164    protected boolean isAllowFailOnInit(final ConfigurationDeclaration decl) {
    165        return decl.isOptional() && decl.isForceCreate();
    166    }
    167
    168    /**
    169     * Creates a collection of parameter objects to be used for configuring the builder. This method creates instances of
    170     * the parameter classes passed to the constructor.
    171     *
    172     * @return a collection with parameter objects for the builder
    173     * @throws Exception if an error occurs while creating parameter objects via reflection
    174     */
    175    protected Collection<BuilderParameters> createParameterObjects() throws Exception {
    176        final Collection<BuilderParameters> params = new ArrayList<>(getParameterClasses().size());
    177        for (final String paramcls : getParameterClasses()) {
    178            params.add(createParameterObject(paramcls));
    179        }
    180        return params;
    181    }
    182
    183    /**
    184     * Initializes the parameter objects with data stored in the current bean declaration. This method is called before the
    185     * newly created builder instance is configured with the parameter objects. It maps attributes of the bean declaration
    186     * to properties of parameter objects. In addition, it invokes the parent {@code CombinedConfigurationBuilder} so that
    187     * the parameters object can inherit properties already defined for this builder.
    188     *
    189     * @param decl the current {@code ConfigurationDeclaration}
    190     * @param params the collection with (uninitialized) parameter objects
    191     * @throws Exception if an error occurs
    192     */
    193    protected void initializeParameterObjects(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params) throws Exception {
    194        inheritParentBuilderProperties(decl, params);
    195        final MultiWrapDynaBean wrapBean = new MultiWrapDynaBean(params);
    196        decl.getConfigurationBuilder().initBean(wrapBean, decl);
    197    }
    198
    199    /**
    200     * Passes all parameter objects to the parent {@code CombinedConfigurationBuilder} so that properties already defined
    201     * for the parent builder can be added. This method is called before the parameter objects are initialized from the
    202     * definition configuration. This way properties from the parent builder are inherited, but can be overridden for child
    203     * configurations.
    204     *
    205     * @param decl the current {@code ConfigurationDeclaration}
    206     * @param params the collection with (uninitialized) parameter objects
    207     */
    208    protected void inheritParentBuilderProperties(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params) {
    209        for (final BuilderParameters p : params) {
    210            decl.getConfigurationBuilder().initChildBuilderParameters(p);
    211        }
    212    }
    213
    214    /**
    215     * Creates a new, uninitialized instance of the builder class managed by this provider. This implementation determines
    216     * the builder class to be used by delegating to {@code determineBuilderClass()}. It then calls the constructor
    217     * expecting the configuration class, the map with properties, and the<em>allowFailOnInit</em> flag.
    218     *
    219     * @param decl the current {@code ConfigurationDeclaration}
    220     * @param params initialization parameters for the new builder object
    221     * @return the newly created builder instance
    222     * @throws Exception if an error occurs
    223     */
    224    protected BasicConfigurationBuilder<? extends Configuration> createBuilder(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params)
    225        throws Exception {
    226        final Class<?> bldCls = ConfigurationUtils.loadClass(determineBuilderClass(decl));
    227        final Class<?> configCls = ConfigurationUtils.loadClass(determineConfigurationClass(decl, params));
    228        final Constructor<?> ctor = bldCls.getConstructor(CTOR_PARAM_TYPES);
    229        // ? extends Configuration is the minimum constraint
    230        @SuppressWarnings("unchecked")
    231        final BasicConfigurationBuilder<? extends Configuration> builder = (BasicConfigurationBuilder<? extends Configuration>) ctor.newInstance(configCls,
    232            null, isAllowFailOnInit(decl));
    233        return builder;
    234    }
    235
    236    /**
    237     * Configures a newly created builder instance with its initialization parameters. This method is called after a new
    238     * instance was created using reflection. This implementation passes the parameter objects to the builder's
    239     * {@code configure()} method.
    240     *
    241     * @param builder the builder to be initialized
    242     * @param decl the current {@code ConfigurationDeclaration}
    243     * @param params the collection with initialization parameter objects
    244     * @throws Exception if an error occurs
    245     */
    246    protected void configureBuilder(final BasicConfigurationBuilder<? extends Configuration> builder, final ConfigurationDeclaration decl,
    247        final Collection<BuilderParameters> params) throws Exception {
    248        builder.configure(params.toArray(new BuilderParameters[params.size()]));
    249    }
    250
    251    /**
    252     * Determines the name of the class to be used for a new builder instance. This implementation selects between the
    253     * normal and the reloading builder class, based on the passed in {@code ConfigurationDeclaration}. If a reloading
    254     * builder is desired, but this provider has no reloading support, an exception is thrown.
    255     *
    256     * @param decl the current {@code ConfigurationDeclaration}
    257     * @return the name of the builder class
    258     * @throws ConfigurationException if the builder class cannot be determined
    259     */
    260    protected String determineBuilderClass(final ConfigurationDeclaration decl) throws ConfigurationException {
    261        if (decl.isReload()) {
    262            if (getReloadingBuilderClass() == null) {
    263                throw new ConfigurationException("No support for reloading for builder class " + getBuilderClass());
    264            }
    265            return getReloadingBuilderClass();
    266        }
    267        return getBuilderClass();
    268    }
    269
    270    /**
    271     * Determines the name of the configuration class produced by the builder. This method is called when obtaining the
    272     * arguments for invoking the constructor of the builder class. This implementation just returns the pre-configured
    273     * configuration class name. Derived classes may determine this class name dynamically based on the passed in
    274     * parameters.
    275     *
    276     * @param decl the current {@code ConfigurationDeclaration}
    277     * @param params the collection with parameter objects
    278     * @return the name of the builder's result configuration class
    279     * @throws ConfigurationException if an error occurs
    280     */
    281    protected String determineConfigurationClass(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params)
    282        throws ConfigurationException {
    283        return getConfigurationClass();
    284    }
    285
    286    /**
    287     * Creates an instance of a parameter class using reflection.
    288     *
    289     * @param paramcls the parameter class
    290     * @return the newly created instance
    291     * @throws Exception if an error occurs
    292     */
    293    private static BuilderParameters createParameterObject(final String paramcls) throws Exception {
    294        final Class<?> cls = ConfigurationUtils.loadClass(paramcls);
    295        return (BuilderParameters) cls.newInstance();
    296    }
    297
    298    /**
    299     * Creates a new, unmodifiable collection for the parameter classes.
    300     *
    301     * @param paramCls the collection with parameter classes passed to the constructor
    302     * @return the collection to be stored
    303     */
    304    private static Collection<String> initParameterClasses(final Collection<String> paramCls) {
    305        if (paramCls == null) {
    306            return Collections.emptySet();
    307        }
    308        return Collections.unmodifiableCollection(new ArrayList<>(paramCls));
    309    }
    310}