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