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