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;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.Map;
023
024import org.apache.commons.configuration2.ConfigurationDecoder;
025import org.apache.commons.configuration2.io.ConfigurationLogger;
026import org.apache.commons.configuration2.beanutils.BeanHelper;
027import org.apache.commons.configuration2.convert.ConversionHandler;
028import org.apache.commons.configuration2.convert.ListDelimiterHandler;
029import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
030import org.apache.commons.configuration2.interpol.InterpolatorSpecification;
031import org.apache.commons.configuration2.interpol.Lookup;
032import org.apache.commons.configuration2.sync.Synchronizer;
033
034/**
035 * <p>
036 * An implementation of {@code BuilderParameters} which handles the parameters
037 * of a {@link ConfigurationBuilder} common to all concrete
038 * {@code Configuration} implementations.
039 * </p>
040 * <p>
041 * This class provides methods for setting standard properties supported by the
042 * {@code AbstractConfiguration} base class. A fluent interface can be used to
043 * set property values.
044 * </p>
045 * <p>
046 * This class is not thread-safe. It is intended that an instance is constructed
047 * and initialized by a single thread during configuration of a
048 * {@code ConfigurationBuilder}.
049 * </p>
050 *
051 * @version $Id: BasicBuilderParameters.java 1842194 2018-09-27 22:24:23Z ggregory $
052 * @since 2.0
053 */
054public class BasicBuilderParameters implements Cloneable, BuilderParameters,
055        BasicBuilderProperties<BasicBuilderParameters>
056{
057    /** The key of the <em>throwExceptionOnMissing</em> property. */
058    private static final String PROP_THROW_EXCEPTION_ON_MISSING =
059            "throwExceptionOnMissing";
060
061    /** The key of the <em>listDelimiterHandler</em> property. */
062    private static final String PROP_LIST_DELIMITER_HANDLER = "listDelimiterHandler";
063
064    /** The key of the <em>logger</em> property. */
065    private static final String PROP_LOGGER = "logger";
066
067    /** The key for the <em>interpolator</em> property. */
068    private static final String PROP_INTERPOLATOR = "interpolator";
069
070    /** The key for the <em>prefixLookups</em> property. */
071    private static final String PROP_PREFIX_LOOKUPS = "prefixLookups";
072
073    /** The key for the <em>defaultLookups</em> property. */
074    private static final String PROP_DEFAULT_LOOKUPS = "defaultLookups";
075
076    /** The key for the <em>parentInterpolator</em> property. */
077    private static final String PROP_PARENT_INTERPOLATOR = "parentInterpolator";
078
079    /** The key for the <em>synchronizer</em> property. */
080    private static final String PROP_SYNCHRONIZER = "synchronizer";
081
082    /** The key for the <em>conversionHandler</em> property. */
083    private static final String PROP_CONVERSION_HANDLER = "conversionHandler";
084
085    /** The key for the <em>configurationDecoder</em> property. */
086    private static final String PROP_CONFIGURATION_DECODER = "configurationDecoder";
087
088    /** The key for the {@code BeanHelper}. */
089    private static final String PROP_BEAN_HELPER = RESERVED_PARAMETER_PREFIX
090            + "BeanHelper";
091
092    /** The map for storing the current property values. */
093    private Map<String, Object> properties;
094
095    /**
096     * Creates a new instance of {@code BasicBuilderParameters}.
097     */
098    public BasicBuilderParameters()
099    {
100        properties = new HashMap<>();
101    }
102
103    /**
104     * {@inheritDoc} This implementation returns a copy of the internal
105     * parameters map with the values set so far. Collection structures
106     * (e.g. for lookup objects) are stored as defensive copies, so the
107     * original data cannot be modified.
108     */
109    @Override
110    public Map<String, Object> getParameters()
111    {
112        final HashMap<String, Object> result =
113                new HashMap<>(properties);
114        if (result.containsKey(PROP_INTERPOLATOR))
115        {
116            // A custom ConfigurationInterpolator overrides lookups
117            result.remove(PROP_PREFIX_LOOKUPS);
118            result.remove(PROP_DEFAULT_LOOKUPS);
119            result.remove(PROP_PARENT_INTERPOLATOR);
120        }
121
122        createDefensiveCopies(result);
123        return result;
124    }
125
126    /**
127     * Sets the <em>logger</em> property. With this property a concrete
128     * {@code Log} object can be set for the configuration. Thus logging
129     * behavior can be controlled.
130     *
131     * @param log the {@code Log} for the configuration produced by this builder
132     * @return a reference to this object for method chaining
133     */
134    @Override
135    public BasicBuilderParameters setLogger(final ConfigurationLogger log)
136    {
137        return setProperty(PROP_LOGGER, log);
138    }
139
140    /**
141     * Sets the value of the <em>throwExceptionOnMissing</em> property. This
142     * property controls the configuration's behavior if missing properties are
143     * queried: a value of <b>true</b> causes the configuration to throw an
144     * exception, for a value of <b>false</b> it will return <b>null</b> values.
145     * (Note: Methods returning a primitive data type will always throw an
146     * exception if the property is not defined.)
147     *
148     * @param b the value of the property
149     * @return a reference to this object for method chaining
150     */
151    @Override
152    public BasicBuilderParameters setThrowExceptionOnMissing(final boolean b)
153    {
154        return setProperty(PROP_THROW_EXCEPTION_ON_MISSING, Boolean.valueOf(b));
155    }
156
157    /**
158     * Sets the value of the <em>listDelimiterHandler</em> property. This
159     * property defines the object responsible for dealing with list delimiter
160     * and escaping characters. Note:
161     * {@link org.apache.commons.configuration2.AbstractConfiguration AbstractConfiguration}
162     * does not allow setting this property to <b>null</b>. If the default
163     * {@code ListDelimiterHandler} is to be used, do not call this method.
164     *
165     * @param handler the {@code ListDelimiterHandler}
166     * @return a reference to this object for method chaining
167     */
168    @Override
169    public BasicBuilderParameters setListDelimiterHandler(
170            final ListDelimiterHandler handler)
171    {
172        return setProperty(PROP_LIST_DELIMITER_HANDLER, handler);
173    }
174
175    /**
176     * {@inheritDoc} The passed in {@code ConfigurationInterpolator} is set
177     * without modifications.
178     */
179    @Override
180    public BasicBuilderParameters setInterpolator(final ConfigurationInterpolator ci)
181    {
182        return setProperty(PROP_INTERPOLATOR, ci);
183    }
184
185    /**
186     * {@inheritDoc} A defensive copy of the passed in map is created. A
187     * <b>null</b> argument causes all prefix lookups to be removed from the
188     * internal parameters map.
189     */
190    @Override
191    public BasicBuilderParameters setPrefixLookups(
192            final Map<String, ? extends Lookup> lookups)
193    {
194        if (lookups == null)
195        {
196            properties.remove(PROP_PREFIX_LOOKUPS);
197            return this;
198        }
199        return setProperty(PROP_PREFIX_LOOKUPS,
200                new HashMap<>(lookups));
201    }
202
203    /**
204     * {@inheritDoc} A defensive copy of the passed in collection is created. A
205     * <b>null</b> argument causes all default lookups to be removed from the
206     * internal parameters map.
207     */
208    @Override
209    public BasicBuilderParameters setDefaultLookups(
210            final Collection<? extends Lookup> lookups)
211    {
212        if (lookups == null)
213        {
214            properties.remove(PROP_DEFAULT_LOOKUPS);
215            return this;
216        }
217        return setProperty(PROP_DEFAULT_LOOKUPS, new ArrayList<>(
218                lookups));
219    }
220
221    /**
222     * {@inheritDoc} This implementation stores the passed in
223     * {@code ConfigurationInterpolator} object in the internal parameters map.
224     */
225    @Override
226    public BasicBuilderParameters setParentInterpolator(
227            final ConfigurationInterpolator parent)
228    {
229        return setProperty(PROP_PARENT_INTERPOLATOR, parent);
230    }
231
232    /**
233     * {@inheritDoc} This implementation stores the passed in
234     * {@code Synchronizer} object in the internal parameters map.
235     */
236    @Override
237    public BasicBuilderParameters setSynchronizer(final Synchronizer sync)
238    {
239        return setProperty(PROP_SYNCHRONIZER, sync);
240    }
241
242    /**
243     * {@inheritDoc} This implementation stores the passed in
244     * {@code ConversionHandler} object in the internal parameters map.
245     */
246    @Override
247    public BasicBuilderParameters setConversionHandler(final ConversionHandler handler)
248    {
249        return setProperty(PROP_CONVERSION_HANDLER, handler);
250    }
251
252    /**
253     * {@inheritDoc} This implementation stores the passed in {@code BeanHelper}
254     * object in the internal parameters map, but uses a reserved key, so that
255     * it is not used for the initialization of properties of the managed
256     * configuration object. The {@code fetchBeanHelper()} method can be used to
257     * obtain the {@code BeanHelper} instance from a parameters map.
258     */
259    @Override
260    public BasicBuilderParameters setBeanHelper(final BeanHelper beanHelper)
261    {
262        return setProperty(PROP_BEAN_HELPER, beanHelper);
263    }
264
265    /**
266     * {@inheritDoc} This implementation stores the passed in
267     * {@code ConfigurationDecoder} object in the internal parameters map.
268     */
269    @Override
270    public BasicBuilderParameters setConfigurationDecoder(
271            final ConfigurationDecoder decoder)
272    {
273        return setProperty(PROP_CONFIGURATION_DECODER, decoder);
274    }
275
276    /**
277     * Merges this object with the given parameters object. This method adds all
278     * property values defined by the passed in parameters object to the
279     * internal storage which are not already in. So properties already defined
280     * in this object take precedence. Property names starting with the reserved
281     * parameter prefix are ignored.
282     *
283     * @param p the object whose properties should be merged (must not be
284     *        <b>null</b>)
285     * @throws IllegalArgumentException if the passed in object is <b>null</b>
286     */
287    public void merge(final BuilderParameters p)
288    {
289        if (p == null)
290        {
291            throw new IllegalArgumentException(
292                    "Parameters to merge must not be null!");
293        }
294
295        for (final Map.Entry<String, Object> e : p.getParameters().entrySet())
296        {
297            if (!properties.containsKey(e.getKey())
298                    && !e.getKey().startsWith(RESERVED_PARAMETER_PREFIX))
299            {
300                storeProperty(e.getKey(), e.getValue());
301            }
302        }
303    }
304
305    /**
306     * Inherits properties from the specified map. This can be used for instance
307     * to reuse parameters from one builder in another builder - also in
308     * parent-child relations in which a parent builder creates child builders.
309     * The purpose of this method is to let a concrete implementation decide
310     * which properties can be inherited. Because parameters are basically
311     * organized as a map it would be possible to simply copy over all
312     * properties from the source object. However, this is not appropriate in
313     * all cases. For instance, some properties - like a
314     * {@code ConfigurationInterpolator} - are tightly connected to a
315     * configuration and cannot be reused in a different context. For other
316     * properties, e.g. a file name, it does not make sense to copy it.
317     * Therefore, an implementation has to be explicit in the properties it
318     * wants to take over.
319     *
320     * @param source the source properties to inherit from
321     * @throws IllegalArgumentException if the source map is <b>null</b>
322     */
323    public void inheritFrom(final Map<String, ?> source)
324    {
325        if (source == null)
326        {
327            throw new IllegalArgumentException(
328                    "Source properties must not be null!");
329        }
330        copyPropertiesFrom(source, PROP_BEAN_HELPER, PROP_CONFIGURATION_DECODER,
331                PROP_CONVERSION_HANDLER, PROP_LIST_DELIMITER_HANDLER,
332                PROP_LOGGER, PROP_SYNCHRONIZER,
333                PROP_THROW_EXCEPTION_ON_MISSING);
334    }
335
336    /**
337     * Obtains a specification for a {@link ConfigurationInterpolator} from the
338     * specified map with parameters. All properties related to interpolation
339     * are evaluated and added to the specification object.
340     *
341     * @param params the map with parameters (must not be <b>null</b>)
342     * @return an {@code InterpolatorSpecification} object constructed with data
343     *         from the map
344     * @throws IllegalArgumentException if the map is <b>null</b> or contains
345     *         invalid data
346     */
347    public static InterpolatorSpecification fetchInterpolatorSpecification(
348            final Map<String, Object> params)
349    {
350        checkParameters(params);
351        return new InterpolatorSpecification.Builder()
352                .withInterpolator(
353                        fetchParameter(params, PROP_INTERPOLATOR,
354                                ConfigurationInterpolator.class))
355                .withParentInterpolator(
356                        fetchParameter(params, PROP_PARENT_INTERPOLATOR,
357                                ConfigurationInterpolator.class))
358                .withPrefixLookups(fetchAndCheckPrefixLookups(params))
359                .withDefaultLookups(fetchAndCheckDefaultLookups(params))
360                .create();
361    }
362
363    /**
364     * Obtains the {@code BeanHelper} object from the specified map with
365     * parameters. This method can be used to obtain an instance from a
366     * parameters map that has been set via the {@code setBeanHelper()} method.
367     * If no such instance is found, result is <b>null</b>.
368     *
369     * @param params the map with parameters (must not be <b>null</b>)
370     * @return the {@code BeanHelper} stored in this map or <b>null</b>
371     * @throws IllegalArgumentException if the map is <b>null</b>
372     */
373    public static BeanHelper fetchBeanHelper(final Map<String, Object> params)
374    {
375        checkParameters(params);
376        return (BeanHelper) params.get(PROP_BEAN_HELPER);
377    }
378
379    /**
380     * Clones this object. This is useful because multiple builder instances may
381     * use a similar set of parameters. However, single instances of parameter
382     * objects must not assigned to multiple builders. Therefore, cloning a
383     * parameters object provides a solution for this use case. This method
384     * creates a new parameters object with the same content as this one. The
385     * internal map storing the parameter values is cloned, too, also collection
386     * structures contained in this map. However, no a full deep clone operation
387     * is performed. Objects like a {@code ConfigurationInterpolator} or
388     * {@code Lookup}s are shared between this and the newly created instance.
389     *
390     * @return a clone of this object
391     */
392    @Override
393    public BasicBuilderParameters clone()
394    {
395        try
396        {
397            final BasicBuilderParameters copy =
398                    (BasicBuilderParameters) super.clone();
399            copy.properties = getParameters();
400            return copy;
401        }
402        catch (final CloneNotSupportedException cnex)
403        {
404            // should not happen
405            throw new AssertionError(cnex);
406        }
407    }
408
409    /**
410     * Sets a property for this parameters object. Properties are stored in an
411     * internal map. With this method a new entry can be added to this map. If
412     * the value is <b>null</b>, the key is removed from the internal map. This
413     * method can be used by sub classes which also store properties in a map.
414     *
415     * @param key the key of the property
416     * @param value the value of the property
417     */
418    protected void storeProperty(final String key, final Object value)
419    {
420        if (value == null)
421        {
422            properties.remove(key);
423        }
424        else
425        {
426            properties.put(key, value);
427        }
428    }
429
430    /**
431     * Obtains the value of the specified property from the internal map. This
432     * method can be used by derived classes if a specific property is to be
433     * accessed. If the given key is not found, result is <b>null</b>.
434     *
435     * @param key the key of the property in question
436     * @return the value of the property with this key or <b>null</b>
437     */
438    protected Object fetchProperty(final String key)
439    {
440        return properties.get(key);
441    }
442
443    /**
444     * Copies a number of properties from the given map into this object.
445     * Properties are only copied if they are defined in the source map.
446     *
447     * @param source the source map
448     * @param keys the keys to be copied
449     */
450    protected void copyPropertiesFrom(final Map<String, ?> source, final String... keys)
451    {
452        for (final String key : keys)
453        {
454            final Object value = source.get(key);
455            if (value != null)
456            {
457                storeProperty(key, value);
458            }
459        }
460    }
461
462    /**
463     * Helper method for setting a property value.
464     *
465     * @param key the key of the property
466     * @param value the value of the property
467     * @return a reference to this object
468     */
469    private BasicBuilderParameters setProperty(final String key, final Object value)
470    {
471        storeProperty(key, value);
472        return this;
473    }
474
475    /**
476     * Creates defensive copies for collection structures when constructing the
477     * map with parameters. It should not be possible to modify this object's
478     * internal state when having access to the parameters map.
479     *
480     * @param params the map with parameters to be passed to the caller
481     */
482    private static void createDefensiveCopies(final HashMap<String, Object> params)
483    {
484        final Map<String, ? extends Lookup> prefixLookups =
485                fetchPrefixLookups(params);
486        if (prefixLookups != null)
487        {
488            params.put(PROP_PREFIX_LOOKUPS, new HashMap<>(
489                    prefixLookups));
490        }
491        final Collection<? extends Lookup> defLookups = fetchDefaultLookups(params);
492        if (defLookups != null)
493        {
494            params.put(PROP_DEFAULT_LOOKUPS, new ArrayList<>(defLookups));
495        }
496    }
497
498    /**
499     * Obtains the map with prefix lookups from the parameters map.
500     *
501     * @param params the map with parameters
502     * @return the map with prefix lookups (may be <b>null</b>)
503     */
504    private static Map<String, ? extends Lookup> fetchPrefixLookups(
505            final Map<String, Object> params)
506    {
507        // This is safe to cast because we either have full control over the map
508        // and thus know the types of the contained values or have checked
509        // the content before
510        @SuppressWarnings("unchecked")
511        final
512        Map<String, ? extends Lookup> prefixLookups =
513                (Map<String, ? extends Lookup>) params.get(PROP_PREFIX_LOOKUPS);
514        return prefixLookups;
515    }
516
517    /**
518     * Tests whether the passed in map with parameters contains a map with
519     * prefix lookups. This method is used if the parameters map is from an
520     * insecure source and we cannot be sure that it contains valid data.
521     * Therefore, we have to map that the key for the prefix lookups actually
522     * points to a map containing keys and values of expected data types.
523     *
524     * @param params the parameters map
525     * @return the obtained map with prefix lookups
526     * @throws IllegalArgumentException if the map contains invalid data
527     */
528    private static Map<String, ? extends Lookup> fetchAndCheckPrefixLookups(
529            final Map<String, Object> params)
530    {
531        final Map<?, ?> prefixes =
532                fetchParameter(params, PROP_PREFIX_LOOKUPS, Map.class);
533        if (prefixes == null)
534        {
535            return null;
536        }
537
538        for (final Map.Entry<?, ?> e : prefixes.entrySet())
539        {
540            if (!(e.getKey() instanceof String)
541                    || !(e.getValue() instanceof Lookup))
542            {
543                throw new IllegalArgumentException(
544                        "Map with prefix lookups contains invalid data: "
545                                + prefixes);
546            }
547        }
548        return fetchPrefixLookups(params);
549    }
550
551    /**
552     * Obtains the collection with default lookups from the parameters map.
553     *
554     * @param params the map with parameters
555     * @return the collection with default lookups (may be <b>null</b>)
556     */
557    private static Collection<? extends Lookup> fetchDefaultLookups(
558            final Map<String, Object> params)
559    {
560        // This is safe to cast because we either have full control over the map
561        // and thus know the types of the contained values or have checked
562        // the content before
563        @SuppressWarnings("unchecked")
564        final
565        Collection<? extends Lookup> defLookups =
566                (Collection<? extends Lookup>) params.get(PROP_DEFAULT_LOOKUPS);
567        return defLookups;
568    }
569
570    /**
571     * Tests whether the passed in map with parameters contains a valid
572     * collection with default lookups. This method works like
573     * {@link #fetchAndCheckPrefixLookups(Map)}, but tests the default lookups
574     * collection.
575     *
576     * @param params the map with parameters
577     * @return the collection with default lookups (may be <b>null</b>)
578     * @throws IllegalArgumentException if invalid data is found
579     */
580    private static Collection<? extends Lookup> fetchAndCheckDefaultLookups(
581            final Map<String, Object> params)
582    {
583        final Collection<?> col =
584                fetchParameter(params, PROP_DEFAULT_LOOKUPS, Collection.class);
585        if (col == null)
586        {
587            return null;
588        }
589
590        for (final Object o : col)
591        {
592            if (!(o instanceof Lookup))
593            {
594                throw new IllegalArgumentException(
595                        "Collection with default lookups contains invalid data: "
596                                + col);
597            }
598        }
599        return fetchDefaultLookups(params);
600    }
601
602    /**
603     * Obtains a parameter from a map and performs a type check.
604     *
605     * @param params the map with parameters
606     * @param key the key of the parameter
607     * @param expClass the expected class of the parameter value
608     * @param <T> the parameter type
609     * @return the value of the parameter in the correct data type
610     * @throws IllegalArgumentException if the parameter is not of the expected
611     *         type
612     */
613    private static <T> T fetchParameter(final Map<String, Object> params, final String key,
614            final Class<T> expClass)
615    {
616        final Object value = params.get(key);
617        if (value == null)
618        {
619            return null;
620        }
621        if (!expClass.isInstance(value))
622        {
623            throw new IllegalArgumentException(String.format(
624                    "Parameter %s is not of type %s!", key,
625                    expClass.getSimpleName()));
626        }
627        return expClass.cast(value);
628    }
629
630    /**
631     * Checks whether a map with parameters is present. Throws an exception if
632     * not.
633     *
634     * @param params the map with parameters to check
635     * @throws IllegalArgumentException if the map is <b>null</b>
636     */
637    private static void checkParameters(final Map<String, Object> params)
638    {
639        if (params == null)
640        {
641            throw new IllegalArgumentException(
642                    "Parameters map must not be null!");
643        }
644    }
645}