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.