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