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