This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-text.git


The following commit(s) were added to refs/heads/master by this push:
     new 1242858  Make it easier to subclass and compose StringSubstitutor.
1242858 is described below

commit 12428582b46e9e3ce0dff65e25c51a71083c19b7
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Sun Jul 5 11:02:53 2020 -0400

    Make it easier to subclass and compose StringSubstitutor.
---
 .../org/apache/commons/text/StringSubstitutor.java | 206 ++++++++++++---------
 .../apache/commons/text/StringSubstitutorTest.java |  14 ++
 2 files changed, 132 insertions(+), 88 deletions(-)

diff --git a/src/main/java/org/apache/commons/text/StringSubstitutor.java 
b/src/main/java/org/apache/commons/text/StringSubstitutor.java
index 8acf438..82e64c9 100644
--- a/src/main/java/org/apache/commons/text/StringSubstitutor.java
+++ b/src/main/java/org/apache/commons/text/StringSubstitutor.java
@@ -142,20 +142,16 @@ import 
org.apache.commons.text.matcher.StringMatcherFactory;
  * <pre>
  * final StringSubstitutor interpolator = 
StringSubstitutor.createInterpolator();
  * interpolator.setEnableSubstitutionInVariables(true); // Allows for nested 
$'s.
- * final String text = interpolator.replace(
- *       "Base64 Decoder:        ${base64Decoder:SGVsbG9Xb3JsZCE=}\n"
+ * final String text = interpolator.replace("Base64 Decoder:        
${base64Decoder:SGVsbG9Xb3JsZCE=}\n"
  *     + "Base64 Encoder:        ${base64Encoder:HelloWorld!}\n"
  *     + "Java Constant:         ${const:java.awt.event.KeyEvent.VK_ESCAPE}\n"
- *     + "Date:                  ${date:yyyy-MM-dd}\n"
- *     + "DNS:                   ${dns:address|apache.org}\n"
+ *     + "Date:                  ${date:yyyy-MM-dd}\n" + "DNS:                 
  ${dns:address|apache.org}\n"
  *     + "Environment Variable:  ${env:USERNAME}\n"
  *     + "File Content:          
${file:UTF-8:src/test/resources/document.properties}\n"
- *     + "Java:                  ${java:version}\n"
- *     + "Localhost:             ${localhost:canonical-name}\n"
+ *     + "Java:                  ${java:version}\n" + "Localhost:             
${localhost:canonical-name}\n"
  *     + "Properties File:       
${properties:src/test/resources/document.properties::mykey}\n"
  *     + "Resource Bundle:       
${resourceBundle:org.example.testResourceBundleLookup:mykey}\n"
- *     + "Script:                ${script:javascript:3 + 4}\n"
- *     + "System Property:       ${sys:user.dir}\n"
+ *     + "Script:                ${script:javascript:3 + 4}\n" + "System 
Property:       ${sys:user.dir}\n"
  *     + "URL Decoder:           ${urlDecoder:Hello%20World%21}\n"
  *     + "URL Encoder:           ${urlEncoder:Hello World!}\n"
  *     + "URL Content (HTTP):    ${url:UTF-8:http://www.apache.org}\n";
@@ -169,9 +165,9 @@ import org.apache.commons.text.matcher.StringMatcherFactory;
  *
  * <h2>Using Recursive Variable Replacement</h2>
  * <p>
- * Variable replacement can work recursively by calling {@link 
#setEnableSubstitutionInVariables(boolean)}
- * with {@code true}. If a variable value contains a variable then that 
variable will
- * also be replaced. Cyclic replacements are detected and will throw an 
exception.
+ * Variable replacement can work recursively by calling {@link 
#setEnableSubstitutionInVariables(boolean)} with
+ * {@code true}. If a variable value contains a variable then that variable 
will also be replaced. Cyclic replacements
+ * are detected and will throw an exception.
  * </p>
  * <p>
  * You can get the replace result to contain a variable prefix. For example:
@@ -222,6 +218,35 @@ import 
org.apache.commons.text.matcher.StringMatcherFactory;
 public class StringSubstitutor {
 
     /**
+     * The low-level result of a substitution.
+     *
+     * @since 1.9
+     */
+    public static class Result {
+
+        /** Whether the buffer is altered. */
+        public final boolean altered;
+
+        /** The length of change. */
+        public final int lengthChange;
+
+        /** The starting position of last variable seen. */
+        public final int startVarPos;
+
+        private Result(final boolean altered, final int lengthChange, final 
int startVarPos) {
+            super();
+            this.altered = altered;
+            this.lengthChange = lengthChange;
+            this.startVarPos = startVarPos;
+        }
+    }
+
+    /**
+     * Constant for the default escape character.
+     */
+    public static final char DEFAULT_ESCAPE = '$';
+
+    /**
      * The default variable default separator.
      *
      * @since 1.5.
@@ -243,11 +268,6 @@ public class StringSubstitutor {
     public static final String DEFAULT_VAR_START = "${";
 
     /**
-     * Constant for the default escape character.
-     */
-    public static final char DEFAULT_ESCAPE = '$';
-
-    /**
      * Constant for the default variable prefix.
      */
     public static final StringMatcher DEFAULT_PREFIX = 
StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_START);
@@ -261,7 +281,7 @@ public class StringSubstitutor {
      * Constant for the default value delimiter of a variable.
      */
     public static final StringMatcher DEFAULT_VALUE_DELIMITER = 
StringMatcherFactory.INSTANCE
-            .stringMatcher(DEFAULT_VAR_DEFAULT);
+        .stringMatcher(DEFAULT_VAR_DEFAULT);
 
     /**
      * Creates a new instance using the interpolator string lookup
@@ -271,8 +291,8 @@ public class StringSubstitutor {
      * </p>
      *
      * <pre>
-     * StringSubstitutor.createInterpolator()
-     *   .replace("OS name: ${sys:os.name}, " + "3 + 4 = ${script:javascript:3 
+ 4}");
+     * StringSubstitutor.createInterpolator().replace(
+     *   "OS name: ${sys:os.name}, " + "3 + 4 = ${script:javascript:3 + 4}");
      * </pre>
      *
      * @return a new instance using the interpolator string lookup.
@@ -286,8 +306,8 @@ public class StringSubstitutor {
     /**
      * Replaces all the occurrences of variables in the given source object 
with their matching values from the map.
      *
-     * @param <V>      the type of the values in the map
-     * @param source   the source text containing the variables to substitute, 
null returns null
+     * @param <V> the type of the values in the map
+     * @param source the source text containing the variables to substitute, 
null returns null
      * @param valueMap the map with the values, may be null
      * @return The result of the replace operation
      * @throws IllegalArgumentException if a variable is not found and 
enableUndefinedVariableException is true
@@ -300,17 +320,17 @@ public class StringSubstitutor {
      * Replaces all the occurrences of variables in the given source object 
with their matching values from the map.
      * This method allows to specify a custom variable prefix and suffix
      *
-     * @param <V>      the type of the values in the map
-     * @param source   the source text containing the variables to substitute, 
null returns null
+     * @param <V> the type of the values in the map
+     * @param source the source text containing the variables to substitute, 
null returns null
      * @param valueMap the map with the values, may be null
-     * @param prefix   the prefix of variables, not null
-     * @param suffix   the suffix of variables, not null
+     * @param prefix the prefix of variables, not null
+     * @param suffix the suffix of variables, not null
      * @return The result of the replace operation
      * @throws IllegalArgumentException if the prefix or suffix is null
      * @throws IllegalArgumentException if a variable is not found and 
enableUndefinedVariableException is true
      */
     public static <V> String replace(final Object source, final Map<String, V> 
valueMap, final String prefix,
-            final String suffix) {
+        final String suffix) {
         return new StringSubstitutor(valueMap, prefix, suffix).replace(source);
     }
 
@@ -318,7 +338,7 @@ public class StringSubstitutor {
      * Replaces all the occurrences of variables in the given source object 
with their matching values from the
      * properties.
      *
-     * @param source          the source text containing the variables to 
substitute, null returns null
+     * @param source the source text containing the variables to substitute, 
null returns null
      * @param valueProperties the properties with values, may be null
      * @return The result of the replace operation
      * @throws IllegalArgumentException if a variable is not found and 
enableUndefinedVariableException is true
@@ -405,7 +425,7 @@ public class StringSubstitutor {
      * Creates a new instance and initializes it. Uses defaults for variable 
prefix and suffix and the escaping
      * character.
      *
-     * @param <V>      the type of the values in the map
+     * @param <V> the type of the values in the map
      * @param valueMap the map with the variables' values, may be null
      */
     public <V> StringSubstitutor(final Map<String, V> valueMap) {
@@ -415,10 +435,10 @@ public class StringSubstitutor {
     /**
      * Creates a new instance and initializes it. Uses a default escaping 
character.
      *
-     * @param <V>      the type of the values in the map
+     * @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 prefix the prefix for variables, not null
+     * @param suffix the suffix for variables, not null
      * @throws IllegalArgumentException if the prefix or suffix is null
      */
     public <V> StringSubstitutor(final Map<String, V> valueMap, final String 
prefix, final String suffix) {
@@ -428,31 +448,31 @@ public class StringSubstitutor {
     /**
      * Creates a new instance and initializes it.
      *
-     * @param <V>      the type of the values in the map
+     * @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 prefix the prefix for variables, not null
+     * @param suffix the suffix for variables, not null
+     * @param escape the escape character
      * @throws IllegalArgumentException if the prefix or suffix is null
      */
     public <V> StringSubstitutor(final Map<String, V> valueMap, final String 
prefix, final String suffix,
-            final char escape) {
+        final char escape) {
         this(StringLookupFactory.INSTANCE.mapStringLookup(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 <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> StringSubstitutor(final Map<String, V> valueMap, final String 
prefix, final String suffix,
-            final char escape, final String valueDelimiter) {
+        final char escape, final String valueDelimiter) {
         this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, 
suffix, escape, valueDelimiter);
     }
 
@@ -469,13 +489,13 @@ public class StringSubstitutor {
      * 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 prefix the prefix for variables, not null
+     * @param suffix the suffix for variables, not null
+     * @param escape the escape character
      * @throws IllegalArgumentException if the prefix or suffix is null
      */
     public StringSubstitutor(final StringLookup variableResolver, final String 
prefix, final String suffix,
-            final char escape) {
+        final char escape) {
         this.setVariableResolver(variableResolver);
         this.setVariablePrefix(prefix);
         this.setVariableSuffix(suffix);
@@ -487,14 +507,14 @@ public class StringSubstitutor {
      * 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
+     * @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 StringSubstitutor(final StringLookup variableResolver, final String 
prefix, final String suffix,
-            final char escape, final String valueDelimiter) {
+        final char escape, final String valueDelimiter) {
         this.setVariableResolver(variableResolver);
         this.setVariablePrefix(prefix);
         this.setVariableSuffix(suffix);
@@ -506,28 +526,28 @@ public class StringSubstitutor {
      * 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 prefixMatcher the prefix for variables, not null
+     * @param suffixMatcher the suffix for variables, not null
+     * @param escape the escape character
      * @throws IllegalArgumentException if the prefix or suffix is null
      */
     public StringSubstitutor(final StringLookup variableResolver, final 
StringMatcher prefixMatcher,
-            final StringMatcher suffixMatcher, final char escape) {
+        final StringMatcher 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 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 StringSubstitutor(final StringLookup variableResolver, final 
StringMatcher prefixMatcher,
-            final StringMatcher suffixMatcher, final char escape, final 
StringMatcher valueDelimiterMatcher) {
+        final StringMatcher suffixMatcher, final char escape, final 
StringMatcher valueDelimiterMatcher) {
         this.setVariableResolver(variableResolver);
         this.setVariablePrefixMatcher(prefixMatcher);
         this.setVariableSuffixMatcher(suffixMatcher);
@@ -539,7 +559,6 @@ public class StringSubstitutor {
      * Creates a new instance based on the given StringSubstitutor.
      *
      * @param other The StringSubstitutor is use as the source.
-     *
      * @since 1.9
      */
     public StringSubstitutor(final StringSubstitutor other) {
@@ -557,7 +576,7 @@ public class StringSubstitutor {
     /**
      * Checks if the specified variable is already in the stack (list) of 
variables.
      *
-     * @param varName        the variable name to check
+     * @param varName the variable name to check
      * @param priorVariables the list of prior variables
      */
     private void checkCyclicSubstitution(final String varName, final 
List<String> priorVariables) {
@@ -582,7 +601,19 @@ public class StringSubstitutor {
         return this.escapeChar;
     }
 
-    // Resolver
+    /**
+     * Gets the minimum expression length based on the size of the prefix 
matcher and suffix matcher.
+     * <p>
+     * By default, {@code 4}, as the shortest variable name length is 1, for 
example, {@code "${k}"}.
+     * </p>
+     *
+     * @return the minimum expression length.
+     * @since 1.9
+     */
+    public int getMinExpressionLength() {
+        return getVariablePrefixMatcher().size() + 1 + 
getVariableSuffixMatcher().size();
+    }
+
     /**
      * Gets the StringLookup that is used to lookup variables.
      *
@@ -592,7 +623,6 @@ public class StringSubstitutor {
         return this.variableResolver;
     }
 
-    // Variable Default Value Delimiter
     /**
      * Gets the variable default value delimiter matcher currently in use.
      * <p>
@@ -1035,13 +1065,13 @@ public class StringSubstitutor {
      * </p>
      *
      * @param variableName the name of the variable, not null
-     * @param buf          the buffer where the substitution is occurring, not 
null
-     * @param startPos     the start position of the variable including the 
prefix, valid
-     * @param endPos       the end position of the variable including the 
suffix, valid
+     * @param buf the buffer where the substitution is occurring, not null
+     * @param startPos the start position of the variable including the 
prefix, valid
+     * @param endPos the end position of the variable including the suffix, 
valid
      * @return The variable's value or <b>null</b> if the variable is unknown
      */
     protected String resolveVariable(final String variableName, final 
TextStringBuilder buf, final int startPos,
-            final int endPos) {
+        final int endPos) {
         final StringLookup resolver = getStringLookup();
         if (resolver == null) {
             return null;
@@ -1098,10 +1128,9 @@ public class StringSubstitutor {
 
     /**
      * Sets a flag controlling whether escapes are preserved during 
substitution. If set to <b>true</b>, the escape
-     * character is retained during substitution (e.g. {@code 
$${this-is-escaped}} remains
-     * {@code $${this-is-escaped}}). If set to <b>false</b>, the escape 
character is removed during substitution
-     * (e.g. {@code $${this-is-escaped}} becomes {@code ${this-is-escaped}}). 
The default value is
-     * <b>false</b>
+     * character is retained during substitution (e.g. {@code 
$${this-is-escaped}} remains {@code $${this-is-escaped}}).
+     * If set to <b>false</b>, the escape character is removed during 
substitution (e.g. {@code $${this-is-escaped}}
+     * becomes {@code ${this-is-escaped}}). The default value is <b>false</b>
      *
      * @param preserveEscapes true if escapes are to be preserved
      * @return this, to enable chaining
@@ -1283,28 +1312,28 @@ public class StringSubstitutor {
      * </p>
      *
      * @param builder the string builder to substitute into, not null
-     * @param offset  the start offset within the builder, must be valid
-     * @param length  the length within the builder to be processed, must be 
valid
+     * @param offset the start offset within the builder, must be valid
+     * @param length the length within the builder to be processed, must be 
valid
      * @return true if altered
      */
     protected boolean substitute(final TextStringBuilder builder, final int 
offset, final int length) {
-        return substitute(builder, offset, length, null) > 0;
+        return substitute(builder, offset, length, null).altered;
     }
 
     /**
      * Recursive handler for multiple levels of interpolation. This is the 
main interpolation method, which resolves the
      * values of all variable references contained in the passed in text.
      *
-     * @param builder        the string builder to substitute into, not null
-     * @param offset         the start offset within the builder, must be valid
-     * @param length         the length within the builder to be processed, 
must be valid
+     * @param builder the string builder to substitute into, not null
+     * @param offset the start offset within the builder, must be valid
+     * @param length the length within the builder to be processed, must be 
valid
      * @param priorVariables the stack keeping track of the replaced 
variables, may be null
-     * @return The length change that occurs, unless priorVariables is null 
when the int represents a boolean flag as to
-     *         whether any change occurred.
+     * @return The result.
      * @throws IllegalArgumentException if variable is not found when its 
allowed to throw exception
+     * @since 1.9
      */
-    private int substitute(final TextStringBuilder builder, final int offset, 
final int length,
-            List<String> priorVariables) {
+    protected Result substitute(final TextStringBuilder builder, final int 
offset, final int length,
+        List<String> priorVariables) {
         Objects.requireNonNull(builder, "builder");
         final StringMatcher prefixMatcher = getVariablePrefixMatcher();
         final StringMatcher suffixMatcher = getVariableSuffixMatcher();
@@ -1315,11 +1344,11 @@ public class StringSubstitutor {
         final boolean undefinedVariableException = 
isEnableUndefinedVariableException();
         final boolean preserveEscapes = isPreserveEscapes();
 
-        final boolean top = priorVariables == null;
         boolean altered = false;
         int lengthChange = 0;
         int bufEnd = offset + length;
         int pos = offset;
+        int startVarPos = -1;
         while (pos < bufEnd) {
             final int startMatchLen = prefixMatcher.isMatch(builder, pos, 
offset, bufEnd);
             if (startMatchLen == 0) {
@@ -1340,6 +1369,7 @@ public class StringSubstitutor {
                     bufEnd--;
                 } else {
                     // find suffix
+                    startVarPos = pos;
                     final int startPos = pos;
                     pos += startMatchLen;
                     int endMatchLen = 0;
@@ -1409,13 +1439,15 @@ public class StringSubstitutor {
                                 if (varValue == null) {
                                     varValue = varDefaultValue;
                                 }
+                                startVarPos = -1;
                                 if (varValue != null) {
+                                    startVarPos = startPos;
                                     final int varLen = varValue.length();
                                     builder.replace(startPos, endPos, 
varValue);
                                     altered = true;
                                     int change = 0;
                                     if (!substitutionInValuesDisabled) { // 
recursive replace
-                                        change = substitute(builder, startPos, 
varLen, priorVariables);
+                                        change = substitute(builder, startPos, 
varLen, priorVariables).lengthChange;
                                     }
                                     change = change + varLen - (endPos - 
startPos);
                                     pos += change;
@@ -1429,6 +1461,7 @@ public class StringSubstitutor {
 
                                 // remove variable from the cyclic stack
                                 priorVariables.remove(priorVariables.size() - 
1);
+                                startVarPos = -1;
                                 break;
                             }
                             nestedVarCount--;
@@ -1438,9 +1471,6 @@ public class StringSubstitutor {
                 }
             }
         }
-        if (top) {
-            return altered ? 1 : 0;
-        }
-        return lengthChange;
+        return new Result(altered, lengthChange, startVarPos);
     }
 }
diff --git a/src/test/java/org/apache/commons/text/StringSubstitutorTest.java 
b/src/test/java/org/apache/commons/text/StringSubstitutorTest.java
index ae9834f..3aaac8d 100644
--- a/src/test/java/org/apache/commons/text/StringSubstitutorTest.java
+++ b/src/test/java/org/apache/commons/text/StringSubstitutorTest.java
@@ -224,6 +224,20 @@ public class StringSubstitutorTest {
             target.getValueDelimiterMatcher().toString());
     }
 
+    @Test
+    public void testGetMinExpressionLength() throws IOException {
+        final StringSubstitutor sub = new StringSubstitutor();
+        assertEquals(4, sub.getMinExpressionLength());
+        sub.setVariablePrefix('a');
+        assertEquals(3, sub.getMinExpressionLength());
+        sub.setVariablePrefix("abc");
+        assertEquals(5, sub.getMinExpressionLength());
+        sub.setVariableSuffix("xyz");
+        assertEquals(7, sub.getMinExpressionLength());
+        sub.setVariablePrefix(StringUtils.EMPTY);
+        sub.setVariableSuffix(StringUtils.EMPTY);
+        assertEquals(1, sub.getMinExpressionLength());
+    }
 
     /**
      * Tests get set.

Reply via email to