diff --git a/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java b/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java index f472087ea..b836c7df0 100644 --- a/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java +++ b/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java @@ -86,6 +86,16 @@ import java.util.Properties; *
  *   The variable $${${name}} must be used.
  * 
+ *

+ * In some complex scenarios you might even want to perform substitution in the + * names of variables, for instance + *

+ * ${jre-${java.specification.version}}
+ * 
+ * StrSubstitutor supports this recursive substitution in variable + * names, but it has to be enabled explicitly by setting the + * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables} + * property to true. * * @author Apache Software Foundation * @version $Id$ @@ -122,6 +132,10 @@ public class StrSubstitutor { * Variable resolution is delegated to an implementor of VariableResolver. */ private StrLookup variableResolver; + /** + * The flag whether substitution in variable names is enabled. + */ + private boolean enableSubstitutionInVariables; //----------------------------------------------------------------------- /** @@ -571,7 +585,8 @@ public class StrSubstitutor { int bufEnd = offset + length; int pos = offset; while (pos < bufEnd) { - int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd); + int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, + bufEnd); if (startMatchLen == 0) { pos++; } else { @@ -579,7 +594,7 @@ public class StrSubstitutor { if (pos > offset && chars[pos - 1] == escape) { // escaped buf.deleteCharAt(pos - 1); - chars = buf.buffer; // in case buffer was altered + chars = buf.buffer; // in case buffer was altered lengthChange--; altered = true; bufEnd--; @@ -588,45 +603,73 @@ public class StrSubstitutor { int startPos = pos; pos += startMatchLen; int endMatchLen = 0; + int nestedVarCount = 0; while (pos < bufEnd) { - endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd); + if (isEnableSubstitutionInVariables() + && (endMatchLen = prefixMatcher.isMatch(chars, + pos, offset, bufEnd)) != 0) { + // found a nested variable start + nestedVarCount++; + pos += endMatchLen; + continue; + } + + endMatchLen = suffixMatcher.isMatch(chars, pos, offset, + bufEnd); if (endMatchLen == 0) { pos++; } else { // found variable end marker - String varName = new String(chars, startPos + startMatchLen, - pos - startPos - startMatchLen); - pos += endMatchLen; - int endPos = pos; + if (nestedVarCount == 0) { + String varName = new String(chars, startPos + + startMatchLen, pos - startPos + - startMatchLen); + if (isEnableSubstitutionInVariables()) { + StrBuilder bufName = new StrBuilder(varName); + substitute(bufName, 0, bufName.length()); + varName = bufName.toString(); + } + pos += endMatchLen; + int endPos = pos; - // on the first call initialize priorVariables - if (priorVariables == null) { - priorVariables = new ArrayList(); - priorVariables.add(new String(chars, offset, length)); + // on the first call initialize priorVariables + if (priorVariables == null) { + priorVariables = new ArrayList(); + priorVariables.add(new String(chars, + offset, length)); + } + + // handle cyclic substitution + checkCyclicSubstitution(varName, priorVariables); + priorVariables.add(varName); + + // resolve the variable + String varValue = resolveVariable(varName, buf, + startPos, endPos); + if (varValue != null) { + // recursive replace + int varLen = varValue.length(); + buf.replace(startPos, endPos, varValue); + altered = true; + int change = substitute(buf, startPos, + varLen, priorVariables); + change = change + + (varLen - (endPos - startPos)); + pos += change; + bufEnd += change; + lengthChange += change; + chars = buf.buffer; // in case buffer was + // altered + } + + // remove variable from the cyclic stack + priorVariables + .remove(priorVariables.size() - 1); + break; + } else { + nestedVarCount--; + pos += endMatchLen; } - - // handle cyclic substitution - checkCyclicSubstitution(varName, priorVariables); - priorVariables.add(varName); - - // resolve the variable - String varValue = resolveVariable(varName, buf, startPos, endPos); - if (varValue != null) { - // recursive replace - int varLen = varValue.length(); - buf.replace(startPos, endPos, varValue); - altered = true; - int change = substitute(buf, startPos, varLen, priorVariables); - change = change + (varLen - (endPos - startPos)); - pos += change; - bufEnd += change; - lengthChange += change; - chars = buf.buffer; // in case buffer was altered - } - - // remove variable from the cyclic stack - priorVariables.remove(priorVariables.size() - 1); - break; } } } @@ -740,7 +783,7 @@ public class StrSubstitutor { /** * Sets the variable prefix to use. *

- * The variable prefix is the characer or characters that identify the + * The variable prefix is the character or characters that identify the * start of a variable. This method allows a single character prefix to * be easily set. * @@ -819,7 +862,7 @@ public class StrSubstitutor { /** * Sets the variable suffix to use. *

- * The variable suffix is the characer or characters that identify the + * The variable suffix is the character or characters that identify the * end of a variable. This method allows a string suffix to be easily set. * * @param suffix the suffix for variables, not null @@ -853,4 +896,29 @@ public class StrSubstitutor { this.variableResolver = variableResolver; } + // Substitution support in variable names + //----------------------------------------------------------------------- + /** + * Returns a flag whether substitution is done in variable names. + * + * @return the substitution in variable names flag + * @since 3.0 + */ + public boolean isEnableSubstitutionInVariables() { + return enableSubstitutionInVariables; + } + + /** + * Sets a flag whether substitution is done in variable names. If set to + * true, the names of variables can contain other variables which are + * processed first before the original variable is evaluated, e.g. + * ${jre-${java.version}}. The default value is false. + * + * @param enableSubstitutionInVariables the new value of the flag + * @since 3.0 + */ + public void setEnableSubstitutionInVariables( + boolean enableSubstitutionInVariables) { + this.enableSubstitutionInVariables = enableSubstitutionInVariables; + } } diff --git a/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java b/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java index edb15d65d..4b2a62e46 100644 --- a/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java +++ b/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java @@ -5,9 +5,9 @@ * 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. @@ -27,7 +27,7 @@ import org.apache.commons.lang3.mutable.MutableObject; /** * Test class for StrSubstitutor. - * + * * @author Oliver Heger * @version $Id$ */ @@ -255,6 +255,57 @@ public class StrSubstitutorTest extends TestCase { assertEquals("${animal} jumps", sub.replace("The ${animal} jumps over the ${target}.", 4, 15)); } + /** + * Tests whether a variable can be replaced in a variable name. + */ + public void testReplaceInVariable() { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + StrSubstitutor sub = new StrSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEquals( + "Wrong result (1)", + "The mouse jumps over the lazy dog.", + sub.replace("The ${animal.${species}} jumps over the ${target}.")); + values.put("species", "1"); + assertEquals( + "Wrong result (2)", + "The fox jumps over the lazy dog.", + sub.replace("The ${animal.${species}} jumps over the ${target}.")); + } + + /** + * Tests whether substitution in variable names is disabled per default. + */ + public void testReplaceInVariableDisabled() { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + StrSubstitutor sub = new StrSubstitutor(values); + assertEquals( + "Wrong result", + "The ${animal.${species}} jumps over the lazy dog.", + sub.replace("The ${animal.${species}} jumps over the ${target}.")); + } + + /** + * Tests complex and recursive substitution in variable names. + */ + public void testReplaceInVariableRecursive() { + values.put("animal.2", "brown fox"); + values.put("animal.1", "white mouse"); + values.put("color", "white"); + values.put("species.white", "1"); + values.put("species.brown", "2"); + StrSubstitutor sub = new StrSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEquals( + "Wrong result", + "The white mouse jumps over the lazy dog.", + sub.replace("The ${animal.${species.${color}}} jumps over the ${target}.")); + } + //----------------------------------------------------------------------- /** * Tests protected. @@ -325,7 +376,7 @@ public class StrSubstitutorTest extends TestCase { assertEquals(true, sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher); sub.setVariablePrefix('<'); assertEquals(true, sub.getVariablePrefixMatcher() instanceof StrMatcher.CharMatcher); - + sub.setVariablePrefix("<<"); assertEquals(true, sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher); try { @@ -335,7 +386,7 @@ public class StrSubstitutorTest extends TestCase { // expected } assertEquals(true, sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher); - + StrMatcher matcher = StrMatcher.commaMatcher(); sub.setVariablePrefixMatcher(matcher); assertSame(matcher, sub.getVariablePrefixMatcher()); @@ -356,7 +407,7 @@ public class StrSubstitutorTest extends TestCase { assertEquals(true, sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher); sub.setVariableSuffix('<'); assertEquals(true, sub.getVariableSuffixMatcher() instanceof StrMatcher.CharMatcher); - + sub.setVariableSuffix("<<"); assertEquals(true, sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher); try { @@ -366,7 +417,7 @@ public class StrSubstitutorTest extends TestCase { // expected } assertEquals(true, sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher); - + StrMatcher matcher = StrMatcher.commaMatcher(); sub.setVariableSuffixMatcher(matcher); assertSame(matcher, sub.getVariableSuffixMatcher()); @@ -412,7 +463,7 @@ public class StrSubstitutorTest extends TestCase { + "working with ${os.name}, your home " + "directory is ${user.home}.")); } - + /** * Test the replace of a properties object */ @@ -430,38 +481,38 @@ public class StrSubstitutorTest extends TestCase { private void doTestReplace(String expectedResult, String replaceTemplate, boolean substring) { String expectedShortResult = expectedResult.substring(1, expectedResult.length() - 1); StrSubstitutor sub = new StrSubstitutor(values); - + // replace using String assertEquals(expectedResult, sub.replace(replaceTemplate)); if (substring) { assertEquals(expectedShortResult, sub.replace(replaceTemplate, 1, replaceTemplate.length() - 2)); } - + // replace using char[] char[] chars = replaceTemplate.toCharArray(); assertEquals(expectedResult, sub.replace(chars)); if (substring) { assertEquals(expectedShortResult, sub.replace(chars, 1, chars.length - 2)); } - + // replace using StringBuffer StringBuffer buf = new StringBuffer(replaceTemplate); assertEquals(expectedResult, sub.replace(buf)); if (substring) { assertEquals(expectedShortResult, sub.replace(buf, 1, buf.length() - 2)); } - + // replace using StrBuilder StrBuilder bld = new StrBuilder(replaceTemplate); assertEquals(expectedResult, sub.replace(bld)); if (substring) { assertEquals(expectedShortResult, sub.replace(bld, 1, bld.length() - 2)); } - + // replace using object MutableObject obj = new MutableObject(replaceTemplate); // toString returns template assertEquals(expectedResult, sub.replace(obj)); - + // replace in StringBuffer buf = new StringBuffer(replaceTemplate); assertEquals(true, sub.replaceIn(buf)); @@ -471,7 +522,7 @@ public class StrSubstitutorTest extends TestCase { assertEquals(true, sub.replaceIn(buf, 1, buf.length() - 2)); assertEquals(expectedResult, buf.toString()); // expect full result as remainder is untouched } - + // replace in StrBuilder bld = new StrBuilder(replaceTemplate); assertEquals(true, sub.replaceIn(bld)); @@ -485,7 +536,7 @@ public class StrSubstitutorTest extends TestCase { private void doTestNoReplace(String replaceTemplate) { StrSubstitutor sub = new StrSubstitutor(values); - + if (replaceTemplate == null) { assertEquals(null, sub.replace((String) null)); assertEquals(null, sub.replace((String) null, 0, 100));