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