    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.util.ArrayList;
    020import java.util.Collection;
    021import java.util.Collections;
    022import java.util.HashMap;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Set;
    026import java.util.concurrent.ConcurrentHashMap;
    027import java.util.concurrent.CopyOnWriteArrayList;
    028
    029import org.apache.commons.text.StringSubstitutor;
    030import org.apache.commons.text.lookup.StringLookup;
    031
    032/**
    033 * <p>
    034 * A class that handles interpolation (variable substitution) for configuration
    035 * objects.
    036 * </p>
    037 * <p>
    038 * Each instance of {@code AbstractConfiguration} is associated with an object
    039 * of this class. All interpolation tasks are delegated to this object.
    040 * </p>
    041 * <p>
    042 * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor}
    043 * class from <a href="http://commons.apache.org/text">Commons Text</a>. Thus it
    044 * supports the same syntax of variable expressions.
    045 * </p>
    046 * <p>
    047 * The basic idea of this class is that it can maintain a set of primitive
    048 * {@link Lookup} objects, each of which is identified by a special prefix. The
    049 * variables to be processed have the form <code>${prefix:name}</code>.
    050 * {@code ConfigurationInterpolator} will extract the prefix and determine,
    051 * which primitive lookup object is registered for it. Then the name of the
    052 * variable is passed to this object to obtain the actual value. It is also
    053 * possible to define an arbitrary number of default lookup objects, which are
    054 * used for variables that do not have a prefix or that cannot be resolved by
    055 * their associated lookup object. When adding default lookup objects their
    056 * order matters; they are queried in this order, and the first non-<b>null</b>
    057 * variable value is used.
    058 * </p>
    059 * <p>
    060 * After an instance has been created it does not contain any {@code Lookup}
    061 * objects. The current set of lookup objects can be modified using the
    062 * {@code registerLookup()} and {@code deregisterLookup()} methods. Default
    063 * lookup objects (that are invoked for variables without a prefix) can be added
    064 * or removed with the {@code addDefaultLookup()} and
    065 * {@code removeDefaultLookup()} methods respectively. (When a
    066 * {@code ConfigurationInterpolator} instance is created by a configuration
    067 * object, a default lookup object is added pointing to the configuration
    068 * itself, so that variables are resolved using the configuration's properties.)
    069 * </p>
    070 * <p>
    071 * The default usage scenario is that on a fully initialized instance the
    072 * {@code interpolate()} method is called. It is passed an object value which
    073 * may contain variables. All these variables are substituted if they can be
    074 * resolved. The result is the passed in value with variables replaced.
    075 * Alternatively, the {@code resolve()} method can be called to obtain the
    076 * values of specific variables without performing interpolation.
    077 * </p>
    078 * <p>
    079 * Implementation node: This class is thread-safe. Lookup objects can be added
    080 * or removed at any time concurrent to interpolation operations.
    081 * </p>
    082 *
    083 * @version $Id: ConfigurationInterpolator.java 1844056 2018-10-17 02:25:06Z ggregory $
    084 * @since 1.4
    085 * @author <a
    086 *         href="http://commons.apache.org/configuration/team-list.html">Commons
    087 *         Configuration team</a>
    088 */
    089public class ConfigurationInterpolator
    090{
    091    /** Constant for the prefix separator. */
    092    private static final char PREFIX_SEPARATOR = ':';
    093
    094    /** The variable prefix. */
    095    private static final String VAR_START = "${";
    096
    097    /** The length of {@link #VAR_START}. */
    098    private static final int VAR_START_LENGTH = VAR_START.length();
    099
    100    /** The variable suffix. */
    101    private static final String VAR_END = "}";
    102
    103    /** The length of {@link #VAR_END}. */
    104    private static final int VAR_END_LENGTH = VAR_END.length();
    105
    106    /** A map containing the default prefix lookups. */
    107    private static final Map<String, Lookup> DEFAULT_PREFIX_LOOKUPS;
    108
    109    static
    110    {
    111        final Map<String, Lookup> lookups = new HashMap<>(DefaultLookups.values().length);
    112        for (final DefaultLookups l : DefaultLookups.values())
    113        {
    114            lookups.put(l.getPrefix(), l.getLookup());
    115        }
    116        DEFAULT_PREFIX_LOOKUPS = Collections.unmodifiableMap(lookups);
    117    }
    118
    119    /**
    120     * Creates a new instance based on the properties in the given specification
    121     * object.
    122     *
    123     * @param spec the {@code InterpolatorSpecification}
    124     * @return the newly created instance
    125     */
    126    private static ConfigurationInterpolator createInterpolator(
    127            final InterpolatorSpecification spec)
    128    {
    129        final ConfigurationInterpolator ci = new ConfigurationInterpolator();
    130        ci.addDefaultLookups(spec.getDefaultLookups());
    131        ci.registerLookups(spec.getPrefixLookups());
    132        ci.setParentInterpolator(spec.getParentInterpolator());
    133        return ci;
    134    }
    135
    136    /**
    137     * Extracts the variable name from a value that consists of a single
    138     * variable.
    139     *
    140     * @param strValue the value
    141     * @return the extracted variable name
    142     */
    143    private static String extractVariableName(final String strValue)
    144    {
    145        return strValue.substring(VAR_START_LENGTH,
    146                strValue.length() - VAR_END_LENGTH);
    147    }
    148
    149    /**
    150     * Creates a new {@code ConfigurationInterpolator} instance based on the
    151     * passed in specification object. If the {@code InterpolatorSpecification}
    152     * already contains a {@code ConfigurationInterpolator} object, it is used
    153     * directly. Otherwise, a new instance is created and initialized with the
    154     * properties stored in the specification.
    155     *
    156     * @param spec the {@code InterpolatorSpecification} (must not be
    157     *        <b>null</b>)
    158     * @return the {@code ConfigurationInterpolator} obtained or created based
    159     *         on the given specification
    160     * @throws IllegalArgumentException if the specification is <b>null</b>
    161     * @since 2.0
    162     */
    163    public static ConfigurationInterpolator fromSpecification(
    164            final InterpolatorSpecification spec)
    165    {
    166        if (spec == null)
    167        {
    168            throw new IllegalArgumentException(
    169                    "InterpolatorSpecification must not be null!");
    170        }
    171        return (spec.getInterpolator() != null) ? spec.getInterpolator()
    172                : createInterpolator(spec);
    173    }
    174
    175    /**
    176     * Returns a map containing the default prefix lookups. Every configuration
    177     * object derived from {@code AbstractConfiguration} is by default
    178     * initialized with a {@code ConfigurationInterpolator} containing these
    179     * {@code Lookup} objects and their prefixes. The map cannot be modified
    180     *
    181     * @return a map with the default prefix {@code Lookup} objects and their
    182     *         prefixes
    183     * @since 2.0
    184     */
    185    public static Map<String, Lookup> getDefaultPrefixLookups()
    186    {
    187        return DEFAULT_PREFIX_LOOKUPS;
    188    }
    189
    190    /**
    191     * Utility method for obtaining a {@code Lookup} object in a safe way. This
    192     * method always returns a non-<b>null</b> {@code Lookup} object. If the
    193     * passed in {@code Lookup} is not <b>null</b>, it is directly returned.
    194     * Otherwise, result is a dummy {@code Lookup} which does not provide any
    195     * values.
    196     *
    197     * @param lookup the {@code Lookup} to check
    198     * @return a non-<b>null</b> {@code Lookup} object
    199     * @since 2.0
    200     */
    201    public static Lookup nullSafeLookup(Lookup lookup)
    202    {
    203        if (lookup == null)
    204        {
    205            lookup = DummyLookup.INSTANCE;
    206        }
    207        return lookup;
    208    }
    209
    210    /** A map with the currently registered lookup objects. */
    211    private final Map<String, Lookup> prefixLookups;
    212
    213    /** Stores the default lookup objects. */
    214    private final List<Lookup> defaultLookups;
    215
    216    /** The helper object performing variable substitution. */
    217    private final StringSubstitutor substitutor;
    218
    219    /** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
    220    private volatile ConfigurationInterpolator parentInterpolator;
    221
    222    /**
    223     * Creates a new instance of {@code ConfigurationInterpolator}.
    224     */
    225    public ConfigurationInterpolator()
    226    {
    227        prefixLookups = new ConcurrentHashMap<>();
    228        defaultLookups = new CopyOnWriteArrayList<>();
    229        substitutor = initSubstitutor();
    230    }
    231
    232    /**
    233     * Adds a default {@code Lookup} object. Default {@code Lookup} objects are
    234     * queried (in the order they were added) for all variables without a
    235     * special prefix. If no default {@code Lookup} objects are present, such
    236     * variables won't be processed.
    237     *
    238     * @param defaultLookup the default {@code Lookup} object to be added (must
    239     *        not be <b>null</b>)
    240     * @throws IllegalArgumentException if the {@code Lookup} object is
    241     *         <b>null</b>
    242     */
    243    public void addDefaultLookup(final Lookup defaultLookup)
    244    {
    245        defaultLookups.add(defaultLookup);
    246    }
    247
    248    /**
    249     * Adds all {@code Lookup} objects in the given collection as default
    250     * lookups. The collection can be <b>null</b>, then this method has no
    251     * effect. It must not contain <b>null</b> entries.
    252     *
    253     * @param lookups the {@code Lookup} objects to be added as default lookups
    254     * @throws IllegalArgumentException if the collection contains a <b>null</b>
    255     *         entry
    256     */
    257    public void addDefaultLookups(final Collection<? extends Lookup> lookups)
    258    {
    259        if (lookups != null)
    260        {
    261            defaultLookups.addAll(lookups);
    262        }
    263    }
    264
    265    /**
    266     * Deregisters the {@code Lookup} object for the specified prefix at this
    267     * instance. It will be removed from this instance.
    268     *
    269     * @param prefix the variable prefix
    270     * @return a flag whether for this prefix a lookup object had been
    271     *         registered
    272     */
    273    public boolean deregisterLookup(final String prefix)
    274    {
    275        return prefixLookups.remove(prefix) != null;
    276    }
    277
    278    /**
    279     * Obtains the lookup object for the specified prefix. This method is called
    280     * by the {@code lookup()} method. This implementation will check
    281     * whether a lookup object is registered for the given prefix. If not, a
    282     * <b>null</b> lookup object will be returned (never <b>null</b>).
    283     *
    284     * @param prefix the prefix
    285     * @return the lookup object to be used for this prefix
    286     */
    287    protected Lookup fetchLookupForPrefix(final String prefix)
    288    {
    289        return nullSafeLookup(prefixLookups.get(prefix));
    290    }
    291
    292    /**
    293     * Returns a collection with the default {@code Lookup} objects
    294     * added to this {@code ConfigurationInterpolator}. These objects are not
    295     * associated with a variable prefix. The returned list is a snapshot copy
    296     * of the internal collection of default lookups; so manipulating it does
    297     * not affect this instance.
    298     *
    299     * @return the default lookup objects
    300     */
    301    public List<Lookup> getDefaultLookups()
    302    {
    303        return new ArrayList<>(defaultLookups);
    304    }
    305
    306    /**
    307     * Returns a map with the currently registered {@code Lookup} objects and
    308     * their prefixes. This is a snapshot copy of the internally used map. So
    309     * modifications of this map do not effect this instance.
    310     *
    311     * @return a copy of the map with the currently registered {@code Lookup}
    312     *         objects
    313     */
    314    public Map<String, Lookup> getLookups()
    315    {
    316        return new HashMap<>(prefixLookups);
    317    }
    318
    319    /**
    320     * Returns the parent {@code ConfigurationInterpolator}.
    321     *
    322     * @return the parent {@code ConfigurationInterpolator} (can be <b>null</b>)
    323     */
    324    public ConfigurationInterpolator getParentInterpolator()
    325    {
    326        return this.parentInterpolator;
    327    }
    328
    329    /**
    330     * Creates and initializes a {@code StringSubstitutor} object which is used for
    331     * variable substitution. This {@code StringSubstitutor} is assigned a
    332     * specialized lookup object implementing the correct variable resolving
    333     * algorithm.
    334     *
    335     * @return the {@code StringSubstitutor} used by this object
    336     */
    337    private StringSubstitutor initSubstitutor()
    338    {
    339        return new StringSubstitutor(new StringLookup()
    340        {
    341            @Override
    342            public String lookup(final String key)
    343            {
    344                final Object result = resolve(key);
    345                return result != null ? result.toString() : null;
    346            }
    347        });
    348    }
    349
    350    /**
    351     * Performs interpolation of the passed in value. If the value is of type
    352     * String, this method checks whether it contains variables. If so, all
    353     * variables are replaced by their current values (if possible). For non
    354     * string arguments, the value is returned without changes.
    355     *
    356     * @param value the value to be interpolated
    357     * @return the interpolated value
    358     */
    359    public Object interpolate(final Object value)
    360    {
    361        if (value instanceof String)
    362        {
    363            final String strValue = (String) value;
    364            if (looksLikeSingleVariable(strValue))
    365            {
    366                final Object resolvedValue = resolveSingleVariable(strValue);
    367                if (resolvedValue != null && !(resolvedValue instanceof String))
    368                {
    369                    // If the value is again a string, it needs no special
    370                    // treatment; it may also contain further variables which
    371                    // must be resolved; therefore, the default mechanism is
    372                    // applied.
    373                    return resolvedValue;
    374                }
    375            }
    376            return substitutor.replace(strValue);
    377        }
    378        return value;
    379    }
    380
    381    /**
    382     * Sets a flag that variable names can contain other variables. If enabled,
    383     * variable substitution is also done in variable names.
    384     *
    385     * @return the substitution in variables flag
    386     */
    387    public boolean isEnableSubstitutionInVariables()
    388    {
    389        return substitutor.isEnableSubstitutionInVariables();
    390    }
    391
    392    /**
    393     * Checks whether a value to be interpolated seems to be a single variable.
    394     * In this case, it is resolved directly without using the
    395     * {@code StringSubstitutor}. Note that it is okay if this method returns a
    396     * false positive: In this case, resolving is going to fail, and standard
    397     * mechanism is used.
    398     *
    399     * @param strValue the value to be interpolated
    400     * @return a flag whether this value seems to be a single variable
    401     */
    402    private boolean looksLikeSingleVariable(final String strValue)
    403    {
    404        return strValue.startsWith(VAR_START) && strValue.endsWith(VAR_END);
    405    }
    406
    407    /**
    408     * Returns an unmodifiable set with the prefixes, for which {@code Lookup}
    409     * objects are registered at this instance. This means that variables with
    410     * these prefixes can be processed.
    411     *
    412     * @return a set with the registered variable prefixes
    413     */
    414    public Set<String> prefixSet()
    415    {
    416        return Collections.unmodifiableSet(prefixLookups.keySet());
    417    }
    418
    419    /**
    420     * Registers the given {@code Lookup} object for the specified prefix at
    421     * this instance. From now on this lookup object will be used for variables
    422     * that have the specified prefix.
    423     *
    424     * @param prefix the variable prefix (must not be <b>null</b>)
    425     * @param lookup the {@code Lookup} object to be used for this prefix (must
    426     *        not be <b>null</b>)
    427     * @throws IllegalArgumentException if either the prefix or the
    428     *         {@code Lookup} object is <b>null</b>
    429     */
    430    public void registerLookup(final String prefix, final Lookup lookup)
    431    {
    432        if (prefix == null)
    433        {
    434            throw new IllegalArgumentException(
    435                    "Prefix for lookup object must not be null!");
    436        }
    437        if (lookup == null)
    438        {
    439            throw new IllegalArgumentException(
    440                    "Lookup object must not be null!");
    441        }
    442        prefixLookups.put(prefix, lookup);
    443    }
    444
    445    /**
    446     * Registers all {@code Lookup} objects in the given map with their prefixes
    447     * at this {@code ConfigurationInterpolator}. Using this method multiple
    448     * {@code Lookup} objects can be registered at once. If the passed in map is
    449     * <b>null</b>, this method does not have any effect.
    450     *
    451     * @param lookups the map with lookups to register (may be <b>null</b>)
    452     * @throws IllegalArgumentException if the map contains <b>entries</b>
    453     */
    454    public void registerLookups(final Map<String, ? extends Lookup> lookups)
    455    {
    456        if (lookups != null)
    457        {
    458            prefixLookups.putAll(lookups);
    459        }
    460    }
    461
    462    /**
    463     * Removes the specified {@code Lookup} object from the list of default
    464     * {@code Lookup}s.
    465     *
    466     * @param lookup the {@code Lookup} object to be removed
    467     * @return a flag whether this {@code Lookup} object actually existed and
    468     *         was removed
    469     */
    470    public boolean removeDefaultLookup(final Lookup lookup)
    471    {
    472        return defaultLookups.remove(lookup);
    473    }
    474
    475    /**
    476     * Resolves the specified variable. This implementation tries to extract
    477     * a variable prefix from the given variable name (the first colon (':') is
    478     * used as prefix separator). It then passes the name of the variable with
    479     * the prefix stripped to the lookup object registered for this prefix. If
    480     * no prefix can be found or if the associated lookup object cannot resolve
    481     * this variable, the default lookup objects are used. If this is not
    482     * successful either and a parent {@code ConfigurationInterpolator} is
    483     * available, this object is asked to resolve the variable.
    484     *
    485     * @param var the name of the variable whose value is to be looked up which may contain a prefix.
    486     * @return the value of this variable or <b>null</b> if it cannot be
    487     * resolved
    488     */
    489    public Object resolve(final String var)
    490    {
    491        if (var == null)
    492        {
    493            return null;
    494        }
    495
    496        final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
    497        if (prefixPos >= 0)
    498        {
    499            final String prefix = var.substring(0, prefixPos);
    500            final String name = var.substring(prefixPos + 1);
    501            final Object value = fetchLookupForPrefix(prefix).lookup(name);
    502            if (value != null)
    503            {
    504                return value;
    505            }
    506        }
    507
    508        for (final Lookup lookup : defaultLookups)
    509        {
    510            final Object value = lookup.lookup(var);
    511            if (value != null)
    512            {
    513                return value;
    514            }
    515        }
    516
    517        final ConfigurationInterpolator parent = getParentInterpolator();
    518        if (parent != null)
    519        {
    520            return getParentInterpolator().resolve(var);
    521        }
    522        return null;
    523    }
    524
    525    /**
    526     * Interpolates a string value that seems to be a single variable.
    527     *
    528     * @param strValue the string to be interpolated
    529     * @return the resolved value or <b>null</b> if resolving failed
    530     */
    531    private Object resolveSingleVariable(final String strValue)
    532    {
    533        return resolve(extractVariableName(strValue));
    534    }
    535
    536    /**
    537     * Sets the flag whether variable names can contain other variables. This
    538     * flag corresponds to the {@code enableSubstitutionInVariables} property of
    539     * the underlying {@code StringSubstitutor} object.
    540     *
    541     * @param f the new value of the flag
    542     */
    543    public void setEnableSubstitutionInVariables(final boolean f)
    544    {
    545        substitutor.setEnableSubstitutionInVariables(f);
    546    }
    547
    548    /**
    549     * Sets the parent {@code ConfigurationInterpolator}. This object is used if
    550     * the {@code Lookup} objects registered at this object cannot resolve a
    551     * variable.
    552     *
    553     * @param parentInterpolator the parent {@code ConfigurationInterpolator}
    554     *        object (can be <b>null</b>)
    555     */
    556    public void setParentInterpolator(
    557            final ConfigurationInterpolator parentInterpolator)
    558    {
    559        this.parentInterpolator = parentInterpolator;
    560    }
    561}