    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(o -> o instanceof Lookup)) {
    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}