View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.interpol;
18  
19  import java.lang.reflect.Array;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Properties;
29  import java.util.Set;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.CopyOnWriteArrayList;
32  import java.util.function.Function;
33  
34  import org.apache.commons.text.StringSubstitutor;
35  
36  /**
37   * <p>
38   * A class that handles interpolation (variable substitution) for configuration objects.
39   * </p>
40   * <p>
41   * Each instance of {@code AbstractConfiguration} is associated with an object of this class. All interpolation tasks
42   * are delegated to this object.
43   * </p>
44   * <p>
45   * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor} class from
46   * <a href="https://commons.apache.org/text">Commons Text</a>. Thus it supports the same syntax of variable expressions.
47   * </p>
48   * <p>
49   * The basic idea of this class is that it can maintain a set of primitive {@link Lookup} objects, each of which is
50   * identified by a special prefix. The variables to be processed have the form {@code ${prefix:name}}.
51   * {@code ConfigurationInterpolator} will extract the prefix and determine, which primitive lookup object is registered
52   * for it. Then the name of the variable is passed to this object to obtain the actual value. It is also possible to
53   * define an arbitrary number of default lookup objects, which are used for variables that do not have a prefix or that
54   * cannot be resolved by their associated lookup object. When adding default lookup objects their order matters; they
55   * are queried in this order, and the first non-<strong>null</strong> variable value is used.
56   * </p>
57   * <p>
58   * After an instance has been created it does not contain any {@code Lookup} objects. The current set of lookup objects
59   * can be modified using the {@code registerLookup()} and {@code deregisterLookup()} methods. Default lookup objects
60   * (that are invoked for variables without a prefix) can be added or removed with the {@code addDefaultLookup()} and
61   * {@code removeDefaultLookup()} methods respectively. (When a {@code ConfigurationInterpolator} instance is created by
62   * a configuration object, a default lookup object is added pointing to the configuration itself, so that variables are
63   * resolved using the configuration's properties.)
64   * </p>
65   * <p>
66   * The default usage scenario is that on a fully initialized instance the {@code interpolate()} method is called. It is
67   * passed an object value which may contain variables. All these variables are substituted if they can be resolved. The
68   * result is the passed in value with variables replaced. Alternatively, the {@code resolve()} method can be called to
69   * obtain the values of specific variables without performing interpolation.
70   * </p>
71   * <p><strong>String Conversion</strong></p>
72   * <p>
73   * When variables are part of larger interpolated strings, the variable values, which can be of any type, must be
74   * converted to strings to produce the full result. Each interpolator instance has a configurable
75   * {@link #setStringConverter(Function) string converter} to perform this conversion. The default implementation of this
76   * function simply uses the value's {@code toString} method in the majority of cases. However, for maximum
77   * consistency with
78   * {@link org.apache.commons.configuration2.convert.DefaultConversionHandler DefaultConversionHandler}, when a variable
79   * value is a container type (such as a collection or array), then only the first element of the container is converted
80   * to a string instead of the container itself. For example, if the variable {@code x} resolves to the integer array
81   * {@code [1, 2, 3]}, then the string {@code "my value = ${x}"} will by default be interpolated to
82   * {@code "my value = 1"}.
83   * </p>
84   * <p>
85   * <strong>Implementation note:</strong> This class is thread-safe. Lookup objects can be added or removed at any time
86   * concurrent to interpolation operations.
87   * </p>
88   *
89   * @since 1.4
90   */
91  public class ConfigurationInterpolator {
92  
93      /**
94       * Internal class used to construct the default {@link Lookup} map used by
95       * {@link ConfigurationInterpolator#getDefaultPrefixLookups()}.
96       */
97      static final class DefaultPrefixLookupsHolder {
98  
99          /** 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 }