[FELIX-4708] Provide more substitution options
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1640968 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java b/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
index 9eef1ca..a5ff2a6 100644
--- a/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
+++ b/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
@@ -37,6 +37,7 @@
private static final char ESCAPE_CHAR = '\\';
private static final String DELIM_START = "${";
private static final String DELIM_STOP = "}";
+ private static final String MARKER = "$__";
/**
@@ -75,11 +76,27 @@
*/
public static void performSubstitution(Map<String,String> properties, SubstitutionCallback callback)
{
+ performSubstitution(properties, callback, true, true, true);
+ }
+
+ /**
+ * Perform substitution on a property set
+ *
+ * @param properties the property set to perform substitution on
+ * @param callback the callback to obtain substitution values
+ * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
+ */
+ public static void performSubstitution(Map<String,String> properties,
+ SubstitutionCallback callback,
+ boolean substituteFromConfig,
+ boolean substituteFromSystemProperties,
+ boolean defaultsToEmptyString)
+ {
Map<String, String> org = new HashMap<String, String>(properties);
for (String name : properties.keySet())
{
String value = properties.get(name);
- properties.put(name, substVars(value, name, null, org, callback));
+ properties.put(name, substVars(value, name, null, org, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString));
}
}
@@ -177,14 +194,54 @@
SubstitutionCallback callback)
throws IllegalArgumentException
{
- return unescape(doSubstVars(val, currentKey, cycleMap, configProps, callback));
+ return substVars(val, currentKey, cycleMap, configProps, callback, true, true, true);
}
- private static String doSubstVars(String val,
+ /**
+ * <p>
+ * This method performs property variable substitution on the
+ * specified value. If the specified value contains the syntax
+ * <tt>${<prop-name>}</tt>, where <tt><prop-name></tt>
+ * refers to either a configuration property or a system property,
+ * then the corresponding property value is substituted for the variable
+ * placeholder. Multiple variable placeholders may exist in the
+ * specified value as well as nested variable placeholders, which
+ * are substituted from inner most to outer most. Configuration
+ * properties override system properties.
+ * </p>
+ *
+ * @param val The string on which to perform property substitution.
+ * @param currentKey The key of the property being evaluated used to
+ * detect cycles.
+ * @param cycleMap Map of variable references used to detect nested cycles.
+ * @param configProps Set of configuration properties.
+ * @param callback the callback to obtain substitution values
+ * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise
+ * @return The value of the specified string after system property substitution.
+ * @throws IllegalArgumentException If there was a syntax error in the
+ * property placeholder syntax or a recursive variable reference.
+ **/
+ public static String substVars(String val,
String currentKey,
Map<String,String> cycleMap,
Map<String,String> configProps,
- SubstitutionCallback callback)
+ SubstitutionCallback callback,
+ boolean substituteFromConfig,
+ boolean substituteFromSystemProperties,
+ boolean defaultsToEmptyString)
+ throws IllegalArgumentException
+ {
+ return unescape(doSubstVars(val, currentKey, cycleMap, configProps, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString));
+ }
+
+ private static String doSubstVars(String val,
+ String currentKey,
+ Map<String,String> cycleMap,
+ Map<String,String> configProps,
+ SubstitutionCallback callback,
+ boolean substituteFromConfig,
+ boolean substituteFromSystemProperties,
+ boolean defaultsToEmptyString)
throws IllegalArgumentException
{
if (cycleMap == null)
@@ -201,28 +258,34 @@
// Find the first ending '}' variable delimiter, which
// will correspond to the first deepest nested variable
// placeholder.
- int stopDelim = val.indexOf(DELIM_STOP);
- while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR)
+ int startDelim;
+ int stopDelim = -1;
+ do
{
stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
- }
+ while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR)
+ {
+ stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
+ }
- // Find the matching starting "${" variable delimiter
- // by looping until we find a start delimiter that is
- // greater than the stop delimiter we have found.
- int startDelim = val.indexOf(DELIM_START);
- while (stopDelim >= 0)
- {
- int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
- if ((idx < 0) || (idx > stopDelim))
+ // Find the matching starting "${" variable delimiter
+ // by looping until we find a start delimiter that is
+ // greater than the stop delimiter we have found.
+ startDelim = val.indexOf(DELIM_START);
+ while (stopDelim >= 0)
{
- break;
- }
- else if (idx < stopDelim)
- {
- startDelim = idx;
+ int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+ if ((idx < 0) || (idx > stopDelim))
+ {
+ break;
+ }
+ else if (idx < stopDelim)
+ {
+ startDelim = idx;
+ }
}
}
+ while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length());
// If we do not have a start or stop delimiter, then just
// return the existing value.
@@ -243,9 +306,13 @@
throw new IllegalArgumentException("recursive variable reference: " + variable);
}
+ String substValue = null;
// Get the value of the deepest nested variable placeholder.
// Try to configuration properties first.
- String substValue = (String) ((configProps != null) ? configProps.get(variable) : null);
+ if (substituteFromConfig && configProps != null)
+ {
+ substValue = configProps.get(variable);
+ }
if (substValue == null)
{
if (variable.length() <= 0)
@@ -258,9 +325,22 @@
{
substValue = callback.getValue(variable);
}
+ if (substValue == null && substituteFromSystemProperties)
+ {
+ substValue = System.getProperty(variable);
+ }
if (substValue == null)
{
- substValue = System.getProperty(variable, "");
+ if (defaultsToEmptyString)
+ {
+ substValue = "";
+ }
+ else
+ {
+ // alters the original token to avoid infinite recursion
+ // altered tokens are reverted in substVarsPreserveUnresolved()
+ substValue = MARKER + "{" + variable + "}";
+ }
}
}
}
@@ -277,7 +357,7 @@
// Now perform substitution again, since there could still
// be substitutions to make.
- val = doSubstVars(val, currentKey, cycleMap, configProps, callback);
+ val = doSubstVars(val, currentKey, cycleMap, configProps, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString);
// Return the value.
return val;
@@ -285,6 +365,7 @@
private static String unescape(String val)
{
+ val = val.replaceAll("\\" + MARKER, "\\$");
int escape = val.indexOf(ESCAPE_CHAR);
while (escape >= 0 && escape < val.length() - 1)
{
diff --git a/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java b/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
index 7dc21d2..fd773d0 100644
--- a/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
+++ b/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
@@ -17,10 +17,15 @@
package org.apache.felix.utils.properties;
import junit.framework.TestCase;
+import org.junit.Test;
import java.util.Enumeration;
+import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
public class InterpolationHelperTest extends TestCase {
@@ -36,7 +41,7 @@
System.setProperty("value1", "sub_value1");
try
{
- Hashtable props = new Hashtable();
+ Hashtable<String, String> props = new Hashtable<String, String>();
props.put("key0", "value0");
props.put("key1", "${value1}");
props.put("key2", "${value2}");
@@ -44,7 +49,7 @@
for (Enumeration e = props.keys(); e.hasMoreElements();)
{
String name = (String) e.nextElement();
- props.put(name, InterpolationHelper.substVars((String) props.get(name), name, null, props, context));
+ props.put(name, InterpolationHelper.substVars(props.get(name), name, null, props, context));
}
assertEquals("value0", props.get("key0"));
@@ -68,7 +73,7 @@
context.setProperty("value3", "context_value1");
context.setProperty("value2", "context_value2");
- Hashtable props = new Hashtable();
+ Hashtable<String, String> props = new Hashtable<String, String>();
props.put("key0", "value0");
props.put("key1", "${value1}");
props.put("key2", "${value2}");
@@ -78,7 +83,7 @@
{
String name = (String) e.nextElement();
props.put(name,
- InterpolationHelper.substVars((String) props.get(name), name, null, props, context));
+ InterpolationHelper.substVars(props.get(name), name, null, props, context));
}
assertEquals("value0", props.get("key0"));
@@ -96,16 +101,16 @@
public void testSubstitutionFailures()
{
- assertEquals("a}", InterpolationHelper.substVars("a}", "b", null, new Hashtable(), context));
- assertEquals("${a", InterpolationHelper.substVars("${a", "b", null, new Hashtable(), context));
+ assertEquals("a}", InterpolationHelper.substVars("a}", "b", null, new Hashtable<String, String>(), context));
+ assertEquals("${a", InterpolationHelper.substVars("${a", "b", null, new Hashtable<String, String>(), context));
}
public void testEmptyVariable() {
- assertEquals("", InterpolationHelper.substVars("${}", "b", null, new Hashtable(), context));
+ assertEquals("", InterpolationHelper.substVars("${}", "b", null, new Hashtable<String, String>(), context));
}
public void testInnerSubst() {
- Hashtable props = new Hashtable();
+ Hashtable<String, String> props = new Hashtable<String, String>();
props.put("a", "b");
props.put("b", "c");
assertEquals("c", InterpolationHelper.substVars("${${a}}", "z", null, props, context));
@@ -113,7 +118,7 @@
public void testSubstLoop() {
try {
- InterpolationHelper.substVars("${a}", "a", null, new Hashtable(), context);
+ InterpolationHelper.substVars("${a}", "a", null, new Hashtable<String, String>(), context);
fail("Should have thrown an exception");
} catch (IllegalArgumentException e) {
// expected
@@ -122,9 +127,9 @@
public void testSubstitutionEscape()
{
- assertEquals("${a}", InterpolationHelper.substVars("$\\{a${#}\\}", "b", null, new Hashtable(), context));
- assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}${#}", "b", null, new Hashtable(), context));
- assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}", "b", null, new Hashtable(), context));
+ assertEquals("${a}", InterpolationHelper.substVars("$\\{a${#}\\}", "b", null, new Hashtable<String, String>(), context));
+ assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}${#}", "b", null, new Hashtable<String, String>(), context));
+ assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}", "b", null, new Hashtable<String, String>(), context));
}
public void testSubstitutionOrder()
@@ -157,4 +162,18 @@
assertEquals("$\\{var}bc", map1.get("abc"));
}
+ @Test
+ public void testPreserveUnresolved() {
+ Map<String, String> props = new HashMap<String, String>();
+ props.put("a", "${b}");
+ assertEquals("", InterpolationHelper.substVars("${b}", "a", null, props, null, true, false, true));
+ assertEquals("${b}", InterpolationHelper.substVars("${b}", "a", null, props, null, true, false, false));
+
+ props.put("b", "c");
+ assertEquals("c", InterpolationHelper.substVars("${b}", "a", null, props, null, true, false, true));
+ assertEquals("c", InterpolationHelper.substVars("${b}", "a", null, props, null, true, false, false));
+
+ props.put("c", "${d}${d}");
+ assertEquals("${d}${d}", InterpolationHelper.substVars("${d}${d}", "c", null, props, null, false, false, false));
+ }
}