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