[LANG-893] StrSubstitutor now supports the declaration of default values for the variables to be replaced. Thanks to Woonsan Ko for the patch.

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@1524541 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oliver Heger 2013-09-18 19:35:16 +00:00
parent 11def3cc7e
commit abc5dda962
2 changed files with 280 additions and 16 deletions

View File

@ -59,6 +59,26 @@ import java.util.Properties;
* The quick brown fox jumped over the lazy dog.
* </pre>
* <p>
* Also, this class allows to set a default value for unresolved variables.
* The default value for a variable can be appended to the variable name after the variable
* default value delimiter. The default value of the variable default value delimiter is ':-',
* as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
* The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
* {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
* The following shows an example with varialbe default value settings:
* <pre>
* Map valuesMap = HashMap();
* valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
* valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
* String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
* StrSubstitutor sub = new StrSubstitutor(valuesMap);
* String resolvedString = sub.replace(templateString);
* </pre>
* yielding:
* <pre>
* The quick brown fox jumped over the lazy dog. 1234567890.
* </pre>
* <p>
* In addition to this usage pattern there are some static convenience methods that
* cover the most common use cases. These methods can be used without the need of
* manually creating an instance. However if multiple replace operations are to be
@ -114,6 +134,10 @@ public class StrSubstitutor {
* Constant for the default variable suffix.
*/
public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
/**
* Constant for the default value delimiter of a variable.
*/
public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
/**
* Stores the escape character.
@ -127,6 +151,10 @@ public class StrSubstitutor {
* Stores the variable suffix.
*/
private StrMatcher suffixMatcher;
/**
* Stores the default variable value delimiter
*/
private StrMatcher valueDelimiterMatcher;
/**
* Variable resolution is delegated to an implementor of VariableResolver.
*/
@ -247,6 +275,21 @@ public class StrSubstitutor {
this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
}
/**
* Creates a new instance and initializes it.
*
* @param <V> the type of the values in the map
* @param valueMap the map with the variables' values, may be null
* @param prefix the prefix for variables, not null
* @param suffix the suffix for variables, not null
* @param escape the escape character
* @param valueDelimiter the variable default value delimiter, may be null
* @throws IllegalArgumentException if the prefix or suffix is null
*/
public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix, final char escape, final String valueDelimiter) {
this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
}
/**
* Creates a new instance and initializes it.
*
@ -270,6 +313,25 @@ public class StrSubstitutor {
this.setVariablePrefix(prefix);
this.setVariableSuffix(suffix);
this.setEscapeChar(escape);
this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
}
/**
* Creates a new instance and initializes it.
*
* @param variableResolver the variable resolver, may be null
* @param prefix the prefix for variables, not null
* @param suffix the suffix for variables, not null
* @param escape the escape character
* @param valueDelimiter the variable default value delimiter string, may be null
* @throws IllegalArgumentException if the prefix or suffix is null
*/
public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix, final char escape, final String valueDelimiter) {
this.setVariableResolver(variableResolver);
this.setVariablePrefix(prefix);
this.setVariableSuffix(suffix);
this.setEscapeChar(escape);
this.setValueDelimiter(valueDelimiter);
}
/**
@ -283,10 +345,26 @@ public class StrSubstitutor {
*/
public StrSubstitutor(
final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape) {
this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
}
/**
* Creates a new instance and initializes it.
*
* @param variableResolver the variable resolver, may be null
* @param prefixMatcher the prefix for variables, not null
* @param suffixMatcher the suffix for variables, not null
* @param escape the escape character
* @param valueDelimiterMatcher the variable default value delimiter matcher, may be null
* @throws IllegalArgumentException if the prefix or suffix is null
*/
public StrSubstitutor(
final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher) {
this.setVariableResolver(variableResolver);
this.setVariablePrefixMatcher(prefixMatcher);
this.setVariableSuffixMatcher(suffixMatcher);
this.setEscapeChar(escape);
this.setValueDelimiterMatcher(valueDelimiterMatcher);
}
//-----------------------------------------------------------------------
@ -410,7 +488,7 @@ public class StrSubstitutor {
substitute(buf, 0, length);
return buf.toString();
}
/**
* Replaces all the occurrences of variables with their matching values
* from the resolver using the given source as a template.
@ -591,7 +669,7 @@ public class StrSubstitutor {
source.replace(offset, offset + length, buf.toString());
return true;
}
//-----------------------------------------------------------------------
/**
* Replaces all the occurrences of variables within the given source
@ -661,6 +739,8 @@ public class StrSubstitutor {
final StrMatcher prefixMatcher = getVariablePrefixMatcher();
final StrMatcher suffixMatcher = getVariableSuffixMatcher();
final char escape = getEscapeChar();
final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
final boolean top = priorVariables == null;
boolean altered = false;
@ -689,7 +769,7 @@ public class StrSubstitutor {
int endMatchLen = 0;
int nestedVarCount = 0;
while (pos < bufEnd) {
if (isEnableSubstitutionInVariables()
if (substitutionInVariablesEnabled
&& (endMatchLen = prefixMatcher.isMatch(chars,
pos, offset, bufEnd)) != 0) {
// found a nested variable start
@ -705,17 +785,37 @@ public class StrSubstitutor {
} else {
// found variable end marker
if (nestedVarCount == 0) {
String varName = new String(chars, startPos
String varNameExpr = new String(chars, startPos
+ startMatchLen, pos - startPos
- startMatchLen);
if (isEnableSubstitutionInVariables()) {
final StrBuilder bufName = new StrBuilder(varName);
if (substitutionInVariablesEnabled) {
final StrBuilder bufName = new StrBuilder(varNameExpr);
substitute(bufName, 0, bufName.length());
varName = bufName.toString();
varNameExpr = bufName.toString();
}
pos += endMatchLen;
final int endPos = pos;
String varName = varNameExpr;
String varDefaultValue = null;
if (valueDelimiterMatcher != null) {
final char [] varNameExprChars = varNameExpr.toCharArray();
int valueDelimiterMatchLen = 0;
for (int i = 0; i < varNameExprChars.length; i++) {
// if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
if (!substitutionInVariablesEnabled
&& prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
break;
}
if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
varName = varNameExpr.substring(0, i);
varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
break;
}
}
}
// on the first call initialize priorVariables
if (priorVariables == null) {
priorVariables = new ArrayList<String>();
@ -728,8 +828,11 @@ public class StrSubstitutor {
priorVariables.add(varName);
// resolve the variable
final String varValue = resolveVariable(varName, buf,
String varValue = resolveVariable(varName, buf,
startPos, endPos);
if (varValue == null) {
varValue = varDefaultValue;
}
if (varValue != null) {
// recursive replace
final int varLen = varValue.length();
@ -960,6 +1063,76 @@ public class StrSubstitutor {
return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
}
// Variable Default Value Delimiter
//-----------------------------------------------------------------------
/**
* Gets the variable default value delimiter matcher currently in use.
* <p>
* The variable default value delimiter is the characer or characters that delimite the
* variable name and the variable default value. This delimiter is expressed in terms of a matcher
* allowing advanced variable default value delimiter matches.
* <p>
* If it returns null, then the variable default value resolution is disabled.
*
* @return the variable default value delimiter matcher in use, may be null
*/
public StrMatcher getValueDelimiterMatcher() {
return valueDelimiterMatcher;
}
/**
* Sets the variable default value delimiter matcher to use.
* <p>
* The variable default value delimiter is the characer or characters that delimite the
* variable name and the variable default value. This delimiter is expressed in terms of a matcher
* allowing advanced variable default value delimiter matches.
* <p>
* If the <code>valueDelimiterMatcher</code> is null, then the variable default value resolution
* becomes disabled.
*
* @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null
* @return this, to enable chaining
*/
public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
this.valueDelimiterMatcher = valueDelimiterMatcher;
return this;
}
/**
* Sets the variable default value delimiter to use.
* <p>
* The variable default value delimiter is the characer or characters that delimite the
* variable name and the variable default value. This method allows a single character
* variable default value delimiter to be easily set.
*
* @param valueDelimiter the variable default value delimiter character to use
* @return this, to enable chaining
*/
public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
}
/**
* Sets the variable default value delimiter to use.
* <p>
* The variable default value delimiter is the characer or characters that delimite the
* variable name and the variable default value. This method allows a string
* variable default value delimiter to be easily set.
* <p>
* If the <code>valueDelimiter</code> is null or empty string, then the variable default
* value resolution becomes disabled.
*
* @param valueDelimiter the variable default value delimiter string to use, may be null or empty
* @return this, to enable chaining
*/
public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
if (valueDelimiter == null || valueDelimiter.length() == 0) {
setValueDelimiterMatcher(null);
return this;
}
return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
}
// Resolver
//-----------------------------------------------------------------------
/**

View File

@ -17,15 +17,21 @@
package org.apache.commons.lang3.text;
import org.junit.After;
import org.junit.Test;
import org.junit.Before;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.lang3.mutable.MutableObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Test class for StrSubstitutor.
@ -105,6 +111,7 @@ public class StrSubstitutorTest {
@Test
public void testReplaceUnknownKey() {
doTestReplace("The ${person} jumps over the lazy dog.", "The ${person} jumps over the ${target}.", true);
doTestReplace("The ${person} jumps over the lazy dog. 1234567890.", "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true);
}
/**
@ -143,6 +150,9 @@ public class StrSubstitutorTest {
values.put("critterColor", "brown");
values.put("critterType", "fox");
doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
values.put("pet", "${petCharacteristicUnknown:-lazy} dog");
doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
}
/**
@ -167,6 +177,7 @@ public class StrSubstitutorTest {
@Test
public void testReplaceComplexEscaping() {
doTestReplace("The ${quick brown fox} jumps over the lazy dog.", "The $${${animal}} jumps over the ${target}.", true);
doTestReplace("The ${quick brown fox} jumps over the lazy dog. ${1234567890}.", "The $${${animal}} jumps over the ${target}. $${${undefined.number:-1234567890}}.", true);
}
/**
@ -207,6 +218,7 @@ public class StrSubstitutorTest {
@Test
public void testReplaceEmptyKeys() {
doTestReplace("The ${} jumps over the lazy dog.", "The ${} jumps over the ${target}.", true);
doTestReplace("The animal jumps over the lazy dog.", "The ${:-animal} jumps over the ${target}.", true);
}
/**
@ -234,7 +246,17 @@ public class StrSubstitutorTest {
map.put("critterSpeed", "quick");
map.put("critterColor", "brown");
map.put("critterType", "${animal}");
final StrSubstitutor sub = new StrSubstitutor(map);
StrSubstitutor sub = new StrSubstitutor(map);
try {
sub.replace("The ${animal} jumps over the ${target}.");
fail("Cyclic replacement was not detected!");
} catch (final IllegalStateException ex) {
// expected
}
// also check even when default value is set.
map.put("critterType", "${animal:-fox}");
sub = new StrSubstitutor(map);
try {
sub.replace("The ${animal} jumps over the ${target}.");
fail("Cyclic replacement was not detected!");
@ -295,6 +317,10 @@ public class StrSubstitutorTest {
"Wrong result (2)",
"The fox jumps over the lazy dog.",
sub.replace("The ${animal.${species}} jumps over the ${target}."));
assertEquals(
"Wrong result (3)",
"The fox jumps over the lazy dog.",
sub.replace("The ${unknown.animal.${unknown.species:-1}:-fox} jumps over the ${unknow.target:-lazy dog}."));
}
/**
@ -307,9 +333,13 @@ public class StrSubstitutorTest {
values.put("species", "2");
final StrSubstitutor sub = new StrSubstitutor(values);
assertEquals(
"Wrong result",
"Wrong result (1)",
"The ${animal.${species}} jumps over the lazy dog.",
sub.replace("The ${animal.${species}} jumps over the ${target}."));
assertEquals(
"Wrong result (2)",
"The ${animal.${species:-1}} jumps over the lazy dog.",
sub.replace("The ${animal.${species:-1}} jumps over the ${target}."));
}
/**
@ -325,9 +355,46 @@ public class StrSubstitutorTest {
final StrSubstitutor sub = new StrSubstitutor(values);
sub.setEnableSubstitutionInVariables(true);
assertEquals(
"Wrong result",
"Wrong result (1)",
"The white mouse jumps over the lazy dog.",
sub.replace("The ${animal.${species.${color}}} jumps over the ${target}."));
assertEquals(
"Wrong result (2)",
"The brown fox jumps over the lazy dog.",
sub.replace("The ${animal.${species.${unknownColor:-brown}}} jumps over the ${target}."));
}
@Test
public void testDefaultValueDelimiters() {
final Map<String, String> map = new HashMap<String, String>();
map.put("animal", "fox");
map.put("target", "dog");
StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$');
assertEquals("The fox jumps over the lazy dog. 1234567890.",
sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number:-1234567890}."));
sub = new StrSubstitutor(map, "${", "}", '$', "?:");
assertEquals("The fox jumps over the lazy dog. 1234567890.",
sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number?:1234567890}."));
sub = new StrSubstitutor(map, "${", "}", '$', "||");
assertEquals("The fox jumps over the lazy dog. 1234567890.",
sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number||1234567890}."));
sub = new StrSubstitutor(map, "${", "}", '$', "!");
assertEquals("The fox jumps over the lazy dog. 1234567890.",
sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
sub = new StrSubstitutor(map, "${", "}", '$', "");
sub.setValueDelimiterMatcher(null);
assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.",
sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
sub = new StrSubstitutor(map, "${", "}", '$');
sub.setValueDelimiterMatcher(null);
assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.",
sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
}
//-----------------------------------------------------------------------
@ -381,8 +448,10 @@ public class StrSubstitutorTest {
public void testConstructorMapFull() {
final Map<String, String> map = new HashMap<String, String>();
map.put("name", "commons");
final StrSubstitutor sub = new StrSubstitutor(map, "<", ">", '!');
StrSubstitutor sub = new StrSubstitutor(map, "<", ">", '!');
assertEquals("Hi < commons", sub.replace("Hi !< <name>"));
sub = new StrSubstitutor(map, "<", ">", '!', "||");
assertEquals("Hi < commons", sub.replace("Hi !< <name2||commons>"));
}
//-----------------------------------------------------------------------
@ -461,6 +530,28 @@ public class StrSubstitutorTest {
assertSame(matcher, sub.getVariableSuffixMatcher());
}
/**
* Tests get set.
*/
@Test
public void testGetSetValueDelimiter() {
final StrSubstitutor sub = new StrSubstitutor();
assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.StringMatcher);
sub.setValueDelimiter(':');
assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.CharMatcher);
sub.setValueDelimiter("||");
assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.StringMatcher);
sub.setValueDelimiter((String) null);
assertNull(sub.getValueDelimiterMatcher());
final StrMatcher matcher = StrMatcher.commaMatcher();
sub.setValueDelimiterMatcher(matcher);
assertSame(matcher, sub.getValueDelimiterMatcher());
sub.setValueDelimiterMatcher((StrMatcher) null);
assertNull(sub.getValueDelimiterMatcher());
}
//-----------------------------------------------------------------------
/**
* Tests static.