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