/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.configuration2.interpol;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.DefaultStringLookup;

A class that handles interpolation (variable substitution) for configuration objects.

Each instance of AbstractConfiguration is associated with an object of this class. All interpolation tasks are delegated to this object.

ConfigurationInterpolator internally uses the StringSubstitutor class from Commons Text. Thus it supports the same syntax of variable expressions.

The basic idea of this class is that it can maintain a set of primitive Lookup objects, each of which is identified by a special prefix. The variables to be processed have the form ${prefix:name}. ConfigurationInterpolator will extract the prefix and determine, which primitive lookup object is registered for it. Then the name of the variable is passed to this object to obtain the actual value. It is also possible to define an arbitrary number of default lookup objects, which are used for variables that do not have a prefix or that cannot be resolved by their associated lookup object. When adding default lookup objects their order matters; they are queried in this order, and the first non-null variable value is used.

After an instance has been created it does not contain any Lookup objects. The current set of lookup objects can be modified using the registerLookup() and deregisterLookup() methods. Default lookup objects (that are invoked for variables without a prefix) can be added or removed with the addDefaultLookup() and removeDefaultLookup() methods respectively. (When a ConfigurationInterpolator instance is created by a configuration object, a default lookup object is added pointing to the configuration itself, so that variables are resolved using the configuration's properties.)

The default usage scenario is that on a fully initialized instance the interpolate() method is called. It is passed an object value which may contain variables. All these variables are substituted if they can be resolved. The result is the passed in value with variables replaced. Alternatively, the resolve() method can be called to obtain the values of specific variables without performing interpolation.

Implementation node: This class is thread-safe. Lookup objects can be added or removed at any time concurrent to interpolation operations.

Since:1.4
/** * <p> * A class that handles interpolation (variable substitution) for configuration * objects. * </p> * <p> * Each instance of {@code AbstractConfiguration} is associated with an object * of this class. All interpolation tasks are delegated to this object. * </p> * <p> * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor} * class from <a href="https://commons.apache.org/text">Commons Text</a>. Thus it * supports the same syntax of variable expressions. * </p> * <p> * The basic idea of this class is that it can maintain a set of primitive * {@link Lookup} objects, each of which is identified by a special prefix. The * variables to be processed have the form <code>${prefix:name}</code>. * {@code ConfigurationInterpolator} will extract the prefix and determine, * which primitive lookup object is registered for it. Then the name of the * variable is passed to this object to obtain the actual value. It is also * possible to define an arbitrary number of default lookup objects, which are * used for variables that do not have a prefix or that cannot be resolved by * their associated lookup object. When adding default lookup objects their * order matters; they are queried in this order, and the first non-<b>null</b> * variable value is used. * </p> * <p> * After an instance has been created it does not contain any {@code Lookup} * objects. The current set of lookup objects can be modified using the * {@code registerLookup()} and {@code deregisterLookup()} methods. Default * lookup objects (that are invoked for variables without a prefix) can be added * or removed with the {@code addDefaultLookup()} and * {@code removeDefaultLookup()} methods respectively. (When a * {@code ConfigurationInterpolator} instance is created by a configuration * object, a default lookup object is added pointing to the configuration * itself, so that variables are resolved using the configuration's properties.) * </p> * <p> * The default usage scenario is that on a fully initialized instance the * {@code interpolate()} method is called. It is passed an object value which * may contain variables. All these variables are substituted if they can be * resolved. The result is the passed in value with variables replaced. * Alternatively, the {@code resolve()} method can be called to obtain the * values of specific variables without performing interpolation. * </p> * <p> * Implementation node: This class is thread-safe. Lookup objects can be added * or removed at any time concurrent to interpolation operations. * </p> * * @since 1.4 */
public class ConfigurationInterpolator {
Constant for the prefix separator.
/** Constant for the prefix separator. */
private static final char PREFIX_SEPARATOR = ':';
The variable prefix.
/** The variable prefix. */
private static final String VAR_START = "${";
The length of VAR_START.
/** The length of {@link #VAR_START}. */
private static final int VAR_START_LENGTH = VAR_START.length();
The variable suffix.
/** The variable suffix. */
private static final String VAR_END = "}";
The length of VAR_END.
/** The length of {@link #VAR_END}. */
private static final int VAR_END_LENGTH = VAR_END.length();
A map containing the default prefix lookups.
/** A map containing the default prefix lookups. */
private static final Map<String, Lookup> DEFAULT_PREFIX_LOOKUPS; static { // TODO Perhaps a 3.0 version should only use Commons Text lookups. // Add our own lookups. final Map<String, Lookup> lookups = new HashMap<>(); for (final DefaultLookups lookup : DefaultLookups.values()) { lookups.put(lookup.getPrefix(), lookup.getLookup()); } // Add Apache Commons Text lookups but don't override existing keys. for (final DefaultStringLookup lookup : DefaultStringLookup.values()) { lookups.putIfAbsent(lookup.getKey(), new StringLookupAdapter(lookup.getStringLookup())); } DEFAULT_PREFIX_LOOKUPS = Collections.unmodifiableMap(lookups); }
A map with the currently registered lookup objects.
/** A map with the currently registered lookup objects. */
private final Map<String, Lookup> prefixLookups;
Stores the default lookup objects.
/** Stores the default lookup objects. */
private final List<Lookup> defaultLookups;
The helper object performing variable substitution.
/** The helper object performing variable substitution. */
private final StringSubstitutor substitutor;
Stores a parent interpolator objects if the interpolator is nested hierarchically.
/** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
private volatile ConfigurationInterpolator parentInterpolator;
Creates a new instance of ConfigurationInterpolator.
/** * Creates a new instance of {@code ConfigurationInterpolator}. */
public ConfigurationInterpolator() { prefixLookups = new ConcurrentHashMap<>(); defaultLookups = new CopyOnWriteArrayList<>(); substitutor = initSubstitutor(); }
Creates a new instance based on the properties in the given specification object.
Params:
  • spec – the InterpolatorSpecification
Returns:the newly created instance
/** * Creates a new instance based on the properties in the given specification * object. * * @param spec the {@code InterpolatorSpecification} * @return the newly created instance */
private static ConfigurationInterpolator createInterpolator( final InterpolatorSpecification spec) { final ConfigurationInterpolator ci = new ConfigurationInterpolator(); ci.addDefaultLookups(spec.getDefaultLookups()); ci.registerLookups(spec.getPrefixLookups()); ci.setParentInterpolator(spec.getParentInterpolator()); return ci; }
Extracts the variable name from a value that consists of a single variable.
Params:
  • strValue – the value
Returns:the extracted variable name
/** * Extracts the variable name from a value that consists of a single * variable. * * @param strValue the value * @return the extracted variable name */
private static String extractVariableName(final String strValue) { return strValue.substring(VAR_START_LENGTH, strValue.length() - VAR_END_LENGTH); }
Creates a new ConfigurationInterpolator instance based on the passed in specification object. If the InterpolatorSpecification already contains a ConfigurationInterpolator object, it is used directly. Otherwise, a new instance is created and initialized with the properties stored in the specification.
Params:
  • spec – the InterpolatorSpecification (must not be null)
Throws:
Returns:the ConfigurationInterpolator obtained or created based on the given specification
Since:2.0
/** * Creates a new {@code ConfigurationInterpolator} instance based on the * passed in specification object. If the {@code InterpolatorSpecification} * already contains a {@code ConfigurationInterpolator} object, it is used * directly. Otherwise, a new instance is created and initialized with the * properties stored in the specification. * * @param spec the {@code InterpolatorSpecification} (must not be * <b>null</b>) * @return the {@code ConfigurationInterpolator} obtained or created based * on the given specification * @throws IllegalArgumentException if the specification is <b>null</b> * @since 2.0 */
public static ConfigurationInterpolator fromSpecification( final InterpolatorSpecification spec) { if (spec == null) { throw new IllegalArgumentException( "InterpolatorSpecification must not be null!"); } return spec.getInterpolator() != null ? spec.getInterpolator() : createInterpolator(spec); }
Returns a map containing the default prefix lookups. Every configuration object derived from AbstractConfiguration is by default initialized with a ConfigurationInterpolator containing these Lookup objects and their prefixes. The map cannot be modified
Returns:a map with the default prefix Lookup objects and their prefixes
Since:2.0
/** * Returns a map containing the default prefix lookups. Every configuration * object derived from {@code AbstractConfiguration} is by default * initialized with a {@code ConfigurationInterpolator} containing these * {@code Lookup} objects and their prefixes. The map cannot be modified * * @return a map with the default prefix {@code Lookup} objects and their * prefixes * @since 2.0 */
public static Map<String, Lookup> getDefaultPrefixLookups() { return DEFAULT_PREFIX_LOOKUPS; }
Utility method for obtaining a Lookup object in a safe way. This method always returns a non-null Lookup object. If the passed in Lookup is not null, it is directly returned. Otherwise, result is a dummy Lookup which does not provide any values.
Params:
  • lookup – the Lookup to check
Returns:a non-null Lookup object
Since:2.0
/** * Utility method for obtaining a {@code Lookup} object in a safe way. This * method always returns a non-<b>null</b> {@code Lookup} object. If the * passed in {@code Lookup} is not <b>null</b>, it is directly returned. * Otherwise, result is a dummy {@code Lookup} which does not provide any * values. * * @param lookup the {@code Lookup} to check * @return a non-<b>null</b> {@code Lookup} object * @since 2.0 */
public static Lookup nullSafeLookup(Lookup lookup) { if (lookup == null) { lookup = DummyLookup.INSTANCE; } return lookup; }
Adds a default Lookup object. Default Lookup objects are queried (in the order they were added) for all variables without a special prefix. If no default Lookup objects are present, such variables won't be processed.
Params:
  • defaultLookup – the default Lookup object to be added (must not be null)
Throws:
/** * Adds a default {@code Lookup} object. Default {@code Lookup} objects are * queried (in the order they were added) for all variables without a * special prefix. If no default {@code Lookup} objects are present, such * variables won't be processed. * * @param defaultLookup the default {@code Lookup} object to be added (must * not be <b>null</b>) * @throws IllegalArgumentException if the {@code Lookup} object is * <b>null</b> */
public void addDefaultLookup(final Lookup defaultLookup) { defaultLookups.add(defaultLookup); }
Adds all Lookup objects in the given collection as default lookups. The collection can be null, then this method has no effect. It must not contain null entries.
Params:
  • lookups – the Lookup objects to be added as default lookups
Throws:
/** * Adds all {@code Lookup} objects in the given collection as default * lookups. The collection can be <b>null</b>, then this method has no * effect. It must not contain <b>null</b> entries. * * @param lookups the {@code Lookup} objects to be added as default lookups * @throws IllegalArgumentException if the collection contains a <b>null</b> * entry */
public void addDefaultLookups(final Collection<? extends Lookup> lookups) { if (lookups != null) { defaultLookups.addAll(lookups); } }
Deregisters the Lookup object for the specified prefix at this instance. It will be removed from this instance.
Params:
  • prefix – the variable prefix
Returns:a flag whether for this prefix a lookup object had been registered
/** * Deregisters the {@code Lookup} object for the specified prefix at this * instance. It will be removed from this instance. * * @param prefix the variable prefix * @return a flag whether for this prefix a lookup object had been * registered */
public boolean deregisterLookup(final String prefix) { return prefixLookups.remove(prefix) != null; }
Obtains the lookup object for the specified prefix. This method is called by the lookup() method. This implementation will check whether a lookup object is registered for the given prefix. If not, a null lookup object will be returned (never null).
Params:
  • prefix – the prefix
Returns:the lookup object to be used for this prefix
/** * Obtains the lookup object for the specified prefix. This method is called * by the {@code lookup()} method. This implementation will check * whether a lookup object is registered for the given prefix. If not, a * <b>null</b> lookup object will be returned (never <b>null</b>). * * @param prefix the prefix * @return the lookup object to be used for this prefix */
protected Lookup fetchLookupForPrefix(final String prefix) { return nullSafeLookup(prefixLookups.get(prefix)); }
Returns a collection with the default Lookup objects added to this ConfigurationInterpolator. These objects are not associated with a variable prefix. The returned list is a snapshot copy of the internal collection of default lookups; so manipulating it does not affect this instance.
Returns:the default lookup objects
/** * Returns a collection with the default {@code Lookup} objects * added to this {@code ConfigurationInterpolator}. These objects are not * associated with a variable prefix. The returned list is a snapshot copy * of the internal collection of default lookups; so manipulating it does * not affect this instance. * * @return the default lookup objects */
public List<Lookup> getDefaultLookups() { return new ArrayList<>(defaultLookups); }
Returns a map with the currently registered Lookup objects and their prefixes. This is a snapshot copy of the internally used map. So modifications of this map do not effect this instance.
Returns:a copy of the map with the currently registered Lookup objects
/** * Returns a map with the currently registered {@code Lookup} objects and * their prefixes. This is a snapshot copy of the internally used map. So * modifications of this map do not effect this instance. * * @return a copy of the map with the currently registered {@code Lookup} * objects */
public Map<String, Lookup> getLookups() { return new HashMap<>(prefixLookups); }
Returns the parent ConfigurationInterpolator.
Returns:the parent ConfigurationInterpolator (can be null)
/** * Returns the parent {@code ConfigurationInterpolator}. * * @return the parent {@code ConfigurationInterpolator} (can be <b>null</b>) */
public ConfigurationInterpolator getParentInterpolator() { return this.parentInterpolator; }
Creates and initializes a StringSubstitutor object which is used for variable substitution. This StringSubstitutor is assigned a specialized lookup object implementing the correct variable resolving algorithm.
Returns:the StringSubstitutor used by this object
/** * Creates and initializes a {@code StringSubstitutor} object which is used for * variable substitution. This {@code StringSubstitutor} is assigned a * specialized lookup object implementing the correct variable resolving * algorithm. * * @return the {@code StringSubstitutor} used by this object */
private StringSubstitutor initSubstitutor() { return new StringSubstitutor(key -> Objects.toString(resolve(key), null)); }
Performs interpolation of the passed in value. If the value is of type String, this method checks whether it contains variables. If so, all variables are replaced by their current values (if possible). For non string arguments, the value is returned without changes.
Params:
  • value – the value to be interpolated
Returns:the interpolated value
/** * Performs interpolation of the passed in value. If the value is of type * String, this method checks whether it contains variables. If so, all * variables are replaced by their current values (if possible). For non * string arguments, the value is returned without changes. * * @param value the value to be interpolated * @return the interpolated value */
public Object interpolate(final Object value) { if (value instanceof String) { final String strValue = (String) value; if (looksLikeSingleVariable(strValue)) { final Object resolvedValue = resolveSingleVariable(strValue); if (resolvedValue != null && !(resolvedValue instanceof String)) { // If the value is again a string, it needs no special // treatment; it may also contain further variables which // must be resolved; therefore, the default mechanism is // applied. return resolvedValue; } } return substitutor.replace(strValue); } return value; }
Sets a flag that variable names can contain other variables. If enabled, variable substitution is also done in variable names.
Returns:the substitution in variables flag
/** * Sets a flag that variable names can contain other variables. If enabled, * variable substitution is also done in variable names. * * @return the substitution in variables flag */
public boolean isEnableSubstitutionInVariables() { return substitutor.isEnableSubstitutionInVariables(); }
Checks whether a value to be interpolated seems to be a single variable. In this case, it is resolved directly without using the StringSubstitutor. Note that it is okay if this method returns a false positive: In this case, resolving is going to fail, and standard mechanism is used.
Params:
  • strValue – the value to be interpolated
Returns:a flag whether this value seems to be a single variable
/** * Checks whether a value to be interpolated seems to be a single variable. * In this case, it is resolved directly without using the * {@code StringSubstitutor}. Note that it is okay if this method returns a * false positive: In this case, resolving is going to fail, and standard * mechanism is used. * * @param strValue the value to be interpolated * @return a flag whether this value seems to be a single variable */
private boolean looksLikeSingleVariable(final String strValue) { return strValue.startsWith(VAR_START) && strValue.endsWith(VAR_END); }
Returns an unmodifiable set with the prefixes, for which Lookup objects are registered at this instance. This means that variables with these prefixes can be processed.
Returns:a set with the registered variable prefixes
/** * Returns an unmodifiable set with the prefixes, for which {@code Lookup} * objects are registered at this instance. This means that variables with * these prefixes can be processed. * * @return a set with the registered variable prefixes */
public Set<String> prefixSet() { return Collections.unmodifiableSet(prefixLookups.keySet()); }
Registers the given Lookup object for the specified prefix at this instance. From now on this lookup object will be used for variables that have the specified prefix.
Params:
  • prefix – the variable prefix (must not be null)
  • lookup – the Lookup object to be used for this prefix (must not be null)
Throws:
/** * Registers the given {@code Lookup} object for the specified prefix at * this instance. From now on this lookup object will be used for variables * that have the specified prefix. * * @param prefix the variable prefix (must not be <b>null</b>) * @param lookup the {@code Lookup} object to be used for this prefix (must * not be <b>null</b>) * @throws IllegalArgumentException if either the prefix or the * {@code Lookup} object is <b>null</b> */
public void registerLookup(final String prefix, final Lookup lookup) { if (prefix == null) { throw new IllegalArgumentException( "Prefix for lookup object must not be null!"); } if (lookup == null) { throw new IllegalArgumentException( "Lookup object must not be null!"); } prefixLookups.put(prefix, lookup); }
Registers all Lookup objects in the given map with their prefixes at this ConfigurationInterpolator. Using this method multiple Lookup objects can be registered at once. If the passed in map is null, this method does not have any effect.
Params:
  • lookups – the map with lookups to register (may be null)
Throws:
/** * Registers all {@code Lookup} objects in the given map with their prefixes * at this {@code ConfigurationInterpolator}. Using this method multiple * {@code Lookup} objects can be registered at once. If the passed in map is * <b>null</b>, this method does not have any effect. * * @param lookups the map with lookups to register (may be <b>null</b>) * @throws IllegalArgumentException if the map contains <b>entries</b> */
public void registerLookups(final Map<String, ? extends Lookup> lookups) { if (lookups != null) { prefixLookups.putAll(lookups); } }
Removes the specified Lookup object from the list of default Lookups.
Params:
  • lookup – the Lookup object to be removed
Returns:a flag whether this Lookup object actually existed and was removed
/** * Removes the specified {@code Lookup} object from the list of default * {@code Lookup}s. * * @param lookup the {@code Lookup} object to be removed * @return a flag whether this {@code Lookup} object actually existed and * was removed */
public boolean removeDefaultLookup(final Lookup lookup) { return defaultLookups.remove(lookup); }
Resolves the specified variable. This implementation tries to extract a variable prefix from the given variable name (the first colon (':') is used as prefix separator). It then passes the name of the variable with the prefix stripped to the lookup object registered for this prefix. If no prefix can be found or if the associated lookup object cannot resolve this variable, the default lookup objects are used. If this is not successful either and a parent ConfigurationInterpolator is available, this object is asked to resolve the variable.
Params:
  • var – the name of the variable whose value is to be looked up which may contain a prefix.
Returns:the value of this variable or null if it cannot be resolved
/** * Resolves the specified variable. This implementation tries to extract * a variable prefix from the given variable name (the first colon (':') is * used as prefix separator). It then passes the name of the variable with * the prefix stripped to the lookup object registered for this prefix. If * no prefix can be found or if the associated lookup object cannot resolve * this variable, the default lookup objects are used. If this is not * successful either and a parent {@code ConfigurationInterpolator} is * available, this object is asked to resolve the variable. * * @param var the name of the variable whose value is to be looked up which may contain a prefix. * @return the value of this variable or <b>null</b> if it cannot be * resolved */
public Object resolve(final String var) { if (var == null) { return null; } final int prefixPos = var.indexOf(PREFIX_SEPARATOR); if (prefixPos >= 0) { final String prefix = var.substring(0, prefixPos); final String name = var.substring(prefixPos + 1); final Object value = fetchLookupForPrefix(prefix).lookup(name); if (value != null) { return value; } } for (final Lookup lookup : defaultLookups) { final Object value = lookup.lookup(var); if (value != null) { return value; } } final ConfigurationInterpolator parent = getParentInterpolator(); if (parent != null) { return getParentInterpolator().resolve(var); } return null; }
Interpolates a string value that seems to be a single variable.
Params:
  • strValue – the string to be interpolated
Returns:the resolved value or null if resolving failed
/** * Interpolates a string value that seems to be a single variable. * * @param strValue the string to be interpolated * @return the resolved value or <b>null</b> if resolving failed */
private Object resolveSingleVariable(final String strValue) { return resolve(extractVariableName(strValue)); }
Sets the flag whether variable names can contain other variables. This flag corresponds to the enableSubstitutionInVariables property of the underlying StringSubstitutor object.
Params:
  • f – the new value of the flag
/** * Sets the flag whether variable names can contain other variables. This * flag corresponds to the {@code enableSubstitutionInVariables} property of * the underlying {@code StringSubstitutor} object. * * @param f the new value of the flag */
public void setEnableSubstitutionInVariables(final boolean f) { substitutor.setEnableSubstitutionInVariables(f); }
Sets the parent ConfigurationInterpolator. This object is used if the Lookup objects registered at this object cannot resolve a variable.
Params:
  • parentInterpolator – the parent ConfigurationInterpolator object (can be null)
/** * Sets the parent {@code ConfigurationInterpolator}. This object is used if * the {@code Lookup} objects registered at this object cannot resolve a * variable. * * @param parentInterpolator the parent {@code ConfigurationInterpolator} * object (can be <b>null</b>) */
public void setParentInterpolator( final ConfigurationInterpolator parentInterpolator) { this.parentInterpolator = parentInterpolator; } }