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}