    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.interpol;
    018
    019import java.lang.reflect.Array;
    020import java.util.ArrayList;
    021import java.util.Collection;
    022import java.util.Collections;
    023import java.util.HashMap;
    024import java.util.Iterator;
    025import java.util.List;
    026import java.util.Map;
    027import java.util.Objects;
    028import java.util.Properties;
    029import java.util.Set;
    030import java.util.concurrent.ConcurrentHashMap;
    031import java.util.concurrent.CopyOnWriteArrayList;
    032import java.util.function.Function;
    033
    034import org.apache.commons.text.StringSubstitutor;
    035
    036/**
    037 * <p>
    038 * A class that handles interpolation (variable substitution) for configuration objects.
    039 * </p>
    040 * <p>
    041 * Each instance of {@code AbstractConfiguration} is associated with an object of this class. All interpolation tasks
    042 * are delegated to this object.
    043 * </p>
    044 * <p>
    045 * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor} class from
    046 * <a href="https://commons.apache.org/text">Commons Text</a>. Thus it supports the same syntax of variable expressions.
    047 * </p>
    048 * <p>
    049 * The basic idea of this class is that it can maintain a set of primitive {@link Lookup} objects, each of which is
    050 * identified by a special prefix. The variables to be processed have the form {@code ${prefix:name}}.
    051 * {@code ConfigurationInterpolator} will extract the prefix and determine, which primitive lookup object is registered
    052 * for it. Then the name of the variable is passed to this object to obtain the actual value. It is also possible to
    053 * define an arbitrary number of default lookup objects, which are used for variables that do not have a prefix or that
    054 * cannot be resolved by their associated lookup object. When adding default lookup objects their order matters; they
    055 * are queried in this order, and the first non-<b>null</b> variable value is used.
    056 * </p>
    057 * <p>
    058 * After an instance has been created it does not contain any {@code Lookup} objects. The current set of lookup objects
    059 * can be modified using the {@code registerLookup()} and {@code deregisterLookup()} methods. Default lookup objects
    060 * (that are invoked for variables without a prefix) can be added or removed with the {@code addDefaultLookup()} and
    061 * {@code removeDefaultLookup()} methods respectively. (When a {@code ConfigurationInterpolator} instance is created by
    062 * a configuration object, a default lookup object is added pointing to the configuration itself, so that variables are
    063 * resolved using the configuration's properties.)
    064 * </p>
    065 * <p>
    066 * The default usage scenario is that on a fully initialized instance the {@code interpolate()} method is called. It is
    067 * passed an object value which may contain variables. All these variables are substituted if they can be resolved. The
    068 * result is the passed in value with variables replaced. Alternatively, the {@code resolve()} method can be called to
    069 * obtain the values of specific variables without performing interpolation.
    070 * </p>
    071 * <p><strong>String Conversion</strong></p>
    072 * <p>
    073 * When variables are part of larger interpolated strings, the variable values, which can be of any type, must be
    074 * converted to strings to produce the full result. Each interpolator instance has a configurable
    075 * {@link #setStringConverter(Function) string converter} to perform this conversion. The default implementation of this
    076 * function simply uses the value's {@code toString} method in the majority of cases. However, for maximum
    077 * consistency with
    078 * {@link org.apache.commons.configuration2.convert.DefaultConversionHandler DefaultConversionHandler}, when a variable
    079 * value is a container type (such as a collection or array), then only the first element of the container is converted
    080 * to a string instead of the container itself. For example, if the variable {@code x} resolves to the integer array
    081 * {@code [1, 2, 3]}, then the string <code>"my value = ${x}"</code> will by default be interpolated to
    082 * {@code "my value = 1"}.
    083 * </p>
    084 * <p>
    085 * <strong>Implementation note:</strong> This class is thread-safe. Lookup objects can be added or removed at any time
    086 * concurrent to interpolation operations.
    087 * </p>
    088 *
    089 * @since 1.4
    090 */
    091public class ConfigurationInterpolator {
    092
    093    /**
    094     * Name of the system property used to determine the lookups added by the
    095     * {@link #getDefaultPrefixLookups()} method. Use of this property is only required
    096     * in cases where the set of default lookups must be modified.
    097     *
    098     * @since 2.8.0
    099     */
    100    public static final String DEFAULT_PREFIX_LOOKUPS_PROPERTY =
    101            "org.apache.commons.configuration2.interpol.ConfigurationInterpolator.defaultPrefixLookups";
    102
    103    /** Constant for the prefix separator. */
    104    private static final char PREFIX_SEPARATOR = ':';
    105
    106    /** The variable prefix. */
    107    private static final String VAR_START = "${";
    108
    109    /** The length of {@link #VAR_START}. */
    110    private static final int VAR_START_LENGTH = VAR_START.length();
    111
    112    /** The variable suffix. */
    113    private static final String VAR_END = "}";
    114
    115    /** The length of {@link #VAR_END}. */
    116    private static final int VAR_END_LENGTH = VAR_END.length();
    117
    118    /** A map with the currently registered lookup objects. */
    119    private final Map<String, Lookup> prefixLookups;
    120
    121    /** Stores the default lookup objects. */
    122    private final List<Lookup> defaultLookups;
    123
    124    /** The helper object performing variable substitution. */
    125    private final StringSubstitutor substitutor;
    126
    127    /** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
    128    private volatile ConfigurationInterpolator parentInterpolator;
    129
    130    /** Function used to convert interpolated values to strings. */
    131    private volatile Function<Object, String> stringConverter = DefaultStringConverter.INSTANCE;
    132
    133    /**
    134     * Creates a new instance of {@code ConfigurationInterpolator}.
    135     */
    136    public ConfigurationInterpolator() {
    137        prefixLookups = new ConcurrentHashMap<>();
    138        defaultLookups = new CopyOnWriteArrayList<>();
    139        substitutor = initSubstitutor();
    140    }
    141
    142    /**
    143     * Creates a new instance based on the properties in the given specification object.
    144     *
    145     * @param spec the {@code InterpolatorSpecification}
    146     * @return the newly created instance
    147     */
    148    private static ConfigurationInterpolator createInterpolator(final InterpolatorSpecification spec) {
    149        final ConfigurationInterpolator ci = new ConfigurationInterpolator();
    150        ci.addDefaultLookups(spec.getDefaultLookups());
    151        ci.registerLookups(spec.getPrefixLookups());
    152        ci.setParentInterpolator(spec.getParentInterpolator());
    153        ci.setStringConverter(spec.getStringConverter());
    154        return ci;
    155    }
    156
    157    /**
    158     * Extracts the variable name from a value that consists of a single variable.
    159     *
    160     * @param strValue the value
    161     * @return the extracted variable name
    162     */
    163    private static String extractVariableName(final String strValue) {
    164        return strValue.substring(VAR_START_LENGTH, strValue.length() - VAR_END_LENGTH);
    165    }
    166
    167    /**
    168     * Creates a new {@code ConfigurationInterpolator} instance based on the passed in specification object. If the
    169     * {@code InterpolatorSpecification} already contains a {@code ConfigurationInterpolator} object, it is used directly.
    170     * Otherwise, a new instance is created and initialized with the properties stored in the specification.
    171     *
    172     * @param spec the {@code InterpolatorSpecification} (must not be <b>null</b>)
    173     * @return the {@code ConfigurationInterpolator} obtained or created based on the given specification
    174     * @throws IllegalArgumentException if the specification is <b>null</b>
    175     * @since 2.0
    176     */
    177    public static ConfigurationInterpolator fromSpecification(final InterpolatorSpecification spec) {
    178        if (spec == null) {
    179            throw new IllegalArgumentException("InterpolatorSpecification must not be null!");
    180        }
    181        return spec.getInterpolator() != null ? spec.getInterpolator() : createInterpolator(spec);
    182    }
    183
    184    /**
    185     * Gets a map containing the default prefix lookups. Every configuration object derived from
    186     * {@code AbstractConfiguration} is by default initialized with a {@code ConfigurationInterpolator} containing
    187     * these {@code Lookup} objects and their prefixes. The map cannot be modified.
    188     *
    189     * <p>
    190     * All of the lookups present in the returned map are from {@link DefaultLookups}. However, not all of the
    191     * available lookups are included by default. Specifically, lookups that can execute code (e.g.,
    192     * {@link DefaultLookups#SCRIPT SCRIPT}) and those that can result in contact with remote servers (e.g.,
    193     * {@link DefaultLookups#URL URL} and {@link DefaultLookups#DNS DNS}) are not included. If this behavior
    194     * must be modified, users can define the {@value #DEFAULT_PREFIX_LOOKUPS_PROPERTY} system property
    195     * with a comma-separated list of {@link DefaultLookups} enum names to be included in the set of defaults.
    196     * For example, setting this system property to {@code "BASE64_ENCODER,ENVIRONMENT"} will only include the
    197     * {@link DefaultLookups#BASE64_ENCODER BASE64_ENCODER} and
    198     * {@link DefaultLookups#ENVIRONMENT ENVIRONMENT} lookups. Setting the property to the empty string will
    199     * cause no defaults to be configured.
    200     * </p>
    201     *
    202     * <table>
    203     * <caption>Default Lookups</caption>
    204     * <tr>
    205     *  <th>Prefix</th>
    206     *  <th>Lookup</th>
    207     * </tr>
    208     * <tr>
    209     *  <td>"base64Decoder"</td>
    210     *  <td>{@link DefaultLookups#BASE64_DECODER BASE64_DECODER}</td>
    211     * </tr>
    212     * <tr>
    213     *  <td>"base64Encoder"</td>
    214     *  <td>{@link DefaultLookups#BASE64_ENCODER BASE64_ENCODER}</td>
    215     * </tr>
    216     * <tr>
    217     *  <td>"const"</td>
    218     *  <td>{@link DefaultLookups#CONST CONST}</td>
    219     * </tr>
    220     * <tr>
    221     *  <td>"date"</td>
    222     *  <td>{@link DefaultLookups#DATE DATE}</td>
    223     * </tr>
    224     * <tr>
    225     *  <td>"env"</td>
    226     *  <td>{@link DefaultLookups#ENVIRONMENT ENVIRONMENT}</td>
    227     * </tr>
    228     * <tr>
    229     *  <td>"file"</td>
    230     *  <td>{@link DefaultLookups#FILE FILE}</td>
    231     * </tr>
    232     * <tr>
    233     *  <td>"java"</td>
    234     *  <td>{@link DefaultLookups#JAVA JAVA}</td>
    235     * </tr>
    236     * <tr>
    237     *  <td>"localhost"</td>
    238     *  <td>{@link DefaultLookups#LOCAL_HOST LOCAL_HOST}</td>
    239     * </tr>
    240     * <tr>
    241     *  <td>"properties"</td>
    242     *  <td>{@link DefaultLookups#PROPERTIES PROPERTIES}</td>
    243     * </tr>
    244     * <tr>
    245     *  <td>"resourceBundle"</td>
    246     *  <td>{@link DefaultLookups#RESOURCE_BUNDLE RESOURCE_BUNDLE}</td>
    247     * </tr>
    248     * <tr>
    249     *  <td>"sys"</td>
    250     *  <td>{@link DefaultLookups#SYSTEM_PROPERTIES SYSTEM_PROPERTIES}</td>
    251     * </tr>
    252     * <tr>
    253     *  <td>"urlDecoder"</td>
    254     *  <td>{@link DefaultLookups#URL_DECODER URL_DECODER}</td>
    255     * </tr>
    256     * <tr>
    257     *  <td>"urlEncoder"</td>
    258     *  <td>{@link DefaultLookups#URL_ENCODER URL_ENCODER}</td>
    259     * </tr>
    260     * <tr>
    261     *  <td>"xml"</td>
    262     *  <td>{@link DefaultLookups#XML XML}</td>
    263     * </tr>
    264     * </table>
    265     *
    266     * <table>
    267     * <caption>Additional Lookups (not included by default)</caption>
    268     * <tr>
    269     *  <th>Prefix</th>
    270     *  <th>Lookup</th>
    271     * </tr>
    272     * <tr>
    273     *  <td>"dns"</td>
    274     *  <td>{@link DefaultLookups#DNS DNS}</td>
    275     * </tr>
    276     * <tr>
    277     *  <td>"url"</td>
    278     *  <td>{@link DefaultLookups#URL URL}</td>
    279     * </tr>
    280     * <tr>
    281     *  <td>"script"</td>
    282     *  <td>{@link DefaultLookups#SCRIPT SCRIPT}</td>
    283     * </tr>
    284     * </table>
    285     *
    286     * @return a map with the default prefix {@code Lookup} objects and their prefixes
    287     * @since 2.0
    288     */
    289    public static Map<String, Lookup> getDefaultPrefixLookups() {
    290        return DefaultPrefixLookupsHolder.INSTANCE.getDefaultPrefixLookups();
    291    }
    292
    293    /**
    294     * Utility method for obtaining a {@code Lookup} object in a safe way. This method always returns a non-<b>null</b>
    295     * {@code Lookup} object. If the passed in {@code Lookup} is not <b>null</b>, it is directly returned. Otherwise, result
    296     * is a dummy {@code Lookup} which does not provide any values.
    297     *
    298     * @param lookup the {@code Lookup} to check
    299     * @return a non-<b>null</b> {@code Lookup} object
    300     * @since 2.0
    301     */
    302    public static Lookup nullSafeLookup(Lookup lookup) {
    303        if (lookup == null) {
    304            lookup = DummyLookup.INSTANCE;
    305        }
    306        return lookup;
    307    }
    308
    309    /**
    310     * Adds a default {@code Lookup} object. Default {@code Lookup} objects are queried (in the order they were added) for
    311     * all variables without a special prefix. If no default {@code Lookup} objects are present, such variables won't be
    312     * processed.
    313     *
    314     * @param defaultLookup the default {@code Lookup} object to be added (must not be <b>null</b>)
    315     * @throws IllegalArgumentException if the {@code Lookup} object is <b>null</b>
    316     */
    317    public void addDefaultLookup(final Lookup defaultLookup) {
    318        defaultLookups.add(defaultLookup);
    319    }
    320
    321    /**
    322     * Adds all {@code Lookup} objects in the given collection as default lookups. The collection can be <b>null</b>, then
    323     * this method has no effect. It must not contain <b>null</b> entries.
    324     *
    325     * @param lookups the {@code Lookup} objects to be added as default lookups
    326     * @throws IllegalArgumentException if the collection contains a <b>null</b> entry
    327     */
    328    public void addDefaultLookups(final Collection<? extends Lookup> lookups) {
    329        if (lookups != null) {
    330            defaultLookups.addAll(lookups);
    331        }
    332    }
    333
    334    /**
    335     * Deregisters the {@code Lookup} object for the specified prefix at this instance. It will be removed from this
    336     * instance.
    337     *
    338     * @param prefix the variable prefix
    339     * @return a flag whether for this prefix a lookup object had been registered
    340     */
    341    public boolean deregisterLookup(final String prefix) {
    342        return prefixLookups.remove(prefix) != null;
    343    }
    344
    345    /**
    346     * Obtains the lookup object for the specified prefix. This method is called by the {@code lookup()} method. This
    347     * implementation will check whether a lookup object is registered for the given prefix. If not, a <b>null</b> lookup
    348     * object will be returned (never <b>null</b>).
    349     *
    350     * @param prefix the prefix
    351     * @return the lookup object to be used for this prefix
    352     */
    353    protected Lookup fetchLookupForPrefix(final String prefix) {
    354        return nullSafeLookup(prefixLookups.get(prefix));
    355    }
    356
    357    /**
    358     * Gets a collection with the default {@code Lookup} objects added to this {@code ConfigurationInterpolator}. These
    359     * objects are not associated with a variable prefix. The returned list is a snapshot copy of the internal collection of
    360     * default lookups; so manipulating it does not affect this instance.
    361     *
    362     * @return the default lookup objects
    363     */
    364    public List<Lookup> getDefaultLookups() {
    365        return new ArrayList<>(defaultLookups);
    366    }
    367
    368    /**
    369     * Gets a map with the currently registered {@code Lookup} objects and their prefixes. This is a snapshot copy of the
    370     * internally used map. So modifications of this map do not effect this instance.
    371     *
    372     * @return a copy of the map with the currently registered {@code Lookup} objects
    373     */
    374    public Map<String, Lookup> getLookups() {
    375        return new HashMap<>(prefixLookups);
    376    }
    377
    378    /**
    379     * Gets the parent {@code ConfigurationInterpolator}.
    380     *
    381     * @return the parent {@code ConfigurationInterpolator} (can be <b>null</b>)
    382     */
    383    public ConfigurationInterpolator getParentInterpolator() {
    384        return this.parentInterpolator;
    385    }
    386
    387    /**
    388     * Creates and initializes a {@code StringSubstitutor} object which is used for variable substitution. This
    389     * {@code StringSubstitutor} is assigned a specialized lookup object implementing the correct variable resolving
    390     * algorithm.
    391     *
    392     * @return the {@code StringSubstitutor} used by this object
    393     */
    394    private StringSubstitutor initSubstitutor() {
    395        return new StringSubstitutor(key -> {
    396            final Object value = resolve(key);
    397            return value != null
    398                ? stringConverter.apply(value)
    399                : null;
    400        });
    401    }
    402
    403    /**
    404     * Performs interpolation of the passed in value. If the value is of type {@code String}, this method checks
    405     * whether it contains variables. If so, all variables are replaced by their current values (if possible). For
    406     * non string arguments, the value is returned without changes. In the special case where the value is a string
    407     * consisting of a single variable reference, the interpolated variable value is <em>not</em> converted to a
    408     * string before returning, so that callers can access the raw value. However, if the variable is part of a larger
    409     * interpolated string, then the variable value is converted to a string using the configured
    410     * {@link #getStringConverter() string converter}. (See the discussion on string conversion in the class
    411     * documentation for more details.)
    412     *
    413     * <p><strong>Examples</strong></p>
    414     * <p>
    415     * For the following examples, assume that the default string conversion function is in place and that the
    416     * variable {@code i} maps to the integer value {@code 42}.
    417     * </p>
    418     * <pre>
    419     *      interpolator.interpolate(1) &rarr; 1 // non-string argument returned unchanged
    420     *      interpolator.interpolate("${i}") &rarr; 42 // single variable value returned with raw type
    421     *      interpolator.interpolate("answer = ${i}") &rarr; "answer = 42" // variable value converted to string
    422     * </pre>
    423     *
    424     * @param value the value to be interpolated
    425     * @return the interpolated value
    426     */
    427    public Object interpolate(final Object value) {
    428        if (value instanceof String) {
    429            final String strValue = (String) value;
    430            if (isSingleVariable(strValue)) {
    431                final Object resolvedValue = resolveSingleVariable(strValue);
    432                if (resolvedValue != null && !(resolvedValue instanceof String)) {
    433                    // If the value is again a string, it needs no special
    434                    // treatment; it may also contain further variables which
    435                    // must be resolved; therefore, the default mechanism is
    436                    // applied.
    437                    return resolvedValue;
    438                }
    439            }
    440            return substitutor.replace(strValue);
    441        }
    442        return value;
    443    }
    444
    445    /**
    446     * Sets a flag that variable names can contain other variables. If enabled, variable substitution is also done in
    447     * variable names.
    448     *
    449     * @return the substitution in variables flag
    450     */
    451    public boolean isEnableSubstitutionInVariables() {
    452        return substitutor.isEnableSubstitutionInVariables();
    453    }
    454
    455    /** Get the function used to convert interpolated values to strings.
    456     * @return function used to convert interpolated values to strings
    457     */
    458    public Function<Object, String> getStringConverter() {
    459        return stringConverter;
    460    }
    461
    462    /** Set the function used to convert interpolated values to strings. Pass
    463     * {@code null} to use the default conversion function.
    464     * @param stringConverter function used to convert interpolated values to strings
    465     *      or {@code null} to use the default conversion function
    466     */
    467    public void setStringConverter(final Function<Object, String> stringConverter) {
    468        this.stringConverter = stringConverter != null
    469                ? stringConverter
    470                : DefaultStringConverter.INSTANCE;
    471    }
    472
    473    /**
    474     * Checks whether a value to be interpolated consists of single, simple variable reference, e.g.,
    475     * <code>${myvar}</code>. In this case, the variable is resolved directly without using the
    476     * {@code StringSubstitutor}.
    477     *
    478     * @param strValue the value to be interpolated
    479     * @return {@code true} if the value contains a single, simple variable reference
    480     */
    481    private boolean isSingleVariable(final String strValue) {
    482        return strValue.startsWith(VAR_START)
    483                && strValue.indexOf(VAR_END, VAR_START_LENGTH) == strValue.length() - VAR_END_LENGTH;
    484    }
    485
    486    /**
    487     * Returns an unmodifiable set with the prefixes, for which {@code Lookup} objects are registered at this instance. This
    488     * means that variables with these prefixes can be processed.
    489     *
    490     * @return a set with the registered variable prefixes
    491     */
    492    public Set<String> prefixSet() {
    493        return Collections.unmodifiableSet(prefixLookups.keySet());
    494    }
    495
    496    /**
    497     * Registers the given {@code Lookup} object for the specified prefix at this instance. From now on this lookup object
    498     * will be used for variables that have the specified prefix.
    499     *
    500     * @param prefix the variable prefix (must not be <b>null</b>)
    501     * @param lookup the {@code Lookup} object to be used for this prefix (must not be <b>null</b>)
    502     * @throws IllegalArgumentException if either the prefix or the {@code Lookup} object is <b>null</b>
    503     */
    504    public void registerLookup(final String prefix, final Lookup lookup) {
    505        if (prefix == null) {
    506            throw new IllegalArgumentException("Prefix for lookup object must not be null!");
    507        }
    508        if (lookup == null) {
    509            throw new IllegalArgumentException("Lookup object must not be null!");
    510        }
    511        prefixLookups.put(prefix, lookup);
    512    }
    513
    514    /**
    515     * Registers all {@code Lookup} objects in the given map with their prefixes at this {@code ConfigurationInterpolator}.
    516     * Using this method multiple {@code Lookup} objects can be registered at once. If the passed in map is <b>null</b>,
    517     * this method does not have any effect.
    518     *
    519     * @param lookups the map with lookups to register (may be <b>null</b>)
    520     * @throws IllegalArgumentException if the map contains <b>entries</b>
    521     */
    522    public void registerLookups(final Map<String, ? extends Lookup> lookups) {
    523        if (lookups != null) {
    524            prefixLookups.putAll(lookups);
    525        }
    526    }
    527
    528    /**
    529     * Removes the specified {@code Lookup} object from the list of default {@code Lookup}s.
    530     *
    531     * @param lookup the {@code Lookup} object to be removed
    532     * @return a flag whether this {@code Lookup} object actually existed and was removed
    533     */
    534    public boolean removeDefaultLookup(final Lookup lookup) {
    535        return defaultLookups.remove(lookup);
    536    }
    537
    538    /**
    539     * Resolves the specified variable. This implementation tries to extract a variable prefix from the given variable name
    540     * (the first colon (':') is used as prefix separator). It then passes the name of the variable with the prefix stripped
    541     * to the lookup object registered for this prefix. If no prefix can be found or if the associated lookup object cannot
    542     * resolve this variable, the default lookup objects are used. If this is not successful either and a parent
    543     * {@code ConfigurationInterpolator} is available, this object is asked to resolve the variable.
    544     *
    545     * @param var the name of the variable whose value is to be looked up which may contain a prefix.
    546     * @return the value of this variable or <b>null</b> if it cannot be resolved
    547     */
    548    public Object resolve(final String var) {
    549        if (var == null) {
    550            return null;
    551        }
    552
    553        final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
    554        if (prefixPos >= 0) {
    555            final String prefix = var.substring(0, prefixPos);
    556            final String name = var.substring(prefixPos + 1);
    557            final Object value = fetchLookupForPrefix(prefix).lookup(name);
    558            if (value != null) {
    559                return value;
    560            }
    561        }
    562
    563        for (final Lookup lookup : defaultLookups) {
    564            final Object value = lookup.lookup(var);
    565            if (value != null) {
    566                return value;
    567            }
    568        }
    569
    570        final ConfigurationInterpolator parent = getParentInterpolator();
    571        if (parent != null) {
    572            return getParentInterpolator().resolve(var);
    573        }
    574        return null;
    575    }
    576
    577    /**
    578     * Interpolates a string value that consists of a single variable.
    579     *
    580     * @param strValue the string to be interpolated
    581     * @return the resolved value or <b>null</b> if resolving failed
    582     */
    583    private Object resolveSingleVariable(final String strValue) {
    584        return resolve(extractVariableName(strValue));
    585    }
    586
    587    /**
    588     * Sets the flag whether variable names can contain other variables. This flag corresponds to the
    589     * {@code enableSubstitutionInVariables} property of the underlying {@code StringSubstitutor} object.
    590     *
    591     * @param f the new value of the flag
    592     */
    593    public void setEnableSubstitutionInVariables(final boolean f) {
    594        substitutor.setEnableSubstitutionInVariables(f);
    595    }
    596
    597    /**
    598     * Sets the parent {@code ConfigurationInterpolator}. This object is used if the {@code Lookup} objects registered at
    599     * this object cannot resolve a variable.
    600     *
    601     * @param parentInterpolator the parent {@code ConfigurationInterpolator} object (can be <b>null</b>)
    602     */
    603    public void setParentInterpolator(final ConfigurationInterpolator parentInterpolator) {
    604        this.parentInterpolator = parentInterpolator;
    605    }
    606
    607    /**
    608     * Internal class used to construct the default {@link Lookup} map used by
    609     * {@link ConfigurationInterpolator#getDefaultPrefixLookups()}.
    610     */
    611    static final class DefaultPrefixLookupsHolder {
    612
    613        /** Singleton instance, initialized with the system properties. */
    614        static final DefaultPrefixLookupsHolder INSTANCE = new DefaultPrefixLookupsHolder(System.getProperties());
    615
    616        /** Default lookup map. */
    617        private final Map<String, Lookup> defaultLookups;
    618
    619        /**
    620         * Construct a new instance initialized with the given properties.
    621         * @param props initialization properties
    622         */
    623        DefaultPrefixLookupsHolder(final Properties props) {
    624            final Map<String, Lookup> lookups =
    625                    props.containsKey(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY)
    626                        ? parseLookups(props.getProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY))
    627                        : createDefaultLookups();
    628
    629            defaultLookups = Collections.unmodifiableMap(lookups);
    630        }
    631
    632        /**
    633         * Get the default prefix lookups map.
    634         * @return default prefix lookups map
    635         */
    636        Map<String, Lookup> getDefaultPrefixLookups() {
    637            return defaultLookups;
    638        }
    639
    640        /**
    641         * Create the lookup map used when the user has requested no customization.
    642         * @return default lookup map
    643         */
    644        private static Map<String, Lookup> createDefaultLookups() {
    645            final Map<String, Lookup> lookupMap = new HashMap<>();
    646
    647            addLookup(DefaultLookups.BASE64_DECODER, lookupMap);
    648            addLookup(DefaultLookups.BASE64_ENCODER, lookupMap);
    649            addLookup(DefaultLookups.CONST, lookupMap);
    650            addLookup(DefaultLookups.DATE, lookupMap);
    651            addLookup(DefaultLookups.ENVIRONMENT, lookupMap);
    652            addLookup(DefaultLookups.FILE, lookupMap);
    653            addLookup(DefaultLookups.JAVA, lookupMap);
    654            addLookup(DefaultLookups.LOCAL_HOST, lookupMap);
    655            addLookup(DefaultLookups.PROPERTIES, lookupMap);
    656            addLookup(DefaultLookups.RESOURCE_BUNDLE, lookupMap);
    657            addLookup(DefaultLookups.SYSTEM_PROPERTIES, lookupMap);
    658            addLookup(DefaultLookups.URL_DECODER, lookupMap);
    659            addLookup(DefaultLookups.URL_ENCODER, lookupMap);
    660            addLookup(DefaultLookups.XML, lookupMap);
    661
    662            return lookupMap;
    663        }
    664
    665        /**
    666         * Construct a lookup map by parsing the given string. The string is expected to contain
    667         * comma or space-separated names of values from the {@link DefaultLookups} enum.
    668         * @param str string to parse; not null
    669         * @return lookup map parsed from the given string
    670         * @throws IllegalArgumentException if the string does not contain a valid default lookup
    671         *      definition
    672         */
    673        private static Map<String, Lookup> parseLookups(final String str) {
    674            final Map<String, Lookup> lookupMap = new HashMap<>();
    675
    676            try {
    677                for (final String lookupName : str.split("[\\s,]+")) {
    678                    if (!lookupName.isEmpty()) {
    679                        addLookup(DefaultLookups.valueOf(lookupName.toUpperCase()), lookupMap);
    680                    }
    681                }
    682            } catch (IllegalArgumentException exc) {
    683                throw new IllegalArgumentException("Invalid default lookups definition: " + str, exc);
    684            }
    685
    686            return lookupMap;
    687        }
    688
    689        /**
    690         * Add the prefix and lookup from {@code lookup} to {@code map}.
    691         * @param lookup lookup to add
    692         * @param map map to add to
    693         */
    694        private static void addLookup(final DefaultLookups lookup, final Map<String, Lookup> map) {
    695            map.put(lookup.getPrefix(), lookup.getLookup());
    696        }
    697    }
    698
    699    /** Class encapsulating the default logic to convert resolved variable values into strings.
    700     * This class is thread-safe.
    701     */
    702    private static final class DefaultStringConverter implements Function<Object, String> {
    703
    704        /** Shared instance. */
    705        static final DefaultStringConverter INSTANCE = new DefaultStringConverter();
    706
    707        /** {@inheritDoc} */
    708        @Override
    709        public String apply(final Object obj) {
    710            return Objects.toString(extractSimpleValue(obj), null);
    711        }
    712
    713        /** Attempt to extract a simple value from {@code obj} for use in string conversion.
    714         * If the input represents a collection of some sort (e.g., an iterable or array),
    715         * the first item from the collection is returned.
    716         * @param obj input object
    717         * @return extracted simple object
    718         */
    719        private Object extractSimpleValue(final Object obj) {
    720            if (!(obj instanceof String)) {
    721                if (obj instanceof Iterable) {
    722                   return nextOrNull(((Iterable<?>) obj).iterator());
    723                } else if (obj instanceof Iterator) {
    724                    return nextOrNull((Iterator<?>) obj);
    725                } else if (obj.getClass().isArray()) {
    726                    return Array.getLength(obj) > 0
    727                            ? Array.get(obj, 0)
    728                            : null;
    729                }
    730            }
    731            return obj;
    732        }
    733
    734        /** Return the next value from {@code it} or {@code null} if no values remain.
    735         * @param <T> iterated type
    736         * @param it iterator
    737         * @return next value from {@code it} or {@code null} if no values remain
    738         */
    739        private <T> T nextOrNull(final Iterator<T> it) {
    740            return it.hasNext()
    741                    ? it.next()
    742                    : null;
    743        }
    744    }
    745}