Skip to content
Permalink
Browse files
CONFIGURATION-753: making collection interpolation behavior consisten…
…t between ConfigurationInterpolator and DefaultConversionHandler; adding ConfigurationInterpolator.setStringConversion() method to allow configuration of string conversion behavior
  • Loading branch information
darkma773r committed May 17, 2022
1 parent c178d31 commit 454fa0ce34fee5f182e8dcd83af77947d5dfddaa
Showing 6 changed files with 343 additions and 17 deletions.
@@ -24,6 +24,11 @@
<release version="2.8.0" date="2020-MM-DD"
description="Minor release with new features and updated dependencies.">
<!-- FIX -->
<action issue="CONFIGURATION-753" type="fix" dev="mattjuntunen">
Make interpolation of collections and arrays in ConfigurationInterpolator consistent with
behavior of DefaultConversionHandler. Add ConfigurationInterpolator.setStringConverter to
allow customized string conversion behavior.
</action>
<action issue="CONFIGURATION-795" type="fix" dev="ggregory" due-to="dpeger">
Computation of blank lines after header comment #82.
</action>
@@ -16,16 +16,19 @@
*/
package org.apache.commons.configuration2.interpol;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
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 java.util.function.Function;

import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.DefaultStringLookup;
@@ -65,9 +68,22 @@
* 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><strong>String Conversion</strong></p>
* <p>
* Implementation node: This class is thread-safe. Lookup objects can be added or removed at any time concurrent to
* interpolation operations.
* When variables are part of larger interpolated strings, the variable values, which can be of any type, must be
* converted to strings to produce the full result. Each interpolator instance has a configurable
* {@link #setStringConverter() string converter} to perform this conversion. The default implementation of this
* function simply uses the value's {@code toString} method in the majority of cases. However, for maximum
* consistency with
* {@link org.apache.commons.configuration3convert.DefaultConversionHandler DefaultConversionHandler}, when a variable
* value is a container type (such as a collection or array), then only the first element of the container is converted
* to a string instead of the container itself. For example, if the variable {@code x} resolves to the integer array
* {@code [1, 2, 3]}, then the string <code>"my value = ${x}"</code> will by default be interpolated to
* {@code "my value = 1"}.
* </p>
* <p>
* <strong>Implementation note:</strong> This class is thread-safe. Lookup objects can be added or removed at any time
* concurrent to interpolation operations.
* </p>
*
* @since 1.4
@@ -117,6 +133,9 @@ public class ConfigurationInterpolator {
/** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
private volatile ConfigurationInterpolator parentInterpolator;

/** Function used to convert interpolated values to strings. */
private volatile Function<Object, String> stringConverter = DefaultStringConverter.INSTANCE;

/**
* Creates a new instance of {@code ConfigurationInterpolator}.
*/
@@ -137,6 +156,7 @@ private static ConfigurationInterpolator createInterpolator(final InterpolatorSp
ci.addDefaultLookups(spec.getDefaultLookups());
ci.registerLookups(spec.getPrefixLookups());
ci.setParentInterpolator(spec.getParentInterpolator());
ci.setStringConverter(spec.getStringConverter());
return ci;
}

@@ -281,13 +301,34 @@ public ConfigurationInterpolator getParentInterpolator() {
* @return the {@code StringSubstitutor} used by this object
*/
private StringSubstitutor initSubstitutor() {
return new StringSubstitutor(key -> Objects.toString(resolve(key), null));
return new StringSubstitutor(key -> {
final Object value = resolve(key);
return value != null
? stringConverter.apply(value)
: 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.
* Performs interpolation of the passed in value. If the value is of type {@code 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. In the special case where the value is a string
* consisting of a single variable reference, the interpolated variable value is <em>not</em> converted to a
* string before returning, so that callers can access the raw value. However, if the variable is part of a larger
* interpolated string, then the variable value is converted to a string using the configured
* {@link #getStringConverter() string converter}. (See the discussion on string conversion in the class
* documentation for more details.)
*
* <p><strong>Examples</strong></p>
* <p>
* For the following examples, assume that the default string conversion function is in place and that the
* variable {@code i} maps to the integer value {@code 42}.
* <pre>
* interpolator.interpolate(1) &rarr; 1 // non-string argument returned unchanged
* interpolator.interpolate("${i}") &rarr; 42 // single variable value returned with raw type
* interpolator.interpolate("answer = ${i}") &rarr; "answer = 42" // variable value converted to string
* </pre>
* </p>
*
* @param value the value to be interpolated
* @return the interpolated value
@@ -320,6 +361,24 @@ public boolean isEnableSubstitutionInVariables() {
return substitutor.isEnableSubstitutionInVariables();
}

/** Get the function used to convert interpolated values to strings.
* @return function used to convert interpolated values to strings
*/
public Function<Object, String> getStringConverter() {
return stringConverter;
}

/** Set the function used to convert interpolated values to strings. Pass
* {@code null} to use the default conversion function.
* @param stringConverter function used to convert interpolated values to strings
* or {@code null} to use the default conversion function
*/
public void setStringConverter(final Function<Object, String> stringConverter) {
this.stringConverter = stringConverter != null
? stringConverter
: DefaultStringConverter.INSTANCE;
}

/**
* 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
@@ -452,4 +511,51 @@ public void setEnableSubstitutionInVariables(final boolean f) {
public void setParentInterpolator(final ConfigurationInterpolator parentInterpolator) {
this.parentInterpolator = parentInterpolator;
}

/** Class encapsulating the default logic to convert resolved variable values into strings.
* This class is thread-safe.
*/
private static final class DefaultStringConverter implements Function<Object, String> {

/** Shared instance. */
static final DefaultStringConverter INSTANCE = new DefaultStringConverter();

/** {@inheritDoc} */
@Override
public String apply(final Object obj) {
return Objects.toString(extractSimpleValue(obj), null);
}

/** Attempt to extract a simple value from {@code obj} for use in string conversion.
* If the input represents a collection of some sort (e.g., an iterable or array),
* the first item from the collection is returned.
* @param obj input object
* @return extracted simple object
*/
private Object extractSimpleValue(final Object obj) {
if (!(obj instanceof String)) {
if (obj instanceof Iterable) {
return nextOrNull(((Iterable<?>) obj).iterator());
} else if (obj instanceof Iterator) {
return nextOrNull((Iterator<?>) obj);
} else if (obj.getClass().isArray()) {
return Array.getLength(obj) > 0
? Array.get(obj, 0)
: null;
}
}
return obj;
}

/** Return the next value from {@code it} or {@code null} if no values remain.
* @param <T> iterated type
* @param it iterator
* @return next value from {@code it} or {@code null} if no values remain
*/
private <T> T nextOrNull(final Iterator<T> it) {
return it.hasNext()
? it.next()
: null;
}
}
}
@@ -22,6 +22,7 @@
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.function.Function;

/**
* <p>
@@ -37,6 +38,7 @@
* <li>a map with {@code Lookup} objects associated with a specific prefix</li>
* <li>a collection with default {@code Lookup} objects (without a prefix)</li>
* <li>a parent {@code ConfigurationInterpolator}</li>
* <li>a function used to convert interpolated values into strings</li>
* </ul>
* <p>
* When setting up a configuration it is possible to define the {@code ConfigurationInterpolator} in terms of this
@@ -62,6 +64,9 @@ public final class InterpolatorSpecification {
/** The collection with default lookups. */
private final Collection<Lookup> defaultLookups;

/** Function used to convert interpolated values to strings. */
private final Function<Object, String> stringConverter;

/**
* Creates a new instance of {@code InterpolatorSpecification} with the properties defined by the given builder object.
*
@@ -72,6 +77,7 @@ private InterpolatorSpecification(final Builder builder) {
parentInterpolator = builder.parentInterpolator;
prefixLookups = Collections.unmodifiableMap(new HashMap<>(builder.prefixLookups));
defaultLookups = Collections.unmodifiableCollection(new ArrayList<>(builder.defLookups));
stringConverter = builder.stringConverter;
}

/**
@@ -111,6 +117,17 @@ public Collection<Lookup> getDefaultLookups() {
return defaultLookups;
}

/**
* Returns the function used to convert interpolated values to strings or {@code null}
* if the default conversion function is to be used.
*
* @return function used to convert interpolated values to strings or {@code null} if
* the default conversion function is to be used
*/
public Function<Object, String> getStringConverter() {
return stringConverter;
}

/**
* <p>
* A <em>builder</em> class for creating instances of {@code InterpolatorSpecification}.
@@ -133,6 +150,9 @@ public static class Builder {
/** The parent {@code ConfigurationInterpolator}. */
private ConfigurationInterpolator parentInterpolator;

/** Function used to convert interpolated values to strings. */
private Function<Object, String> stringConverter;

public Builder() {
prefixLookups = new HashMap<>();
defLookups = new LinkedList<>();
@@ -226,6 +246,20 @@ public Builder withParentInterpolator(final ConfigurationInterpolator parent) {
return this;
}

/**
* Sets the function used to convert interpolated values to strings. Pass {@code null}
* if the default conversion function is to be used.
*
* @param fn function used to convert interpolated values to string or {@code null} if the
* default conversion function is to be used
* @return a reference to this builder for method chaining
*/
public Builder withStringConverter(final Function<Object, String> fn) {
this.stringConverter = fn;
return this;
}


/**
* Creates a new {@code InterpolatorSpecification} instance with the properties set so far. After that this builder
* instance is reset so that it can be reused for creating further specification objects.
@@ -247,6 +281,7 @@ public void reset() {
parentInterpolator = null;
prefixLookups.clear();
defLookups.clear();
stringConverter = null;
}

/**
@@ -200,7 +200,7 @@ private void checkGetStringArrayScalar(final Object value) {
final BaseConfiguration config = new BaseConfiguration();
config.addProperty(KEY_PREFIX, value);
final String[] array = config.getStringArray(KEY_PREFIX);
assertEquals("Weong number of elements", 1, array.length);
assertEquals("Wrong number of elements", 1, array.length);
assertEquals("Wrong value", value.toString(), array[0]);
}

@@ -858,6 +858,21 @@ public void testInterpolateString() {
assertEquals("Wrong interpolation", "The quick brown fox jumps over the lazy dog.", config.getString(KEY_PREFIX));
}

/**
* Tests that variables with list values in interpolated string are resolved with the first element
* in the list.
*/
@Test
public void testInterpolateStringWithListVariable() {
final PropertiesConfiguration config = new PropertiesConfiguration();
final List<String> values = Arrays.asList("some", "test", "values");
final String keyList = "testList";
config.addProperty(keyList, values);
config.addProperty(KEY_PREFIX, "result = ${" + keyList + "}");

assertEquals("Wrong interpolation", "result = some", config.getString(KEY_PREFIX));
}

/**
* Tests interpolate() if the configuration does not have a {@code ConfigurationInterpolator}.
*/

0 comments on commit 454fa0c

Please sign in to comment.