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.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
062    /** The types of the constructor parameters for a basic builder. */
063    private static final Class<?>[] CTOR_PARAM_TYPES = {Class.class, Map.class, Boolean.TYPE};
064
065    /**
066     * Creates an instance of a parameter class using reflection.
067     *
068     * @param paramcls the parameter class
069     * @return the newly created instance
070     * @throws Exception if an error occurs
071     */
072    private static BuilderParameters createParameterObject(final String paramcls) throws ReflectiveOperationException {
073        return (BuilderParameters) ConfigurationUtils.loadClass(paramcls).getConstructor().newInstance();
074    }
075
076    /**
077     * Creates a new, unmodifiable collection for the parameter classes.
078     *
079     * @param paramCls the collection with parameter classes passed to the constructor
080     * @return the collection to be stored
081     */
082    private static Collection<String> initParameterClasses(final Collection<String> paramCls) {
083        if (paramCls == null) {
084            return Collections.emptySet();
085        }
086        return Collections.unmodifiableCollection(new ArrayList<>(paramCls));
087    }
088
089    /** The name of the builder class. */
090    private final String builderClass;
091
092    /** The name of a builder class with reloading support. */
093    private final String reloadingBuilderClass;
094
095    /** Stores the name of the configuration class to be created. */
096    private final String configurationClass;
097
098    /** A collection with the names of parameter classes. */
099    private final Collection<String> parameterClasses;
100
101    /**
102     * Creates a new instance of {@code BaseConfigurationBuilderProvider} and initializes all its properties.
103     *
104     * @param bldrCls the name of the builder class (must not be <strong>null</strong>)
105     * @param reloadBldrCls the name of a builder class to be used if reloading support is required (<strong>null</strong> if
106     *        reloading is not supported)
107     * @param configCls the name of the configuration class (must not be <strong>null</strong>)
108     * @param paramCls a collection with the names of parameters classes
109     * @throws IllegalArgumentException if a required parameter is missing
110     */
111    public BaseConfigurationBuilderProvider(final String bldrCls, final String reloadBldrCls, final String configCls, final Collection<String> paramCls) {
112        if (bldrCls == null) {
113            throw new IllegalArgumentException("Builder class must not be null!");
114        }
115        if (configCls == null) {
116            throw new IllegalArgumentException("Configuration class must not be null!");
117        }
118
119        builderClass = bldrCls;
120        reloadingBuilderClass = reloadBldrCls;
121        configurationClass = configCls;
122        parameterClasses = initParameterClasses(paramCls);
123    }
124
125    /**
126     * Configures a newly created builder instance with its initialization parameters. This method is called after a new
127     * instance was created using reflection. This implementation passes the parameter objects to the builder's
128     * {@code configure()} method.
129     *
130     * @param builder the builder to be initialized
131     * @param decl the current {@code ConfigurationDeclaration}
132     * @param params the collection with initialization parameter objects
133     * @throws Exception if an error occurs
134     */
135    protected void configureBuilder(final BasicConfigurationBuilder<? extends Configuration> builder, final ConfigurationDeclaration decl,
136        final Collection<BuilderParameters> params) throws Exception {
137        builder.configure(params.toArray(new BuilderParameters[params.size()]));
138    }
139
140    /**
141     * Creates a new, uninitialized instance of the builder class managed by this provider. This implementation determines
142     * the builder class to be used by delegating to {@code determineBuilderClass()}. It then calls the constructor
143     * expecting the configuration class, the map with properties, and the<em>allowFailOnInit</em> flag.
144     *
145     * @param decl the current {@code ConfigurationDeclaration}
146     * @param params initialization parameters for the new builder object
147     * @return the newly created builder instance
148     * @throws Exception if an error occurs
149     */
150    protected BasicConfigurationBuilder<? extends Configuration> createBuilder(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params)
151        throws Exception {
152        final Class<?> bldCls = ConfigurationUtils.loadClass(determineBuilderClass(decl));
153        final Class<?> configCls = ConfigurationUtils.loadClass(determineConfigurationClass(decl, params));
154        final Constructor<?> ctor = bldCls.getConstructor(CTOR_PARAM_TYPES);
155        // ? extends Configuration is the minimum constraint
156        @SuppressWarnings("unchecked")
157        final BasicConfigurationBuilder<? extends Configuration> builder = (BasicConfigurationBuilder<? extends Configuration>) ctor.newInstance(configCls,
158            null, isAllowFailOnInit(decl));
159        return builder;
160    }
161
162    /**
163     * Creates a collection of parameter objects to be used for configuring the builder. This method creates instances of
164     * the parameter classes passed to the constructor.
165     *
166     * @return a collection with parameter objects for the builder
167     * @throws Exception if an error occurs while creating parameter objects via reflection
168     */
169    protected Collection<BuilderParameters> createParameterObjects() throws Exception {
170        final Collection<BuilderParameters> params = new ArrayList<>(getParameterClasses().size());
171        for (final String paramcls : getParameterClasses()) {
172            params.add(createParameterObject(paramcls));
173        }
174        return params;
175    }
176
177    /**
178     * Determines the name of the class to be used for a new builder instance. This implementation selects between the
179     * normal and the reloading builder class, based on the passed in {@code ConfigurationDeclaration}. If a reloading
180     * builder is desired, but this provider has no reloading support, an exception is thrown.
181     *
182     * @param decl the current {@code ConfigurationDeclaration}
183     * @return the name of the builder class
184     * @throws ConfigurationException if the builder class cannot be determined
185     */
186    protected String determineBuilderClass(final ConfigurationDeclaration decl) throws ConfigurationException {
187        if (decl.isReload()) {
188            if (getReloadingBuilderClass() == null) {
189                throw new ConfigurationException("No support for reloading for builder class %s", getBuilderClass());
190            }
191            return getReloadingBuilderClass();
192        }
193        return getBuilderClass();
194    }
195
196    /**
197     * Determines the name of the configuration class produced by the builder. This method is called when obtaining the
198     * arguments for invoking the constructor of the builder class. This implementation just returns the pre-configured
199     * configuration class name. Derived classes may determine this class name dynamically based on the passed in
200     * parameters.
201     *
202     * @param decl the current {@code ConfigurationDeclaration}
203     * @param params the collection with parameter objects
204     * @return the name of the builder's result configuration class
205     * @throws ConfigurationException if an error occurs
206     */
207    protected String determineConfigurationClass(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params)
208        throws ConfigurationException {
209        return getConfigurationClass();
210    }
211
212    /**
213     * Gets the name of the class of the builder created by this provider.
214     *
215     * @return the builder class
216     */
217    public String getBuilderClass() {
218        return builderClass;
219    }
220
221    /**
222     * {@inheritDoc} This implementation delegates to some protected methods to create a new builder instance using
223     * reflection and to configure it with parameter values defined by the passed in {@code BeanDeclaration}.
224     */
225    @Override
226    public ConfigurationBuilder<? extends Configuration> getConfigurationBuilder(final ConfigurationDeclaration decl) throws ConfigurationException {
227        try {
228            final Collection<BuilderParameters> params = createParameterObjects();
229            initializeParameterObjects(decl, params);
230            final BasicConfigurationBuilder<? extends Configuration> builder = createBuilder(decl, params);
231            configureBuilder(builder, decl, params);
232            return builder;
233        } catch (final ConfigurationException cex) {
234            throw cex;
235        } catch (final Exception ex) {
236            throw new ConfigurationException(ex);
237        }
238    }
239
240    /**
241     * Gets the name of the configuration class created by the builder produced by this provider.
242     *
243     * @return the configuration class
244     */
245    public String getConfigurationClass() {
246        return configurationClass;
247    }
248
249    /**
250     * Gets an unmodifiable collection with the names of parameter classes supported by this provider.
251     *
252     * @return the parameter classes
253     */
254    public Collection<String> getParameterClasses() {
255        return parameterClasses;
256    }
257
258    /**
259     * Gets the name of the class of the builder created by this provider if the reload flag is set. If this method
260     * returns <strong>null</strong>, reloading builders are not supported by this provider.
261     *
262     * @return the reloading builder class
263     */
264    public String getReloadingBuilderClass() {
265        return reloadingBuilderClass;
266    }
267
268    /**
269     * Passes all parameter objects to the parent {@code CombinedConfigurationBuilder} so that properties already defined
270     * for the parent builder can be added. This method is called before the parameter objects are initialized from the
271     * definition configuration. This way properties from the parent builder are inherited, but can be overridden for child
272     * configurations.
273     *
274     * @param decl the current {@code ConfigurationDeclaration}
275     * @param params the collection with (uninitialized) parameter objects
276     */
277    protected void inheritParentBuilderProperties(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params) {
278        params.forEach(p -> decl.getConfigurationBuilder().initChildBuilderParameters(p));
279    }
280
281    /**
282     * Initializes the parameter objects with data stored in the current bean declaration. This method is called before the
283     * newly created builder instance is configured with the parameter objects. It maps attributes of the bean declaration
284     * to properties of parameter objects. In addition, it invokes the parent {@code CombinedConfigurationBuilder} so that
285     * the parameters object can inherit properties already defined for this builder.
286     *
287     * @param decl the current {@code ConfigurationDeclaration}
288     * @param params the collection with (uninitialized) parameter objects
289     * @throws Exception if an error occurs
290     */
291    protected void initializeParameterObjects(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params) throws Exception {
292        inheritParentBuilderProperties(decl, params);
293        final MultiWrapDynaBean wrapBean = new MultiWrapDynaBean(params);
294        decl.getConfigurationBuilder().initBean(wrapBean, decl);
295    }
296
297    /**
298     * Determines the <em>allowFailOnInit</em> flag for the newly created builder based on the given
299     * {@code ConfigurationDeclaration}. Some combinations of flags in the declaration say that a configuration source is
300     * optional, but an empty instance should be created if its creation fail.
301     *
302     * @param decl the current {@code ConfigurationDeclaration}
303     * @return the value of the <em>allowFailOnInit</em> flag
304     */
305    protected boolean isAllowFailOnInit(final ConfigurationDeclaration decl) {
306        return decl.isOptional() && decl.isForceCreate();
307    }
308}