StrSubstitutor.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.text;

  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.Properties;
  22. import java.util.function.Function;
  23. import java.util.stream.Collectors;

  24. import org.apache.commons.lang3.Validate;

  25. /**
  26.  * Substitutes variables within a string by values.
  27.  * <p>
  28.  * This class takes a piece of text and substitutes all the variables within it.
  29.  * The default definition of a variable is {@code ${variableName}}.
  30.  * The prefix and suffix can be changed via constructors and set methods.
  31.  * <p>
  32.  * Variable values are typically resolved from a map, but could also be resolved
  33.  * from system properties, or by supplying a custom variable resolver.
  34.  * <p>
  35.  * The simplest example is to use this class to replace Java System properties. For example:
  36.  * <pre>
  37.  * StrSubstitutor.replaceSystemProperties(
  38.  *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
  39.  * </pre>
  40.  * <p>
  41.  * Typical usage of this class follows the following pattern: First an instance is created
  42.  * and initialized with the map that contains the values for the available variables.
  43.  * If a prefix and/or suffix for variables should be used other than the default ones,
  44.  * the appropriate settings can be performed. After that the {@code replace()}
  45.  * method can be called passing in the source text for interpolation. In the returned
  46.  * text all variable references (as long as their values are known) will be resolved.
  47.  * The following example demonstrates this:
  48.  * <pre>
  49.  * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
  50.  * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
  51.  * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
  52.  * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
  53.  * StrSubstitutor sub = new StrSubstitutor(valuesMap);
  54.  * String resolvedString = sub.replace(templateString);
  55.  * </pre>
  56.  * yielding:
  57.  * <pre>
  58.  *      The quick brown fox jumped over the lazy dog.
  59.  * </pre>
  60.  * <p>
  61.  * Also, this class allows to set a default value for unresolved variables.
  62.  * The default value for a variable can be appended to the variable name after the variable
  63.  * default value delimiter. The default value of the variable default value delimiter is ':-',
  64.  * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
  65.  * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
  66.  * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
  67.  * The following shows an example with variable default value settings:
  68.  * <pre>
  69.  * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
  70.  * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
  71.  * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
  72.  * String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
  73.  * StrSubstitutor sub = new StrSubstitutor(valuesMap);
  74.  * String resolvedString = sub.replace(templateString);
  75.  * </pre>
  76.  * yielding:
  77.  * <pre>
  78.  *      The quick brown fox jumped over the lazy dog. 1234567890.
  79.  * </pre>
  80.  * <p>
  81.  * In addition to this usage pattern there are some static convenience methods that
  82.  * cover the most common use cases. These methods can be used without the need of
  83.  * manually creating an instance. However if multiple replace operations are to be
  84.  * performed, creating and reusing an instance of this class will be more efficient.
  85.  * <p>
  86.  * Variable replacement works in a recursive way. Thus, if a variable value contains
  87.  * a variable then that variable will also be replaced. Cyclic replacements are
  88.  * detected and will cause an exception to be thrown.
  89.  * <p>
  90.  * Sometimes the interpolation's result must contain a variable prefix. As an example
  91.  * take the following source text:
  92.  * <pre>
  93.  *   The variable ${${name}} must be used.
  94.  * </pre>
  95.  * Here only the variable's name referred to in the text should be replaced resulting
  96.  * in the text (assuming that the value of the {@code name} variable is {@code x}):
  97.  * <pre>
  98.  *   The variable ${x} must be used.
  99.  * </pre>
  100.  * To achieve this effect there are two possibilities: Either set a different prefix
  101.  * and suffix for variables which do not conflict with the result text you want to
  102.  * produce. The other possibility is to use the escape character, by default '$'.
  103.  * If this character is placed before a variable reference, this reference is ignored
  104.  * and won't be replaced. For example:
  105.  * <pre>
  106.  *   The variable $${${name}} must be used.
  107.  * </pre>
  108.  * <p>
  109.  * In some complex scenarios you might even want to perform substitution in the
  110.  * names of variables, for instance
  111.  * <pre>
  112.  * ${jre-${java.specification.version}}
  113.  * </pre>
  114.  * {@code StrSubstitutor} supports this recursive substitution in variable
  115.  * names, but it has to be enabled explicitly by setting the
  116.  * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
  117.  * property to <strong>true</strong>.
  118.  * <p>This class is <strong>not</strong> thread safe.</p>
  119.  *
  120.  * @since 1.0
  121.  * @deprecated Deprecated as of 1.3, use {@link StringSubstitutor} instead. This class will be removed in 2.0.
  122.  */
  123. @Deprecated
  124. public class StrSubstitutor {

  125.     /**
  126.      * Constant for the default escape character.
  127.      */
  128.     public static final char DEFAULT_ESCAPE = '$';

  129.     /**
  130.      * Constant for the default variable prefix.
  131.      */
  132.     public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");

  133.     /**
  134.      * Constant for the default variable suffix.
  135.      */
  136.     public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");

  137.     /**
  138.      * Constant for the default value delimiter of a variable.
  139.      */
  140.     public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");

  141.     /**
  142.      * Replaces all the occurrences of variables in the given source object with
  143.      * their matching values from the map.
  144.      *
  145.      * @param <V> the type of the values in the map
  146.      * @param source  the source text containing the variables to substitute, null returns null
  147.      * @param valueMap  the map with the values, may be null
  148.      * @return The result of the replace operation
  149.      */
  150.     public static <V> String replace(final Object source, final Map<String, V> valueMap) {
  151.         return new StrSubstitutor(valueMap).replace(source);
  152.     }

  153.     /**
  154.      * Replaces all the occurrences of variables in the given source object with
  155.      * their matching values from the map. This method allows to specify a
  156.      * custom variable prefix and suffix
  157.      *
  158.      * @param <V> the type of the values in the map
  159.      * @param source  the source text containing the variables to substitute, null returns null
  160.      * @param valueMap  the map with the values, may be null
  161.      * @param prefix  the prefix of variables, not null
  162.      * @param suffix  the suffix of variables, not null
  163.      * @return The result of the replace operation
  164.      * @throws IllegalArgumentException if the prefix or suffix is null
  165.      */
  166.     public static <V> String replace(final Object source,
  167.                                      final Map<String, V> valueMap,
  168.                                      final String prefix,
  169.                                      final String suffix) {
  170.         return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
  171.     }

  172.     /**
  173.      * Replaces all the occurrences of variables in the given source object with their matching
  174.      * values from the properties.
  175.      *
  176.      * @param source the source text containing the variables to substitute, null returns null
  177.      * @param valueProperties the properties with values, may be null
  178.      * @return The result of the replace operation
  179.      */
  180.     public static String replace(final Object source, final Properties valueProperties) {
  181.         if (valueProperties == null) {
  182.             return source.toString();
  183.         }
  184.         return StrSubstitutor.replace(source,
  185.                 valueProperties.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), valueProperties::getProperty)));
  186.     }

  187.     /**
  188.      * Replaces all the occurrences of variables in the given source object with
  189.      * their matching values from the system properties.
  190.      *
  191.      * @param source  the source text containing the variables to substitute, null returns null
  192.      * @return The result of the replace operation
  193.      */
  194.     public static String replaceSystemProperties(final Object source) {
  195.         return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
  196.     }

  197.     /**
  198.      * Stores the escape character.
  199.      */
  200.     private char escapeChar;

  201.     /**
  202.      * Stores the variable prefix.
  203.      */
  204.     private StrMatcher prefixMatcher;

  205.     /**
  206.      * Stores the variable suffix.
  207.      */
  208.     private StrMatcher suffixMatcher;

  209.     /**
  210.      * Stores the default variable value delimiter.
  211.      */
  212.     private StrMatcher valueDelimiterMatcher;

  213.     /**
  214.      * Variable resolution is delegated to an implementor of VariableResolver.
  215.      */
  216.     private StrLookup<?> variableResolver;

  217.     /**
  218.      * The flag whether substitution in variable names is enabled.
  219.      */
  220.     private boolean enableSubstitutionInVariables;

  221.     /**
  222.      * Whether escapes should be preserved.  Default is false;
  223.      */
  224.     private boolean preserveEscapes;

  225.     /**
  226.      * The flag whether substitution in variable values is disabled.
  227.      */
  228.     private boolean disableSubstitutionInValues;

  229.     /**
  230.      * Constructs a new instance with defaults for variable prefix and suffix
  231.      * and the escaping character.
  232.      */
  233.     public StrSubstitutor() {
  234.         this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
  235.     }

  236.     /**
  237.      * Constructs a new instance and initializes it. Uses defaults for variable
  238.      * prefix and suffix and the escaping character.
  239.      *
  240.      * @param <V> the type of the values in the map
  241.      * @param valueMap  the map with the variables' values, may be null
  242.      */
  243.     public <V> StrSubstitutor(final Map<String, V> valueMap) {
  244.         this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
  245.     }

  246.     /**
  247.      * Constructs a new instance and initializes it. Uses a default escaping character.
  248.      *
  249.      * @param <V> the type of the values in the map
  250.      * @param valueMap  the map with the variables' values, may be null
  251.      * @param prefix  the prefix for variables, not null
  252.      * @param suffix  the suffix for variables, not null
  253.      * @throws IllegalArgumentException if the prefix or suffix is null
  254.      */
  255.     public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
  256.         this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
  257.     }

  258.     /**
  259.      * Constructs a new instance and initializes it.
  260.      *
  261.      * @param <V> the type of the values in the map
  262.      * @param valueMap  the map with the variables' values, may be null
  263.      * @param prefix  the prefix for variables, not null
  264.      * @param suffix  the suffix for variables, not null
  265.      * @param escape  the escape character
  266.      * @throws IllegalArgumentException if the prefix or suffix is null
  267.      */
  268.     public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
  269.                               final char escape) {
  270.         this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
  271.     }

  272.     /**
  273.      * Constructs a new instance and initializes it.
  274.      *
  275.      * @param <V> the type of the values in the map
  276.      * @param valueMap  the map with the variables' values, may be null
  277.      * @param prefix  the prefix for variables, not null
  278.      * @param suffix  the suffix for variables, not null
  279.      * @param escape  the escape character
  280.      * @param valueDelimiter  the variable default value delimiter, may be null
  281.      * @throws IllegalArgumentException if the prefix or suffix is null
  282.      */
  283.     public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
  284.                               final char escape, final String valueDelimiter) {
  285.         this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
  286.     }

  287.     /**
  288.      * Constructs a new instance and initializes it.
  289.      *
  290.      * @param variableResolver  the variable resolver, may be null
  291.      */
  292.     public StrSubstitutor(final StrLookup<?> variableResolver) {
  293.         this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
  294.     }

  295.     /**
  296.      * Constructs a new instance and initializes it.
  297.      *
  298.      * @param variableResolver  the variable resolver, may be null
  299.      * @param prefix  the prefix for variables, not null
  300.      * @param suffix  the suffix for variables, not null
  301.      * @param escape  the escape character
  302.      * @throws IllegalArgumentException if the prefix or suffix is null
  303.      */
  304.     public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
  305.                           final char escape) {
  306.         setVariableResolver(variableResolver);
  307.         setVariablePrefix(prefix);
  308.         setVariableSuffix(suffix);
  309.         setEscapeChar(escape);
  310.         setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
  311.     }

  312.     /**
  313.      * Constructs a new instance and initializes it.
  314.      *
  315.      * @param variableResolver  the variable resolver, may be null
  316.      * @param prefix  the prefix for variables, not null
  317.      * @param suffix  the suffix for variables, not null
  318.      * @param escape  the escape character
  319.      * @param valueDelimiter  the variable default value delimiter string, may be null
  320.      * @throws IllegalArgumentException if the prefix or suffix is null
  321.      */
  322.     public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
  323.                           final char escape, final String valueDelimiter) {
  324.         setVariableResolver(variableResolver);
  325.         setVariablePrefix(prefix);
  326.         setVariableSuffix(suffix);
  327.         setEscapeChar(escape);
  328.         setValueDelimiter(valueDelimiter);
  329.     }

  330.     /**
  331.      * Constructs a new instance and initializes it.
  332.      *
  333.      * @param variableResolver  the variable resolver, may be null
  334.      * @param prefixMatcher  the prefix for variables, not null
  335.      * @param suffixMatcher  the suffix for variables, not null
  336.      * @param escape  the escape character
  337.      * @throws IllegalArgumentException if the prefix or suffix is null
  338.      */
  339.     public StrSubstitutor(
  340.             final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
  341.             final char escape) {
  342.         this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
  343.     }

  344.     /**
  345.      * Constructs a new instance and initializes it.
  346.      *
  347.      * @param variableResolver  the variable resolver, may be null
  348.      * @param prefixMatcher  the prefix for variables, not null
  349.      * @param suffixMatcher  the suffix for variables, not null
  350.      * @param escape  the escape character
  351.      * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
  352.      * @throws IllegalArgumentException if the prefix or suffix is null
  353.      */
  354.     public StrSubstitutor(
  355.             final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
  356.             final char escape, final StrMatcher valueDelimiterMatcher) {
  357.         setVariableResolver(variableResolver);
  358.         setVariablePrefixMatcher(prefixMatcher);
  359.         setVariableSuffixMatcher(suffixMatcher);
  360.         setEscapeChar(escape);
  361.         setValueDelimiterMatcher(valueDelimiterMatcher);
  362.     }

  363.     /**
  364.      * Checks if the specified variable is already in the stack (list) of variables.
  365.      *
  366.      * @param varName  the variable name to check
  367.      * @param priorVariables  the list of prior variables
  368.      */
  369.     private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
  370.         if (!priorVariables.contains(varName)) {
  371.             return;
  372.         }
  373.         final StrBuilder buf = new StrBuilder(256);
  374.         buf.append("Infinite loop in property interpolation of ");
  375.         buf.append(priorVariables.remove(0));
  376.         buf.append(": ");
  377.         buf.appendWithSeparators(priorVariables, "->");
  378.         throw new IllegalStateException(buf.toString());
  379.     }

  380.     /**
  381.      * Returns the escape character.
  382.      *
  383.      * @return The character used for escaping variable references
  384.      */
  385.     public char getEscapeChar() {
  386.         return this.escapeChar;
  387.     }

  388.     /**
  389.      * Gets the variable default value delimiter matcher currently in use.
  390.      * <p>
  391.      * The variable default value delimiter is the character or characters that delimit the
  392.      * variable name and the variable default value. This delimiter is expressed in terms of a matcher
  393.      * allowing advanced variable default value delimiter matches.
  394.      * </p>
  395.      * <p>
  396.      * If it returns null, then the variable default value resolution is disabled.
  397.      * </p>
  398.      *
  399.      * @return The variable default value delimiter matcher in use, may be null
  400.      */
  401.     public StrMatcher getValueDelimiterMatcher() {
  402.         return valueDelimiterMatcher;
  403.     }

  404.     /**
  405.      * Gets the variable prefix matcher currently in use.
  406.      * <p>
  407.      * The variable prefix is the character or characters that identify the
  408.      * start of a variable. This prefix is expressed in terms of a matcher
  409.      * allowing advanced prefix matches.
  410.      * </p>
  411.      *
  412.      * @return The prefix matcher in use
  413.      */
  414.     public StrMatcher getVariablePrefixMatcher() {
  415.         return prefixMatcher;
  416.     }

  417.     /**
  418.      * Gets the VariableResolver that is used to lookup variables.
  419.      *
  420.      * @return The VariableResolver
  421.      */
  422.     public StrLookup<?> getVariableResolver() {
  423.         return this.variableResolver;
  424.     }

  425.     /**
  426.      * Gets the variable suffix matcher currently in use.
  427.      * <p>
  428.      * The variable suffix is the character or characters that identify the
  429.      * end of a variable. This suffix is expressed in terms of a matcher
  430.      * allowing advanced suffix matches.
  431.      * </p>
  432.      *
  433.      * @return The suffix matcher in use
  434.      */
  435.     public StrMatcher getVariableSuffixMatcher() {
  436.         return suffixMatcher;
  437.     }

  438.     /**
  439.      * Returns a flag whether substitution is disabled in variable values.If set to
  440.      * <strong>true</strong>, the values of variables can contain other variables will not be
  441.      * processed and substituted original variable is evaluated, e.g.
  442.      * <pre>
  443.      * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
  444.      * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
  445.      * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
  446.      * String templateString = &quot;Hi ${name}&quot;;
  447.      * StrSubstitutor sub = new StrSubstitutor(valuesMap);
  448.      * String resolvedString = sub.replace(templateString);
  449.      * </pre>
  450.      * yielding:
  451.      * <pre>
  452.      *      Hi Douglas ${surname}
  453.      * </pre>
  454.      *
  455.      * @return The substitution in variable values flag
  456.      * @since 1.2
  457.      */
  458.     public boolean isDisableSubstitutionInValues() {
  459.         return disableSubstitutionInValues;
  460.     }

  461.     /**
  462.      * Returns a flag whether substitution is done in variable names.
  463.      *
  464.      * @return The substitution in variable names flag
  465.      */
  466.     public boolean isEnableSubstitutionInVariables() {
  467.         return enableSubstitutionInVariables;
  468.     }

  469.     /**
  470.      * Returns the flag controlling whether escapes are preserved during
  471.      * substitution.
  472.      *
  473.      * @return The preserve escape flag
  474.      */
  475.     public boolean isPreserveEscapes() {
  476.         return preserveEscapes;
  477.     }

  478.     /**
  479.      * Replaces all the occurrences of variables with their matching values
  480.      * from the resolver using the given source array as a template.
  481.      * The array is not altered by this method.
  482.      *
  483.      * @param source  the character array to replace in, not altered, null returns null
  484.      * @return The result of the replace operation
  485.      */
  486.     public String replace(final char[] source) {
  487.         if (source == null) {
  488.             return null;
  489.         }
  490.         final StrBuilder buf = new StrBuilder(source.length).append(source);
  491.         substitute(buf, 0, source.length);
  492.         return buf.toString();
  493.     }

  494.     /**
  495.      * Replaces all the occurrences of variables with their matching values
  496.      * from the resolver using the given source array as a template.
  497.      * The array is not altered by this method.
  498.      * <p>
  499.      * Only the specified portion of the array will be processed.
  500.      * The rest of the array is not processed, and is not returned.
  501.      * </p>
  502.      *
  503.      * @param source  the character array to replace in, not altered, null returns null
  504.      * @param offset  the start offset within the array, must be valid
  505.      * @param length  the length within the array to be processed, must be valid
  506.      * @return The result of the replace operation
  507.      */
  508.     public String replace(final char[] source, final int offset, final int length) {
  509.         if (source == null) {
  510.             return null;
  511.         }
  512.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  513.         substitute(buf, 0, length);
  514.         return buf.toString();
  515.     }

  516.     /**
  517.      * Replaces all the occurrences of variables with their matching values
  518.      * from the resolver using the given source as a template.
  519.      * The source is not altered by this method.
  520.      *
  521.      * @param source  the buffer to use as a template, not changed, null returns null
  522.      * @return The result of the replace operation
  523.      */
  524.     public String replace(final CharSequence source) {
  525.         if (source == null) {
  526.             return null;
  527.         }
  528.         return replace(source, 0, source.length());
  529.     }

  530.     /**
  531.      * Replaces all the occurrences of variables with their matching values
  532.      * from the resolver using the given source as a template.
  533.      * The source is not altered by this method.
  534.      * <p>
  535.      * Only the specified portion of the buffer will be processed.
  536.      * The rest of the buffer is not processed, and is not returned.
  537.      * </p>
  538.      *
  539.      * @param source  the buffer to use as a template, not changed, null returns null
  540.      * @param offset  the start offset within the array, must be valid
  541.      * @param length  the length within the array to be processed, must be valid
  542.      * @return The result of the replace operation
  543.      */
  544.     public String replace(final CharSequence source, final int offset, final int length) {
  545.         if (source == null) {
  546.             return null;
  547.         }
  548.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  549.         substitute(buf, 0, length);
  550.         return buf.toString();
  551.     }

  552.     /**
  553.      * Replaces all the occurrences of variables in the given source object with
  554.      * their matching values from the resolver. The input source object is
  555.      * converted to a string using {@code toString} and is not altered.
  556.      *
  557.      * @param source  the source to replace in, null returns null
  558.      * @return The result of the replace operation
  559.      */
  560.     public String replace(final Object source) {
  561.         if (source == null) {
  562.             return null;
  563.         }
  564.         final StrBuilder buf = new StrBuilder().append(source);
  565.         substitute(buf, 0, buf.length());
  566.         return buf.toString();
  567.     }

  568.     /**
  569.      * Replaces all the occurrences of variables with their matching values
  570.      * from the resolver using the given source builder as a template.
  571.      * The builder is not altered by this method.
  572.      *
  573.      * @param source  the builder to use as a template, not changed, null returns null
  574.      * @return The result of the replace operation
  575.      */
  576.     public String replace(final StrBuilder source) {
  577.         if (source == null) {
  578.             return null;
  579.         }
  580.         final StrBuilder buf = new StrBuilder(source.length()).append(source);
  581.         substitute(buf, 0, buf.length());
  582.         return buf.toString();
  583.     }

  584.     /**
  585.      * Replaces all the occurrences of variables with their matching values
  586.      * from the resolver using the given source builder as a template.
  587.      * The builder is not altered by this method.
  588.      * <p>
  589.      * Only the specified portion of the builder will be processed.
  590.      * The rest of the builder is not processed, and is not returned.
  591.      * </p>
  592.      *
  593.      * @param source  the builder to use as a template, not changed, null returns null
  594.      * @param offset  the start offset within the array, must be valid
  595.      * @param length  the length within the array to be processed, must be valid
  596.      * @return The result of the replace operation
  597.      */
  598.     public String replace(final StrBuilder source, final int offset, final int length) {
  599.         if (source == null) {
  600.             return null;
  601.         }
  602.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  603.         substitute(buf, 0, length);
  604.         return buf.toString();
  605.     }

  606.     /**
  607.      * Replaces all the occurrences of variables with their matching values
  608.      * from the resolver using the given source string as a template.
  609.      *
  610.      * @param source  the string to replace in, null returns null
  611.      * @return The result of the replace operation
  612.      */
  613.     public String replace(final String source) {
  614.         if (source == null) {
  615.             return null;
  616.         }
  617.         final StrBuilder buf = new StrBuilder(source);
  618.         if (!substitute(buf, 0, source.length())) {
  619.             return source;
  620.         }
  621.         return buf.toString();
  622.     }

  623.     /**
  624.      * Replaces all the occurrences of variables with their matching values
  625.      * from the resolver using the given source string as a template.
  626.      * <p>
  627.      * Only the specified portion of the string will be processed.
  628.      * The rest of the string is not processed, and is not returned.
  629.      * </p>
  630.      *
  631.      * @param source  the string to replace in, null returns null
  632.      * @param offset  the start offset within the array, must be valid
  633.      * @param length  the length within the array to be processed, must be valid
  634.      * @return The result of the replace operation
  635.      */
  636.     public String replace(final String source, final int offset, final int length) {
  637.         if (source == null) {
  638.             return null;
  639.         }
  640.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  641.         if (!substitute(buf, 0, length)) {
  642.             return source.substring(offset, offset + length);
  643.         }
  644.         return buf.toString();
  645.     }

  646.     /**
  647.      * Replaces all the occurrences of variables with their matching values
  648.      * from the resolver using the given source buffer as a template.
  649.      * The buffer is not altered by this method.
  650.      *
  651.      * @param source  the buffer to use as a template, not changed, null returns null
  652.      * @return The result of the replace operation
  653.      */
  654.     public String replace(final StringBuffer source) {
  655.         if (source == null) {
  656.             return null;
  657.         }
  658.         final StrBuilder buf = new StrBuilder(source.length()).append(source);
  659.         substitute(buf, 0, buf.length());
  660.         return buf.toString();
  661.     }

  662.     /**
  663.      * Replaces all the occurrences of variables with their matching values
  664.      * from the resolver using the given source buffer as a template.
  665.      * The buffer is not altered by this method.
  666.      * <p>
  667.      * Only the specified portion of the buffer will be processed.
  668.      * The rest of the buffer is not processed, and is not returned.
  669.      * </p>
  670.      *
  671.      * @param source  the buffer to use as a template, not changed, null returns null
  672.      * @param offset  the start offset within the array, must be valid
  673.      * @param length  the length within the array to be processed, must be valid
  674.      * @return The result of the replace operation
  675.      */
  676.     public String replace(final StringBuffer source, final int offset, final int length) {
  677.         if (source == null) {
  678.             return null;
  679.         }
  680.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  681.         substitute(buf, 0, length);
  682.         return buf.toString();
  683.     }

  684.     /**
  685.      * Replaces all the occurrences of variables within the given source
  686.      * builder with their matching values from the resolver.
  687.      *
  688.      * @param source  the builder to replace in, updated, null returns zero
  689.      * @return true if altered
  690.      */
  691.     public boolean replaceIn(final StrBuilder source) {
  692.         if (source == null) {
  693.             return false;
  694.         }
  695.         return substitute(source, 0, source.length());
  696.     }

  697.     /**
  698.      * Replaces all the occurrences of variables within the given source
  699.      * builder with their matching values from the resolver.
  700.      * <p>
  701.      * Only the specified portion of the builder will be processed.
  702.      * The rest of the builder is not processed, but it is not deleted.
  703.      * </p>
  704.      *
  705.      * @param source  the builder to replace in, null returns zero
  706.      * @param offset  the start offset within the array, must be valid
  707.      * @param length  the length within the builder to be processed, must be valid
  708.      * @return true if altered
  709.      */
  710.     public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
  711.         if (source == null) {
  712.             return false;
  713.         }
  714.         return substitute(source, offset, length);
  715.     }

  716.     /**
  717.      * Replaces all the occurrences of variables within the given source buffer
  718.      * with their matching values from the resolver.
  719.      * The buffer is updated with the result.
  720.      *
  721.      * @param source  the buffer to replace in, updated, null returns zero
  722.      * @return true if altered
  723.      */
  724.     public boolean replaceIn(final StringBuffer source) {
  725.         if (source == null) {
  726.             return false;
  727.         }
  728.         return replaceIn(source, 0, source.length());
  729.     }

  730.     /**
  731.      * Replaces all the occurrences of variables within the given source buffer
  732.      * with their matching values from the resolver.
  733.      * The buffer is updated with the result.
  734.      * <p>
  735.      * Only the specified portion of the buffer will be processed.
  736.      * The rest of the buffer is not processed, but it is not deleted.
  737.      * </p>
  738.      *
  739.      * @param source  the buffer to replace in, updated, null returns zero
  740.      * @param offset  the start offset within the array, must be valid
  741.      * @param length  the length within the buffer to be processed, must be valid
  742.      * @return true if altered
  743.      */
  744.     public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
  745.         if (source == null) {
  746.             return false;
  747.         }
  748.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  749.         if (!substitute(buf, 0, length)) {
  750.             return false;
  751.         }
  752.         source.replace(offset, offset + length, buf.toString());
  753.         return true;
  754.     }

  755.     /**
  756.      * Replaces all the occurrences of variables within the given source buffer
  757.      * with their matching values from the resolver.
  758.      * The buffer is updated with the result.
  759.      *
  760.      * @param source  the buffer to replace in, updated, null returns zero
  761.      * @return true if altered
  762.      */
  763.     public boolean replaceIn(final StringBuilder source) {
  764.         if (source == null) {
  765.             return false;
  766.         }
  767.         return replaceIn(source, 0, source.length());
  768.     }

  769.     /**
  770.      * Replaces all the occurrences of variables within the given source builder
  771.      * with their matching values from the resolver.
  772.      * The builder is updated with the result.
  773.      * <p>
  774.      * Only the specified portion of the buffer will be processed.
  775.      * The rest of the buffer is not processed, but it is not deleted.
  776.      * </p>
  777.      *
  778.      * @param source  the buffer to replace in, updated, null returns zero
  779.      * @param offset  the start offset within the array, must be valid
  780.      * @param length  the length within the buffer to be processed, must be valid
  781.      * @return true if altered
  782.      */
  783.     public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
  784.         if (source == null) {
  785.             return false;
  786.         }
  787.         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
  788.         if (!substitute(buf, 0, length)) {
  789.             return false;
  790.         }
  791.         source.replace(offset, offset + length, buf.toString());
  792.         return true;
  793.     }

  794.     /**
  795.      * Internal method that resolves the value of a variable.
  796.      * <p>
  797.      * Most users of this class do not need to call this method. This method is
  798.      * called automatically by the substitution process.
  799.      * </p>
  800.      * <p>
  801.      * Writers of subclasses can override this method if they need to alter
  802.      * how each substitution occurs. The method is passed the variable's name
  803.      * and must return the corresponding value. This implementation uses the
  804.      * {@link #getVariableResolver()} with the variable's name as the key.
  805.      * </p>
  806.      *
  807.      * @param variableName  the name of the variable, not null
  808.      * @param buf  the buffer where the substitution is occurring, not null
  809.      * @param startPos  the start position of the variable including the prefix, valid
  810.      * @param endPos  the end position of the variable including the suffix, valid
  811.      * @return The variable's value or <strong>null</strong> if the variable is unknown
  812.      */
  813.     protected String resolveVariable(final String variableName,
  814.                                      final StrBuilder buf,
  815.                                      final int startPos,
  816.                                      final int endPos) {
  817.         final StrLookup<?> resolver = getVariableResolver();
  818.         if (resolver == null) {
  819.             return null;
  820.         }
  821.         return resolver.lookup(variableName);
  822.     }

  823.     /**
  824.      * Sets a flag whether substitution is done in variable values (recursive).
  825.      *
  826.      * @param disableSubstitutionInValues true if substitution in variable value are disabled
  827.      * @since 1.2
  828.      */
  829.     public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
  830.         this.disableSubstitutionInValues = disableSubstitutionInValues;
  831.     }

  832.     /**
  833.      * Sets a flag whether substitution is done in variable names. If set to
  834.      * <strong>true</strong>, the names of variables can contain other variables which are
  835.      * processed first before the original variable is evaluated, e.g.
  836.      * {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
  837.      *
  838.      * @param enableSubstitutionInVariables the new value of the flag
  839.      */
  840.     public void setEnableSubstitutionInVariables(
  841.             final boolean enableSubstitutionInVariables) {
  842.         this.enableSubstitutionInVariables = enableSubstitutionInVariables;
  843.     }

  844.     /**
  845.      * Sets the escape character.
  846.      * If this character is placed before a variable reference in the source
  847.      * text, this variable will be ignored.
  848.      *
  849.      * @param escapeCharacter  the escape character (0 for disabling escaping)
  850.      */
  851.     public void setEscapeChar(final char escapeCharacter) {
  852.         this.escapeChar = escapeCharacter;
  853.     }

  854.     /**
  855.      * Sets a flag controlling whether escapes are preserved during
  856.      * substitution.  If set to <strong>true</strong>, the escape character is retained
  857.      * during substitution (e.g. {@code $${this-is-escaped}} remains
  858.      * {@code $${this-is-escaped}}).  If set to <strong>false</strong>, the escape
  859.      * character is removed during substitution (e.g.
  860.      * {@code $${this-is-escaped}} becomes
  861.      * {@code ${this-is-escaped}}).  The default value is <strong>false</strong>
  862.      *
  863.      * @param preserveEscapes true if escapes are to be preserved
  864.      */
  865.     public void setPreserveEscapes(final boolean preserveEscapes) {
  866.         this.preserveEscapes = preserveEscapes;
  867.     }

  868.     /**
  869.      * Sets the variable default value delimiter to use.
  870.      * <p>
  871.      * The variable default value delimiter is the character or characters that delimit the
  872.      * variable name and the variable default value. This method allows a single character
  873.      * variable default value delimiter to be easily set.
  874.      * </p>
  875.      *
  876.      * @param valueDelimiter  the variable default value delimiter character to use
  877.      * @return this, to enable chaining
  878.      */
  879.     public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
  880.         return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
  881.     }

  882.     /**
  883.      * Sets the variable default value delimiter to use.
  884.      * <p>
  885.      * The variable default value delimiter is the character or characters that delimit the
  886.      * variable name and the variable default value. This method allows a string
  887.      * variable default value delimiter to be easily set.
  888.      * </p>
  889.      * <p>
  890.      * If the {@code valueDelimiter} is null or empty string, then the variable default
  891.      * value resolution becomes disabled.
  892.      * </p>
  893.      *
  894.      * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
  895.      * @return this, to enable chaining
  896.      */
  897.     public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
  898.         if (valueDelimiter == null || valueDelimiter.isEmpty()) {
  899.             setValueDelimiterMatcher(null);
  900.             return this;
  901.         }
  902.         return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
  903.     }

  904.     /**
  905.      * Sets the variable default value delimiter matcher to use.
  906.      * <p>
  907.      * The variable default value delimiter is the character or characters that delimit the
  908.      * variable name and the variable default value. This delimiter is expressed in terms of a matcher
  909.      * allowing advanced variable default value delimiter matches.
  910.      * </p>
  911.      * <p>
  912.      * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution
  913.      * becomes disabled.
  914.      * </p>
  915.      *
  916.      * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
  917.      * @return this, to enable chaining
  918.      */
  919.     public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
  920.         this.valueDelimiterMatcher = valueDelimiterMatcher;
  921.         return this;
  922.     }

  923.     /**
  924.      * Sets the variable prefix to use.
  925.      * <p>
  926.      * The variable prefix is the character or characters that identify the
  927.      * start of a variable. This method allows a single character prefix to
  928.      * be easily set.
  929.      * </p>
  930.      *
  931.      * @param prefix  the prefix character to use
  932.      * @return this, to enable chaining
  933.      */
  934.     public StrSubstitutor setVariablePrefix(final char prefix) {
  935.         return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
  936.     }

  937.     /**
  938.      * Sets the variable prefix to use.
  939.      * <p>
  940.      * The variable prefix is the character or characters that identify the
  941.      * start of a variable. This method allows a string prefix to be easily set.
  942.      * </p>
  943.      *
  944.      * @param prefix  the prefix for variables, not null
  945.      * @return this, to enable chaining
  946.      * @throws IllegalArgumentException if the prefix is null
  947.      */
  948.     public StrSubstitutor setVariablePrefix(final String prefix) {
  949.         Validate.isTrue(prefix != null, "Variable prefix must not be null!");
  950.         return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
  951.     }

  952.     /**
  953.      * Sets the variable prefix matcher currently in use.
  954.      * <p>
  955.      * The variable prefix is the character or characters that identify the
  956.      * start of a variable. This prefix is expressed in terms of a matcher
  957.      * allowing advanced prefix matches.
  958.      * </p>
  959.      *
  960.      * @param prefixMatcher  the prefix matcher to use, null ignored
  961.      * @return this, to enable chaining
  962.      * @throws IllegalArgumentException if the prefix matcher is null
  963.      */
  964.     public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
  965.         Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
  966.         this.prefixMatcher = prefixMatcher;
  967.         return this;
  968.     }

  969.     /**
  970.      * Sets the VariableResolver that is used to lookup variables.
  971.      *
  972.      * @param variableResolver  the VariableResolver
  973.      */
  974.     public void setVariableResolver(final StrLookup<?> variableResolver) {
  975.         this.variableResolver = variableResolver;
  976.     }

  977.     /**
  978.      * Sets the variable suffix to use.
  979.      * <p>
  980.      * The variable suffix is the character or characters that identify the
  981.      * end of a variable. This method allows a single character suffix to
  982.      * be easily set.
  983.      * </p>
  984.      *
  985.      * @param suffix  the suffix character to use
  986.      * @return this, to enable chaining
  987.      */
  988.     public StrSubstitutor setVariableSuffix(final char suffix) {
  989.         return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
  990.     }

  991.     /**
  992.      * Sets the variable suffix to use.
  993.      * <p>
  994.      * The variable suffix is the character or characters that identify the
  995.      * end of a variable. This method allows a string suffix to be easily set.
  996.      * </p>
  997.      *
  998.      * @param suffix  the suffix for variables, not null
  999.      * @return this, to enable chaining
  1000.      * @throws IllegalArgumentException if the suffix is null
  1001.      */
  1002.     public StrSubstitutor setVariableSuffix(final String suffix) {
  1003.         Validate.isTrue(suffix != null, "Variable suffix must not be null!");
  1004.         return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
  1005.     }

  1006.     /**
  1007.      * Sets the variable suffix matcher currently in use.
  1008.      * <p>
  1009.      * The variable suffix is the character or characters that identify the
  1010.      * end of a variable. This suffix is expressed in terms of a matcher
  1011.      * allowing advanced suffix matches.
  1012.      * </p>
  1013.      *
  1014.      * @param suffixMatcher  the suffix matcher to use, null ignored
  1015.      * @return this, to enable chaining
  1016.      * @throws IllegalArgumentException if the suffix matcher is null
  1017.      */
  1018.     public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
  1019.         Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
  1020.         this.suffixMatcher = suffixMatcher;
  1021.         return this;
  1022.     }

  1023.     /**
  1024.      * Internal method that substitutes the variables.
  1025.      * <p>
  1026.      * Most users of this class do not need to call this method. This method will
  1027.      * be called automatically by another (public) method.
  1028.      * </p>
  1029.      * <p>
  1030.      * Writers of subclasses can override this method if they need access to
  1031.      * the substitution process at the start or end.
  1032.      * </p>
  1033.      *
  1034.      * @param buf  the string builder to substitute into, not null
  1035.      * @param offset  the start offset within the builder, must be valid
  1036.      * @param length  the length within the builder to be processed, must be valid
  1037.      * @return true if altered
  1038.      */
  1039.     protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
  1040.         return substitute(buf, offset, length, null) > 0;
  1041.     }

  1042.     /**
  1043.      * Recursive handler for multiple levels of interpolation. This is the main
  1044.      * interpolation method, which resolves the values of all variable references
  1045.      * contained in the passed in text.
  1046.      *
  1047.      * @param buf  the string builder to substitute into, not null
  1048.      * @param offset  the start offset within the builder, must be valid
  1049.      * @param length  the length within the builder to be processed, must be valid
  1050.      * @param priorVariables  the stack keeping track of the replaced variables, may be null
  1051.      * @return The length change that occurs, unless priorVariables is null when the int
  1052.      *  represents a boolean flag as to whether any change occurred.
  1053.      */
  1054.     private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
  1055.         final StrMatcher pfxMatcher = getVariablePrefixMatcher();
  1056.         final StrMatcher suffMatcher = getVariableSuffixMatcher();
  1057.         final char escape = getEscapeChar();
  1058.         final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
  1059.         final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
  1060.         final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();

  1061.         final boolean top = priorVariables == null;
  1062.         boolean altered = false;
  1063.         int lengthChange = 0;
  1064.         char[] chars = buf.buffer;
  1065.         int bufEnd = offset + length;
  1066.         int pos = offset;
  1067.         while (pos < bufEnd) {
  1068.             final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
  1069.                     bufEnd);
  1070.             if (startMatchLen == 0) {
  1071.                 pos++;
  1072.             } else // found variable start marker
  1073.             if (pos > offset && chars[pos - 1] == escape) {
  1074.                 // escaped
  1075.                 if (preserveEscapes) {
  1076.                     pos++;
  1077.                     continue;
  1078.                 }
  1079.                 buf.deleteCharAt(pos - 1);
  1080.                 chars = buf.buffer; // in case buffer was altered
  1081.                 lengthChange--;
  1082.                 altered = true;
  1083.                 bufEnd--;
  1084.             } else {
  1085.                 // find suffix
  1086.                 final int startPos = pos;
  1087.                 pos += startMatchLen;
  1088.                 int endMatchLen = 0;
  1089.                 int nestedVarCount = 0;
  1090.                 while (pos < bufEnd) {
  1091.                     if (substitutionInVariablesEnabled
  1092.                             && pfxMatcher.isMatch(chars,
  1093.                                     pos, offset, bufEnd) != 0) {
  1094.                         // found a nested variable start
  1095.                         endMatchLen = pfxMatcher.isMatch(chars,
  1096.                                 pos, offset, bufEnd);
  1097.                         nestedVarCount++;
  1098.                         pos += endMatchLen;
  1099.                         continue;
  1100.                     }

  1101.                     endMatchLen = suffMatcher.isMatch(chars, pos, offset,
  1102.                             bufEnd);
  1103.                     if (endMatchLen == 0) {
  1104.                         pos++;
  1105.                     } else {
  1106.                         // found variable end marker
  1107.                         if (nestedVarCount == 0) {
  1108.                             String varNameExpr = new String(chars, startPos
  1109.                                     + startMatchLen, pos - startPos
  1110.                                     - startMatchLen);
  1111.                             if (substitutionInVariablesEnabled) {
  1112.                                 final StrBuilder bufName = new StrBuilder(varNameExpr);
  1113.                                 substitute(bufName, 0, bufName.length());
  1114.                                 varNameExpr = bufName.toString();
  1115.                             }
  1116.                             pos += endMatchLen;
  1117.                             final int endPos = pos;

  1118.                             String varName = varNameExpr;
  1119.                             String varDefaultValue = null;

  1120.                             if (valueDelimMatcher != null) {
  1121.                                 final char[] varNameExprChars = varNameExpr.toCharArray();
  1122.                                 int valueDelimiterMatchLen = 0;
  1123.                                 for (int i = 0; i < varNameExprChars.length; i++) {
  1124.                                     // if there's any nested variable when nested variable substitution disabled,
  1125.                                     // then stop resolving name and default value.
  1126.                                     if (!substitutionInVariablesEnabled
  1127.                                             && pfxMatcher.isMatch(varNameExprChars,
  1128.                                                                     i,
  1129.                                                                     i,
  1130.                                                                     varNameExprChars.length) != 0) {
  1131.                                         break;
  1132.                                     }
  1133.                                     if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
  1134.                                         valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
  1135.                                         varName = varNameExpr.substring(0, i);
  1136.                                         varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
  1137.                                         break;
  1138.                                     }
  1139.                                 }
  1140.                             }

  1141.                             // on the first call initialize priorVariables
  1142.                             if (priorVariables == null) {
  1143.                                 priorVariables = new ArrayList<>();
  1144.                                 priorVariables.add(new String(chars,
  1145.                                         offset, length));
  1146.                             }

  1147.                             // handle cyclic substitution
  1148.                             checkCyclicSubstitution(varName, priorVariables);
  1149.                             priorVariables.add(varName);

  1150.                             // resolve the variable
  1151.                             String varValue = resolveVariable(varName, buf,
  1152.                                     startPos, endPos);
  1153.                             if (varValue == null) {
  1154.                                 varValue = varDefaultValue;
  1155.                             }
  1156.                             if (varValue != null) {
  1157.                                 final int varLen = varValue.length();
  1158.                                 buf.replace(startPos, endPos, varValue);
  1159.                                 altered = true;
  1160.                                 int change = 0;
  1161.                                 if (!substitutionInValuesDisabled) { // recursive replace
  1162.                                     change = substitute(buf, startPos,
  1163.                                         varLen, priorVariables);
  1164.                                 }
  1165.                                 change = change
  1166.                                     + varLen - (endPos - startPos);
  1167.                                 pos += change;
  1168.                                 bufEnd += change;
  1169.                                 lengthChange += change;
  1170.                                 chars = buf.buffer; // in case buffer was
  1171.                                                     // altered
  1172.                             }

  1173.                             // remove variable from the cyclic stack
  1174.                             priorVariables
  1175.                                     .remove(priorVariables.size() - 1);
  1176.                             break;
  1177.                         }
  1178.                         nestedVarCount--;
  1179.                         pos += endMatchLen;
  1180.                     }
  1181.                 }
  1182.             }
  1183.         }
  1184.         if (top) {
  1185.             return altered ? 1 : 0;
  1186.         }
  1187.         return lengthChange;
  1188.     }
  1189. }