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     * 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 (e.g., 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 <b>null</b>)
289     * @return the {@code ConfigurationInterpolator} obtained or created based on the given specification
290     * @throws IllegalArgumentException if the specification is <b>null</b>
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 (e.g.,
308     * {@link DefaultLookups#SCRIPT SCRIPT}) and those that can result in contact with remote servers (e.g.,
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-<b>null</b>
411     * {@code Lookup} object. If the passed in {@code Lookup} is not <b>null</b>, 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-<b>null</b> {@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 <b>null</b>)
455     * @throws IllegalArgumentException if the {@code Lookup} object is <b>null</b>
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 <b>null</b>, then
463     * this method has no effect. It must not contain <b>null</b> entries.
464     *
465     * @param lookups the {@code Lookup} objects to be added as default lookups
466     * @throws IllegalArgumentException if the collection contains a <b>null</b> 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 <b>null</b> lookup
488     * object will be returned (never <b>null</b>).
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 <b>null</b>)
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, e.g.,
604     * <code>${myvar}</code>. 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 <b>null</b>)
630     * @param lookup the {@code Lookup} object to be used for this prefix (must not be <b>null</b>)
631     * @throws IllegalArgumentException if either the prefix or the {@code Lookup} object is <b>null</b>
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 <b>null</b>,
646     * this method does not have any effect.
647     *
648     * @param lookups the map with lookups to register (may be <b>null</b>)
649     * @throws IllegalArgumentException if the map contains <b>entries</b>
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 <b>null</b> 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 <b>null</b> 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 <b>null</b>)
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}