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