You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by oh...@apache.org on 2013/09/18 21:35:16 UTC

svn commit: r1524541 - in /commons/proper/lang/trunk/src: main/java/org/apache/commons/lang3/text/StrSubstitutor.java test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java

Author: oheger
Date: Wed Sep 18 19:35:16 2013
New Revision: 1524541

URL: http://svn.apache.org/r1524541
Log:
[LANG-893] StrSubstitutor now supports the declaration of default values for the variables to be replaced. Thanks to Woonsan Ko for the patch.

Modified:
    commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java
    commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java

Modified: commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java
URL: http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java?rev=1524541&r1=1524540&r2=1524541&view=diff
==============================================================================
--- commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java (original)
+++ commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java Wed Sep 18 19:35:16 2013
@@ -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.
@@ -128,6 +152,10 @@ public class StrSubstitutor {
      */
     private StrMatcher suffixMatcher;
     /**
+     * Stores the default variable value delimiter
+     */
+    private StrMatcher valueDelimiterMatcher;
+    /**
      * Variable resolution is delegated to an implementor of VariableResolver.
      */
     private StrLookup<?> variableResolver;
@@ -250,6 +278,21 @@ public class StrSubstitutor {
     /**
      * 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.
+     *
      * @param variableResolver  the variable resolver, may be null
      */
     public StrSubstitutor(final StrLookup<?> variableResolver) {
@@ -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
     //-----------------------------------------------------------------------
     /**

Modified: commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java
URL: http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java?rev=1524541&r1=1524540&r2=1524541&view=diff
==============================================================================
--- commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java (original)
+++ commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java Wed Sep 18 19:35:16 2013
@@ -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.