Source code
Description: source: package: org.apache.commons.configuration2.interpol, class: ConfigurationInterpolator

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