StringSubstitutor.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.Objects;
  22. import java.util.Properties;
  23. import java.util.function.Function;
  24. import java.util.stream.Collectors;

  25. import org.apache.commons.lang3.Validate;
  26. import org.apache.commons.text.lookup.StringLookup;
  27. import org.apache.commons.text.lookup.StringLookupFactory;
  28. import org.apache.commons.text.matcher.StringMatcher;
  29. import org.apache.commons.text.matcher.StringMatcherFactory;

  30. /**
  31.  * Substitutes variables within a string by values.
  32.  * <p>
  33.  * This class takes a piece of text and substitutes all the variables within it. The default definition of a variable is
  34.  * {@code ${variableName}}. The prefix and suffix can be changed via constructors and set methods.
  35.  * </p>
  36.  * <p>
  37.  * Variable values are typically resolved from a map, but could also be resolved from system properties, or by supplying
  38.  * a custom variable resolver.
  39.  * </p>
  40.  * <h2>Using System Properties</h2>
  41.  * <p>
  42.  * The simplest example is to use this class to replace Java System properties. For example:
  43.  * </p>
  44.  *
  45.  * <pre>
  46.  * StringSubstitutor
  47.  *     .replaceSystemProperties("You are running with java.version = ${java.version} and os.name = ${os.name}.");
  48.  * </pre>
  49.  *
  50.  * <h2>Using a Custom Map</h2>
  51.  * <p>
  52.  * Typical usage of this class follows the following pattern:
  53.  * </p>
  54.  * <ul>
  55.  * <li>Create and initialize a StringSubstitutor with the map that contains the values for the variables you want to
  56.  * make available.</li>
  57.  * <li>Optionally set attributes like variable prefix, variable suffix, default value delimiter, and so on.</li>
  58.  * <li>Call the {@code replace()} method with in the source text for interpolation.</li>
  59.  * <li>The returned text contains all variable references (as long as their values are known) as resolved.</li>
  60.  * </ul>
  61.  * <p>
  62.  * For example:
  63.  * </p>
  64.  *
  65.  * <pre>
  66.  * // Build map
  67.  * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
  68.  * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
  69.  * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
  70.  * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
  71.  *
  72.  * // Build StringSubstitutor
  73.  * StringSubstitutor sub = new StringSubstitutor(valuesMap);
  74.  *
  75.  * // Replace
  76.  * String resolvedString = sub.replace(templateString);
  77.  * </pre>
  78.  *
  79.  * <p>
  80.  * yielding:
  81.  * </p>
  82.  *
  83.  * <pre>
  84.  * "The quick brown fox jumped over the lazy dog."
  85.  * </pre>
  86.  *
  87.  * <h2>Providing Default Values</h2>
  88.  * <p>
  89.  * You can set a default value for unresolved variables. The default value for a variable can be appended to the
  90.  * variable name after the variable default value delimiter. The default value of the variable default value delimiter
  91.  * is ":-", as in bash and other *nix shells.
  92.  * </p>
  93.  * <p>
  94.  * You can set the variable value delimiter with {@link #setValueDelimiterMatcher(StringMatcher)},
  95.  * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
  96.  * </p>
  97.  * <p>
  98.  * For example:
  99.  * </p>
  100.  *
  101.  * <pre>
  102.  * // Build map
  103.  * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
  104.  * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
  105.  * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
  106.  * String templateString = &quot;The ${animal} jumped over the ${target} ${undefined.number:-1234567890} times.&quot;;
  107.  *
  108.  * // Build StringSubstitutor
  109.  * StringSubstitutor sub = new StringSubstitutor(valuesMap);
  110.  *
  111.  * // Replace
  112.  * String resolvedString = sub.replace(templateString);
  113.  * </pre>
  114.  *
  115.  * <p>
  116.  * yielding:
  117.  * </p>
  118.  *
  119.  * <pre>
  120.  * "The quick brown fox jumped over the lazy dog 1234567890 times."
  121.  * </pre>
  122.  *
  123.  * <p>
  124.  * {@code StringSubstitutor} supports throwing exceptions for unresolved variables, you enable this by setting calling
  125.  * {@link #setEnableUndefinedVariableException(boolean)} with {@code true}.
  126.  * </p>
  127.  *
  128.  * <h2>Reusing Instances</h2>
  129.  * <p>
  130.  * Static shortcut methods cover the most common use cases. If multiple replace operations are to be performed, creating
  131.  * and reusing an instance of this class will be more efficient.
  132.  * </p>
  133.  *
  134.  * <h2>Using Interpolation</h2>
  135.  * <p>
  136.  * The default interpolator lets you use string lookups like:
  137.  * </p>
  138.  *
  139.  * <pre>
  140.  * final StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
  141.  * final String text = interpolator.replace(
  142.  *       "Base64 Decoder:        ${base64Decoder:SGVsbG9Xb3JsZCE=}\n"
  143.  *     + "Base64 Encoder:        ${base64Encoder:HelloWorld!}\n"
  144.  *     + "Java Constant:         ${const:java.awt.event.KeyEvent.VK_ESCAPE}\n"
  145.  *     + "Date:                  ${date:yyyy-MM-dd}\n"
  146.  *     + "Environment Variable:  ${env:USERNAME}\n"
  147.  *     + "File Content:          ${file:UTF-8:src/test/resources/document.properties}\n"
  148.  *     + "Java:                  ${java:version}\n"
  149.  *     + "Localhost:             ${localhost:canonical-name}\n"
  150.  *     + "Properties File:       ${properties:src/test/resources/document.properties::mykey}\n"
  151.  *     + "Resource Bundle:       ${resourceBundle:org.apache.commons.text.example.testResourceBundleLookup:mykey}\n"
  152.  *     + "System Property:       ${sys:user.dir}\n"
  153.  *     + "URL Decoder:           ${urlDecoder:Hello%20World%21}\n"
  154.  *     + "URL Encoder:           ${urlEncoder:Hello World!}\n"
  155.  *     + "XML XPath:             ${xml:src/test/resources/document.xml:/root/path/to/node}\n");
  156.  * </pre>
  157.  * <p>
  158.  * For documentation and a full list of available lookups, see {@link StringLookupFactory}.
  159.  * </p>
  160.  * <p><strong>NOTE:</strong> The list of lookups available by default in {@link #createInterpolator()} changed
  161.  * in version {@code 1.10.0}. See the {@link StringLookupFactory} documentation for details and an explanation
  162.  * on how to reproduce the previous functionality.
  163.  * </p>
  164.  *
  165.  * <h2>Using Recursive Variable Replacement</h2>
  166.  * <p>
  167.  * Variable replacement can work recursively by calling {@link #setEnableSubstitutionInVariables(boolean)} with
  168.  * {@code true}. If a variable value contains a variable then that variable will also be replaced. Cyclic replacements
  169.  * are detected and will throw an exception.
  170.  * </p>
  171.  * <p>
  172.  * You can get the replace result to contain a variable prefix. For example:
  173.  * </p>
  174.  *
  175.  * <pre>
  176.  * "The variable ${${name}} must be used."
  177.  * </pre>
  178.  *
  179.  * <p>
  180.  * If the value of the "name" variable is "x", then only the variable "name" is replaced resulting in:
  181.  * </p>
  182.  *
  183.  * <pre>
  184.  * "The variable ${x} must be used."
  185.  * </pre>
  186.  *
  187.  * <p>
  188.  * To achieve this effect there are two possibilities: Either set a different prefix and suffix for variables which do
  189.  * not conflict with the result text you want to produce. The other possibility is to use the escape character, by
  190.  * default '$'. If this character is placed before a variable reference, this reference is ignored and won't be
  191.  * replaced. For example:
  192.  * </p>
  193.  *
  194.  * <pre>
  195.  * "The variable $${${name}} must be used."
  196.  * </pre>
  197.  * <p>
  198.  * In some complex scenarios you might even want to perform substitution in the names of variables, for instance
  199.  * </p>
  200.  *
  201.  * <pre>
  202.  * ${jre-${java.specification.version}}
  203.  * </pre>
  204.  *
  205.  * <p>
  206.  * {@code StringSubstitutor} supports this recursive substitution in variable names, but it has to be enabled explicitly
  207.  * by calling {@link #setEnableSubstitutionInVariables(boolean)} with {@code true}.
  208.  * </p>
  209.  *
  210.  * <h2>Thread Safety</h2>
  211.  * <p>
  212.  * This class is <strong>not</strong> thread safe.
  213.  * </p>
  214.  *
  215.  * @since 1.3
  216.  */
  217. public class StringSubstitutor {

  218.     /**
  219.      * The low-level result of a substitution.
  220.      *
  221.      * @since 1.9
  222.      */
  223.     private static final class Result {

  224.         /** Whether the buffer is altered. */
  225.         public final boolean altered;

  226.         /** The length of change. */
  227.         public final int lengthChange;

  228.         private Result(final boolean altered, final int lengthChange) {
  229.             this.altered = altered;
  230.             this.lengthChange = lengthChange;
  231.         }

  232.         @Override
  233.         public String toString() {
  234.             return "Result [altered=" + altered + ", lengthChange=" + lengthChange + "]";
  235.         }
  236.     }

  237.     /**
  238.      * Constant for the default escape character.
  239.      */
  240.     public static final char DEFAULT_ESCAPE = '$';

  241.     /**
  242.      * The default variable default separator.
  243.      *
  244.      * @since 1.5.
  245.      */
  246.     public static final String DEFAULT_VAR_DEFAULT = ":-";

  247.     /**
  248.      * The default variable end separator.
  249.      *
  250.      * @since 1.5.
  251.      */
  252.     public static final String DEFAULT_VAR_END = "}";

  253.     /**
  254.      * The default variable start separator.
  255.      *
  256.      * @since 1.5.
  257.      */
  258.     public static final String DEFAULT_VAR_START = "${";

  259.     /**
  260.      * Constant for the default variable prefix.
  261.      */
  262.     public static final StringMatcher DEFAULT_PREFIX = StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_START);

  263.     /**
  264.      * Constant for the default variable suffix.
  265.      */
  266.     public static final StringMatcher DEFAULT_SUFFIX = StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_END);

  267.     /**
  268.      * Constant for the default value delimiter of a variable.
  269.      */
  270.     public static final StringMatcher DEFAULT_VALUE_DELIMITER = StringMatcherFactory.INSTANCE
  271.         .stringMatcher(DEFAULT_VAR_DEFAULT);

  272.     /**
  273.      * Creates a new instance using the interpolator string lookup
  274.      * {@link StringLookupFactory#interpolatorStringLookup()}.
  275.      * <p>
  276.      * This StringSubstitutor lets you perform substitutions like:
  277.      * </p>
  278.      *
  279.      * <pre>
  280.      * StringSubstitutor.createInterpolator().replace(
  281.      *   "OS name: ${sys:os.name}, user: ${env:USER}");
  282.      * </pre>
  283.      *
  284.      * <p>The table below lists the lookups available by default in the returned instance. These
  285.      * may be modified through the use of the
  286.      * {@value org.apache.commons.text.lookup.StringLookupFactory#DEFAULT_STRING_LOOKUPS_PROPERTY}
  287.      * system property, as described in the {@link StringLookupFactory} documentation.</p>
  288.      *
  289.      * <p><strong>NOTE:</strong> The list of lookups available by default changed in version {@code 1.10.0}.
  290.      * Configuration via system property (as mentioned above) may be necessary to reproduce previous functionality.
  291.      * </p>
  292.      *
  293.      * <table>
  294.      * <caption>Default Lookups</caption>
  295.      * <tr>
  296.      * <th>Key</th>
  297.      * <th>Lookup</th>
  298.      * </tr>
  299.      * <tr>
  300.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_BASE64_DECODER}</td>
  301.      * <td>{@link StringLookupFactory#base64DecoderStringLookup()}</td>
  302.      * </tr>
  303.      * <tr>
  304.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_BASE64_ENCODER}</td>
  305.      * <td>{@link StringLookupFactory#base64EncoderStringLookup()}</td>
  306.      * </tr>
  307.      * <tr>
  308.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_CONST}</td>
  309.      * <td>{@link StringLookupFactory#constantStringLookup()}</td>
  310.      * </tr>
  311.      * <tr>
  312.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_DATE}</td>
  313.      * <td>{@link StringLookupFactory#dateStringLookup()}</td>
  314.      * </tr>
  315.      * <tr>
  316.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_ENV}</td>
  317.      * <td>{@link StringLookupFactory#environmentVariableStringLookup()}</td>
  318.      * </tr>
  319.      * <tr>
  320.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_FILE}</td>
  321.      * <td>{@link StringLookupFactory#fileStringLookup()}</td>
  322.      * </tr>
  323.      * <tr>
  324.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_JAVA}</td>
  325.      * <td>{@link StringLookupFactory#javaPlatformStringLookup()}</td>
  326.      * </tr>
  327.      * <tr>
  328.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_LOCALHOST}</td>
  329.      * <td>{@link StringLookupFactory#localHostStringLookup()}</td>
  330.      * </tr>
  331.      * <tr>
  332.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_LOOPBACK_ADDRESS}</td>
  333.      * <td>{@link StringLookupFactory#loopbackAddressStringLookup()}</td>
  334.      * </tr>
  335.      * <tr>
  336.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_PROPERTIES}</td>
  337.      * <td>{@link StringLookupFactory#propertiesStringLookup()}</td>
  338.      * </tr>
  339.      * <tr>
  340.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_RESOURCE_BUNDLE}</td>
  341.      * <td>{@link StringLookupFactory#resourceBundleStringLookup()}</td>
  342.      * </tr>
  343.      * <tr>
  344.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_SYS}</td>
  345.      * <td>{@link StringLookupFactory#systemPropertyStringLookup()}</td>
  346.      * </tr>
  347.      * <tr>
  348.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_URL_DECODER}</td>
  349.      * <td>{@link StringLookupFactory#urlDecoderStringLookup()}</td>
  350.      * </tr>
  351.      * <tr>
  352.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_URL_ENCODER}</td>
  353.      * <td>{@link StringLookupFactory#urlEncoderStringLookup()}</td>
  354.      * </tr>
  355.      * <tr>
  356.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML}</td>
  357.      * <td>{@link StringLookupFactory#xmlStringLookup()}</td>
  358.      * </tr>
  359.      * <tr>
  360.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML_DECODER}</td>
  361.      * <td>{@link StringLookupFactory#xmlDecoderStringLookup()}</td>
  362.      * </tr>
  363.      * <tr>
  364.      * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML_ENCODER}</td>
  365.      * <td>{@link StringLookupFactory#xmlEncoderStringLookup()}</td>
  366.      * </tr>
  367.      * </table>
  368.      *
  369.      * @return a new instance using the interpolator string lookup.
  370.      * @see StringLookupFactory#interpolatorStringLookup()
  371.      * @since 1.8
  372.      */
  373.     public static StringSubstitutor createInterpolator() {
  374.         return new StringSubstitutor(StringLookupFactory.INSTANCE.interpolatorStringLookup());
  375.     }

  376.     /**
  377.      * Replaces all the occurrences of variables in the given source object with their matching values from the map.
  378.      *
  379.      * @param <V> the type of the values in the map
  380.      * @param source the source text containing the variables to substitute, null returns null
  381.      * @param valueMap the map with the values, may be null
  382.      * @return The result of the replace operation
  383.      * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
  384.      */
  385.     public static <V> String replace(final Object source, final Map<String, V> valueMap) {
  386.         return new StringSubstitutor(valueMap).replace(source);
  387.     }

  388.     /**
  389.      * Replaces all the occurrences of variables in the given source object with their matching values from the map.
  390.      * This method allows to specify a custom variable prefix and suffix
  391.      *
  392.      * @param <V> the type of the values in the map
  393.      * @param source the source text containing the variables to substitute, null returns null
  394.      * @param valueMap the map with the values, may be null
  395.      * @param prefix the prefix of variables, not null
  396.      * @param suffix the suffix of variables, not null
  397.      * @return The result of the replace operation
  398.      * @throws IllegalArgumentException if the prefix or suffix is null
  399.      * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
  400.      */
  401.     public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix,
  402.         final String suffix) {
  403.         return new StringSubstitutor(valueMap, prefix, suffix).replace(source);
  404.     }

  405.     /**
  406.      * Replaces all the occurrences of variables in the given source object with their matching values from the
  407.      * properties.
  408.      *
  409.      * @param source the source text containing the variables to substitute, null returns null
  410.      * @param valueProperties the properties with values, may be null
  411.      * @return The result of the replace operation
  412.      * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
  413.      */
  414.     public static String replace(final Object source, final Properties valueProperties) {
  415.         if (valueProperties == null) {
  416.             return source.toString();
  417.         }
  418.         return StringSubstitutor.replace(source,
  419.                 valueProperties.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), valueProperties::getProperty)));
  420.     }

  421.     /**
  422.      * Replaces all the occurrences of variables in the given source object with their matching values from the system
  423.      * properties.
  424.      *
  425.      * @param source the source text containing the variables to substitute, null returns null
  426.      * @return The result of the replace operation
  427.      * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
  428.      */
  429.     public static String replaceSystemProperties(final Object source) {
  430.         return new StringSubstitutor(StringLookupFactory.INSTANCE.systemPropertyStringLookup()).replace(source);
  431.     }

  432.     /**
  433.      * The flag whether substitution in variable values is disabled.
  434.      */
  435.     private boolean disableSubstitutionInValues;

  436.     /**
  437.      * The flag whether substitution in variable names is enabled.
  438.      */
  439.     private boolean enableSubstitutionInVariables;

  440.     /**
  441.      * The flag whether exception should be thrown on undefined variable.
  442.      */
  443.     private boolean failOnUndefinedVariable;

  444.     /**
  445.      * Stores the escape character.
  446.      */
  447.     private char escapeChar;

  448.     /**
  449.      * Stores the variable prefix.
  450.      */
  451.     private StringMatcher prefixMatcher;

  452.     /**
  453.      * Whether escapes should be preserved. Default is false;
  454.      */
  455.     private boolean preserveEscapes;

  456.     /**
  457.      * Stores the variable suffix.
  458.      */
  459.     private StringMatcher suffixMatcher;

  460.     /**
  461.      * Stores the default variable value delimiter.
  462.      */
  463.     private StringMatcher valueDelimiterMatcher;

  464.     /**
  465.      * Variable resolution is delegated to an implementor of {@link StringLookup}.
  466.      */
  467.     private StringLookup variableResolver;

  468.     /**
  469.      * Constructs a new instance with defaults for variable prefix and suffix and the escaping character.
  470.      */
  471.     public StringSubstitutor() {
  472.         this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
  473.     }

  474.     /**
  475.      * Constructs a new initialized instance. Uses defaults for variable prefix and suffix and the escaping
  476.      * character.
  477.      *
  478.      * @param <V> the type of the values in the map
  479.      * @param valueMap the map with the variables' values, may be null
  480.      */
  481.     public <V> StringSubstitutor(final Map<String, V> valueMap) {
  482.         this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
  483.     }

  484.     /**
  485.      * Constructs a new initialized instance. Uses a default escaping character.
  486.      *
  487.      * @param <V> the type of the values in the map
  488.      * @param valueMap the map with the variables' values, may be null
  489.      * @param prefix the prefix for variables, not null
  490.      * @param suffix the suffix for variables, not null
  491.      * @throws IllegalArgumentException if the prefix or suffix is null
  492.      */
  493.     public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
  494.         this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
  495.     }

  496.     /**
  497.      * Constructs a new initialized instance.
  498.      *
  499.      * @param <V> the type of the values in the map
  500.      * @param valueMap the map with the variables' values, may be null
  501.      * @param prefix the prefix for variables, not null
  502.      * @param suffix the suffix for variables, not null
  503.      * @param escape the escape character
  504.      * @throws IllegalArgumentException if the prefix or suffix is null
  505.      */
  506.     public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
  507.         final char escape) {
  508.         this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape);
  509.     }

  510.     /**
  511.      * Constructs a new initialized instance.
  512.      *
  513.      * @param <V> the type of the values in the map
  514.      * @param valueMap the map with the variables' values, may be null
  515.      * @param prefix the prefix for variables, not null
  516.      * @param suffix the suffix for variables, not null
  517.      * @param escape the escape character
  518.      * @param valueDelimiter the variable default value delimiter, may be null
  519.      * @throws IllegalArgumentException if the prefix or suffix is null
  520.      */
  521.     public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
  522.         final char escape, final String valueDelimiter) {
  523.         this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape, valueDelimiter);
  524.     }

  525.     /**
  526.      * Constructs a new initialized instance.
  527.      *
  528.      * @param variableResolver the variable resolver, may be null
  529.      */
  530.     public StringSubstitutor(final StringLookup variableResolver) {
  531.         this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
  532.     }

  533.     /**
  534.      * Constructs a new initialized instance.
  535.      *
  536.      * @param variableResolver the variable resolver, may be null
  537.      * @param prefix the prefix for variables, not null
  538.      * @param suffix the suffix for variables, not null
  539.      * @param escape the escape character
  540.      * @throws IllegalArgumentException if the prefix or suffix is null
  541.      */
  542.     public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
  543.         final char escape) {
  544.         setVariableResolver(variableResolver);
  545.         setVariablePrefix(prefix);
  546.         setVariableSuffix(suffix);
  547.         setEscapeChar(escape);
  548.         setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
  549.     }

  550.     /**
  551.      * Constructs a new initialized instance.
  552.      *
  553.      * @param variableResolver the variable resolver, may be null
  554.      * @param prefix the prefix for variables, not null
  555.      * @param suffix the suffix for variables, not null
  556.      * @param escape the escape character
  557.      * @param valueDelimiter the variable default value delimiter string, may be null
  558.      * @throws IllegalArgumentException if the prefix or suffix is null
  559.      */
  560.     public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
  561.         final char escape, final String valueDelimiter) {
  562.         setVariableResolver(variableResolver);
  563.         setVariablePrefix(prefix);
  564.         setVariableSuffix(suffix);
  565.         setEscapeChar(escape);
  566.         setValueDelimiter(valueDelimiter);
  567.     }

  568.     /**
  569.      * Constructs a new initialized instance.
  570.      *
  571.      * @param variableResolver the variable resolver, may be null
  572.      * @param prefixMatcher the prefix for variables, not null
  573.      * @param suffixMatcher the suffix for variables, not null
  574.      * @param escape the escape character
  575.      * @throws IllegalArgumentException if the prefix or suffix is null
  576.      */
  577.     public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
  578.         final StringMatcher suffixMatcher, final char escape) {
  579.         this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
  580.     }

  581.     /**
  582.      * Constructs a new initialized instance.
  583.      *
  584.      * @param variableResolver the variable resolver, may be null
  585.      * @param prefixMatcher the prefix for variables, not null
  586.      * @param suffixMatcher the suffix for variables, not null
  587.      * @param escape the escape character
  588.      * @param valueDelimiterMatcher the variable default value delimiter matcher, may be null
  589.      * @throws IllegalArgumentException if the prefix or suffix is null
  590.      */
  591.     public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
  592.         final StringMatcher suffixMatcher, final char escape, final StringMatcher valueDelimiterMatcher) {
  593.         setVariableResolver(variableResolver);
  594.         setVariablePrefixMatcher(prefixMatcher);
  595.         setVariableSuffixMatcher(suffixMatcher);
  596.         setEscapeChar(escape);
  597.         setValueDelimiterMatcher(valueDelimiterMatcher);
  598.     }

  599.     /**
  600.      * Creates a new instance based on the given StringSubstitutor.
  601.      *
  602.      * @param other The StringSubstitutor used as the source.
  603.      * @since 1.9
  604.      */
  605.     public StringSubstitutor(final StringSubstitutor other) {
  606.         disableSubstitutionInValues = other.isDisableSubstitutionInValues();
  607.         enableSubstitutionInVariables = other.isEnableSubstitutionInVariables();
  608.         failOnUndefinedVariable = other.isEnableUndefinedVariableException();
  609.         escapeChar = other.getEscapeChar();
  610.         prefixMatcher = other.getVariablePrefixMatcher();
  611.         preserveEscapes = other.isPreserveEscapes();
  612.         suffixMatcher = other.getVariableSuffixMatcher();
  613.         valueDelimiterMatcher = other.getValueDelimiterMatcher();
  614.         variableResolver = other.getStringLookup();
  615.     }

  616.     /**
  617.      * Checks if the specified variable is already in the stack (list) of variables.
  618.      *
  619.      * @param varName the variable name to check
  620.      * @param priorVariables the list of prior variables
  621.      */
  622.     private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
  623.         if (!priorVariables.contains(varName)) {
  624.             return;
  625.         }
  626.         final TextStringBuilder buf = new TextStringBuilder(256);
  627.         buf.append("Infinite loop in property interpolation of ");
  628.         buf.append(priorVariables.remove(0));
  629.         buf.append(": ");
  630.         buf.appendWithSeparators(priorVariables, "->");
  631.         throw new IllegalStateException(buf.toString());
  632.     }

  633.     // Escape
  634.     /**
  635.      * Returns the escape character.
  636.      *
  637.      * @return The character used for escaping variable references
  638.      */
  639.     public char getEscapeChar() {
  640.         return escapeChar;
  641.     }

  642.     /**
  643.      * Gets the StringLookup that is used to lookup variables.
  644.      *
  645.      * @return The StringLookup
  646.      */
  647.     public StringLookup getStringLookup() {
  648.         return variableResolver;
  649.     }

  650.     /**
  651.      * Gets the variable default value delimiter matcher currently in use.
  652.      * <p>
  653.      * The variable default value delimiter is the character or characters that delimit the variable name and the
  654.      * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
  655.      * value delimiter matches.
  656.      * </p>
  657.      * <p>
  658.      * If it returns null, then the variable default value resolution is disabled.
  659.      *
  660.      * @return The variable default value delimiter matcher in use, may be null
  661.      */
  662.     public StringMatcher getValueDelimiterMatcher() {
  663.         return valueDelimiterMatcher;
  664.     }

  665.     /**
  666.      * Gets the variable prefix matcher currently in use.
  667.      * <p>
  668.      * The variable prefix is the character or characters that identify the start of a variable. This prefix is
  669.      * expressed in terms of a matcher allowing advanced prefix matches.
  670.      * </p>
  671.      *
  672.      * @return The prefix matcher in use
  673.      */
  674.     public StringMatcher getVariablePrefixMatcher() {
  675.         return prefixMatcher;
  676.     }

  677.     /**
  678.      * Gets the variable suffix matcher currently in use.
  679.      * <p>
  680.      * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
  681.      * in terms of a matcher allowing advanced suffix matches.
  682.      * </p>
  683.      *
  684.      * @return The suffix matcher in use
  685.      */
  686.     public StringMatcher getVariableSuffixMatcher() {
  687.         return suffixMatcher;
  688.     }

  689.     /**
  690.      * Returns a flag whether substitution is disabled in variable values.If set to <strong>true</strong>, the values of variables
  691.      * can contain other variables will not be processed and substituted original variable is evaluated, e.g.
  692.      *
  693.      * <pre>
  694.      * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
  695.      * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
  696.      * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
  697.      * String templateString = &quot;Hi ${name}&quot;;
  698.      * StrSubstitutor sub = new StrSubstitutor(valuesMap);
  699.      * String resolvedString = sub.replace(templateString);
  700.      * </pre>
  701.      *
  702.      * yielding:
  703.      *
  704.      * <pre>
  705.      *      Hi Douglas ${surname}
  706.      * </pre>
  707.      *
  708.      * @return The substitution in variable values flag
  709.      */
  710.     public boolean isDisableSubstitutionInValues() {
  711.         return disableSubstitutionInValues;
  712.     }

  713.     /**
  714.      * Returns a flag whether substitution is done in variable names.
  715.      *
  716.      * @return The substitution in variable names flag
  717.      */
  718.     public boolean isEnableSubstitutionInVariables() {
  719.         return enableSubstitutionInVariables;
  720.     }

  721.     /**
  722.      * Returns a flag whether exception can be thrown upon undefined variable.
  723.      *
  724.      * @return The fail on undefined variable flag
  725.      */
  726.     public boolean isEnableUndefinedVariableException() {
  727.         return failOnUndefinedVariable;
  728.     }

  729.     /**
  730.      * Returns the flag controlling whether escapes are preserved during substitution.
  731.      *
  732.      * @return The preserve escape flag
  733.      */
  734.     public boolean isPreserveEscapes() {
  735.         return preserveEscapes;
  736.     }

  737.     /**
  738.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  739.      * array as a template. The array is not altered by this method.
  740.      *
  741.      * @param source the character array to replace in, not altered, null returns null
  742.      * @return The result of the replace operation
  743.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  744.      */
  745.     public String replace(final char[] source) {
  746.         if (source == null) {
  747.             return null;
  748.         }
  749.         final TextStringBuilder buf = new TextStringBuilder(source.length).append(source);
  750.         substitute(buf, 0, source.length);
  751.         return buf.toString();
  752.     }

  753.     /**
  754.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  755.      * array as a template. The array is not altered by this method.
  756.      * <p>
  757.      * Only the specified portion of the array will be processed. The rest of the array is not processed, and is not
  758.      * returned.
  759.      * </p>
  760.      *
  761.      * @param source the character array to replace in, not altered, null returns null
  762.      * @param offset the start offset within the array, must be valid
  763.      * @param length the length within the array to be processed, must be valid
  764.      * @return The result of the replace operation
  765.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  766.      * @throws StringIndexOutOfBoundsException if {@code offset} is not in the
  767.      *  range {@code 0 <= offset <= chars.length}
  768.      * @throws StringIndexOutOfBoundsException if {@code length < 0}
  769.      * @throws StringIndexOutOfBoundsException if {@code offset + length > chars.length}
  770.      */
  771.     public String replace(final char[] source, final int offset, final int length) {
  772.         if (source == null) {
  773.             return null;
  774.         }
  775.         final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
  776.         substitute(buf, 0, length);
  777.         return buf.toString();
  778.     }

  779.     /**
  780.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
  781.      * a template. The source is not altered by this method.
  782.      *
  783.      * @param source the buffer to use as a template, not changed, null returns null
  784.      * @return The result of the replace operation
  785.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  786.      */
  787.     public String replace(final CharSequence source) {
  788.         if (source == null) {
  789.             return null;
  790.         }
  791.         return replace(source, 0, source.length());
  792.     }

  793.     /**
  794.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
  795.      * a template. The source is not altered by this method.
  796.      * <p>
  797.      * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
  798.      * returned.
  799.      * </p>
  800.      *
  801.      * @param source the buffer to use as a template, not changed, null returns null
  802.      * @param offset the start offset within the array, must be valid
  803.      * @param length the length within the array to be processed, must be valid
  804.      * @return The result of the replace operation
  805.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  806.      */
  807.     public String replace(final CharSequence source, final int offset, final int length) {
  808.         if (source == null) {
  809.             return null;
  810.         }
  811.         final TextStringBuilder buf = new TextStringBuilder(length).append(source.toString(), offset, length);
  812.         substitute(buf, 0, length);
  813.         return buf.toString();
  814.     }

  815.     /**
  816.      * Replaces all the occurrences of variables in the given source object with their matching values from the
  817.      * resolver. The input source object is converted to a string using {@code toString} and is not altered.
  818.      *
  819.      * @param source the source to replace in, null returns null
  820.      * @return The result of the replace operation
  821.      * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
  822.      */
  823.     public String replace(final Object source) {
  824.         if (source == null) {
  825.             return null;
  826.         }
  827.         final TextStringBuilder buf = new TextStringBuilder().append(source);
  828.         substitute(buf, 0, buf.length());
  829.         return buf.toString();
  830.     }

  831.     /**
  832.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  833.      * string as a template.
  834.      *
  835.      * @param source the string to replace in, null returns null
  836.      * @return The result of the replace operation
  837.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  838.      */
  839.     public String replace(final String source) {
  840.         if (source == null) {
  841.             return null;
  842.         }
  843.         final TextStringBuilder buf = new TextStringBuilder(source);
  844.         if (!substitute(buf, 0, source.length())) {
  845.             return source;
  846.         }
  847.         return buf.toString();
  848.     }

  849.     /**
  850.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  851.      * string as a template.
  852.      * <p>
  853.      * Only the specified portion of the string will be processed. The rest of the string is not processed, and is not
  854.      * returned.
  855.      * </p>
  856.      *
  857.      * @param source the string to replace in, null returns null
  858.      * @param offset the start offset within the source, must be valid
  859.      * @param length the length within the source to be processed, must be valid
  860.      * @return The result of the replace operation
  861.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  862.      * @throws StringIndexOutOfBoundsException if {@code offset} is not in the
  863.      *  range {@code 0 <= offset <= source.length()}
  864.      * @throws StringIndexOutOfBoundsException if {@code length < 0}
  865.      * @throws StringIndexOutOfBoundsException if {@code offset + length > source.length()}
  866.      */
  867.     public String replace(final String source, final int offset, final int length) {
  868.         if (source == null) {
  869.             return null;
  870.         }
  871.         final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
  872.         if (!substitute(buf, 0, length)) {
  873.             return source.substring(offset, offset + length);
  874.         }
  875.         return buf.toString();
  876.     }

  877.     /**
  878.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  879.      * buffer as a template. The buffer is not altered by this method.
  880.      *
  881.      * @param source the buffer to use as a template, not changed, null returns null
  882.      * @return The result of the replace operation
  883.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  884.      */
  885.     public String replace(final StringBuffer source) {
  886.         if (source == null) {
  887.             return null;
  888.         }
  889.         final TextStringBuilder buf = new TextStringBuilder(source.length()).append(source);
  890.         substitute(buf, 0, buf.length());
  891.         return buf.toString();
  892.     }

  893.     /**
  894.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  895.      * buffer as a template. The buffer is not altered by this method.
  896.      * <p>
  897.      * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
  898.      * returned.
  899.      * </p>
  900.      *
  901.      * @param source the buffer to use as a template, not changed, null returns null
  902.      * @param offset the start offset within the source, must be valid
  903.      * @param length the length within the source to be processed, must be valid
  904.      * @return The result of the replace operation
  905.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  906.      */
  907.     public String replace(final StringBuffer source, final int offset, final int length) {
  908.         if (source == null) {
  909.             return null;
  910.         }
  911.         final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
  912.         substitute(buf, 0, length);
  913.         return buf.toString();
  914.     }

  915.     /**
  916.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  917.      * builder as a template. The builder is not altered by this method.
  918.      *
  919.      * @param source the builder to use as a template, not changed, null returns null
  920.      * @return The result of the replace operation
  921.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  922.      */
  923.     public String replace(final TextStringBuilder source) {
  924.         if (source == null) {
  925.             return null;
  926.         }
  927.         final TextStringBuilder builder = new TextStringBuilder(source.length()).append(source);
  928.         substitute(builder, 0, builder.length());
  929.         return builder.toString();
  930.     }

  931.     /**
  932.      * Replaces all the occurrences of variables with their matching values from the resolver using the given source
  933.      * builder as a template. The builder is not altered by this method.
  934.      * <p>
  935.      * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not
  936.      * returned.
  937.      * </p>
  938.      *
  939.      * @param source the builder to use as a template, not changed, null returns null
  940.      * @param offset the start offset within the source, must be valid
  941.      * @param length the length within the source to be processed, must be valid
  942.      * @return The result of the replace operation
  943.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  944.      */
  945.     public String replace(final TextStringBuilder source, final int offset, final int length) {
  946.         if (source == null) {
  947.             return null;
  948.         }
  949.         final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
  950.         substitute(buf, 0, length);
  951.         return buf.toString();
  952.     }

  953.     /**
  954.      * Replaces all the occurrences of variables within the given source buffer with their matching values from the
  955.      * resolver. The buffer is updated with the result.
  956.      *
  957.      * @param source the buffer to replace in, updated, null returns zero
  958.      * @return true if altered
  959.      */
  960.     public boolean replaceIn(final StringBuffer source) {
  961.         if (source == null) {
  962.             return false;
  963.         }
  964.         return replaceIn(source, 0, source.length());
  965.     }

  966.     /**
  967.      * Replaces all the occurrences of variables within the given source buffer with their matching values from the
  968.      * resolver. The buffer is updated with the result.
  969.      * <p>
  970.      * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
  971.      * not deleted.
  972.      * </p>
  973.      *
  974.      * @param source the buffer to replace in, updated, null returns zero
  975.      * @param offset the start offset within the source, must be valid
  976.      * @param length the length within the source to be processed, must be valid
  977.      * @return true if altered
  978.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  979.      */
  980.     public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
  981.         if (source == null) {
  982.             return false;
  983.         }
  984.         final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
  985.         if (!substitute(buf, 0, length)) {
  986.             return false;
  987.         }
  988.         source.replace(offset, offset + length, buf.toString());
  989.         return true;
  990.     }

  991.     /**
  992.      * Replaces all the occurrences of variables within the given source buffer with their matching values from the
  993.      * resolver. The buffer is updated with the result.
  994.      *
  995.      * @param source the buffer to replace in, updated, null returns zero
  996.      * @return true if altered
  997.      */
  998.     public boolean replaceIn(final StringBuilder source) {
  999.         if (source == null) {
  1000.             return false;
  1001.         }
  1002.         return replaceIn(source, 0, source.length());
  1003.     }

  1004.     /**
  1005.      * Replaces all the occurrences of variables within the given source builder with their matching values from the
  1006.      * resolver. The builder is updated with the result.
  1007.      * <p>
  1008.      * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
  1009.      * not deleted.
  1010.      * </p>
  1011.      *
  1012.      * @param source the buffer to replace in, updated, null returns zero
  1013.      * @param offset the start offset within the source, must be valid
  1014.      * @param length the length within the source to be processed, must be valid
  1015.      * @return true if altered
  1016.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  1017.      */
  1018.     public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
  1019.         if (source == null) {
  1020.             return false;
  1021.         }
  1022.         final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
  1023.         if (!substitute(buf, 0, length)) {
  1024.             return false;
  1025.         }
  1026.         source.replace(offset, offset + length, buf.toString());
  1027.         return true;
  1028.     }

  1029.     /**
  1030.      * Replaces all the occurrences of variables within the given source builder with their matching values from the
  1031.      * resolver.
  1032.      *
  1033.      * @param source the builder to replace in, updated, null returns zero
  1034.      * @return true if altered
  1035.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  1036.      */
  1037.     public boolean replaceIn(final TextStringBuilder source) {
  1038.         if (source == null) {
  1039.             return false;
  1040.         }
  1041.         return substitute(source, 0, source.length());
  1042.     }

  1043.     /**
  1044.      * Replaces all the occurrences of variables within the given source builder with their matching values from the
  1045.      * resolver.
  1046.      * <p>
  1047.      * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is
  1048.      * not deleted.
  1049.      * </p>
  1050.      *
  1051.      * @param source the builder to replace in, null returns zero
  1052.      * @param offset the start offset within the source, must be valid
  1053.      * @param length the length within the source to be processed, must be valid
  1054.      * @return true if altered
  1055.      * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
  1056.      */
  1057.     public boolean replaceIn(final TextStringBuilder source, final int offset, final int length) {
  1058.         if (source == null) {
  1059.             return false;
  1060.         }
  1061.         return substitute(source, offset, length);
  1062.     }

  1063.     /**
  1064.      * Internal method that resolves the value of a variable.
  1065.      * <p>
  1066.      * Most users of this class do not need to call this method. This method is called automatically by the substitution
  1067.      * process.
  1068.      * </p>
  1069.      * <p>
  1070.      * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is
  1071.      * passed the variable's name and must return the corresponding value. This implementation uses the
  1072.      * {@link #getStringLookup()} with the variable's name as the key.
  1073.      * </p>
  1074.      *
  1075.      * @param variableName the name of the variable, not null
  1076.      * @param buf the buffer where the substitution is occurring, not null
  1077.      * @param startPos the start position of the variable including the prefix, valid
  1078.      * @param endPos the end position of the variable including the suffix, valid
  1079.      * @return The variable's value or <strong>null</strong> if the variable is unknown
  1080.      */
  1081.     protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos,
  1082.         final int endPos) {
  1083.         final StringLookup resolver = getStringLookup();
  1084.         if (resolver == null) {
  1085.             return null;
  1086.         }
  1087.         return resolver.lookup(variableName);
  1088.     }

  1089.     /**
  1090.      * Sets a flag whether substitution is done in variable values (recursive).
  1091.      *
  1092.      * @param disableSubstitutionInValues true if substitution in variable value are disabled
  1093.      * @return this, to enable chaining
  1094.      */
  1095.     public StringSubstitutor setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
  1096.         this.disableSubstitutionInValues = disableSubstitutionInValues;
  1097.         return this;
  1098.     }

  1099.     /**
  1100.      * Sets a flag whether substitution is done in variable names. If set to <strong>true</strong>, the names of variables can
  1101.      * contain other variables which are processed first before the original variable is evaluated, e.g.
  1102.      * {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
  1103.      *
  1104.      * @param enableSubstitutionInVariables the new value of the flag
  1105.      * @return this, to enable chaining
  1106.      */
  1107.     public StringSubstitutor setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
  1108.         this.enableSubstitutionInVariables = enableSubstitutionInVariables;
  1109.         return this;
  1110.     }

  1111.     /**
  1112.      * Sets a flag whether exception should be thrown if any variable is undefined.
  1113.      *
  1114.      * @param failOnUndefinedVariable true if exception should be thrown on undefined variable
  1115.      * @return this, to enable chaining
  1116.      */
  1117.     public StringSubstitutor setEnableUndefinedVariableException(final boolean failOnUndefinedVariable) {
  1118.         this.failOnUndefinedVariable = failOnUndefinedVariable;
  1119.         return this;
  1120.     }

  1121.     /**
  1122.      * Sets the escape character. If this character is placed before a variable reference in the source text, this
  1123.      * variable will be ignored.
  1124.      *
  1125.      * @param escapeChar the escape character (0 for disabling escaping)
  1126.      * @return this, to enable chaining
  1127.      */
  1128.     public StringSubstitutor setEscapeChar(final char escapeChar) {
  1129.         this.escapeChar = escapeChar;
  1130.         return this;
  1131.     }

  1132.     /**
  1133.      * Sets a flag controlling whether escapes are preserved during substitution. If set to <strong>true</strong>, the escape
  1134.      * character is retained during substitution (e.g. {@code $${this-is-escaped}} remains {@code $${this-is-escaped}}).
  1135.      * If set to <strong>false</strong>, the escape character is removed during substitution (e.g. {@code $${this-is-escaped}}
  1136.      * becomes {@code ${this-is-escaped}}). The default value is <strong>false</strong>
  1137.      *
  1138.      * @param preserveEscapes true if escapes are to be preserved
  1139.      * @return this, to enable chaining
  1140.      */
  1141.     public StringSubstitutor setPreserveEscapes(final boolean preserveEscapes) {
  1142.         this.preserveEscapes = preserveEscapes;
  1143.         return this;
  1144.     }

  1145.     /**
  1146.      * Sets the variable default value delimiter to use.
  1147.      * <p>
  1148.      * The variable default value delimiter is the character or characters that delimit the variable name and the
  1149.      * variable default value. This method allows a single character variable default value delimiter to be easily set.
  1150.      * </p>
  1151.      *
  1152.      * @param valueDelimiter the variable default value delimiter character to use
  1153.      * @return this, to enable chaining
  1154.      */
  1155.     public StringSubstitutor setValueDelimiter(final char valueDelimiter) {
  1156.         return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.charMatcher(valueDelimiter));
  1157.     }

  1158.     /**
  1159.      * Sets the variable default value delimiter to use.
  1160.      * <p>
  1161.      * The variable default value delimiter is the character or characters that delimit the variable name and the
  1162.      * variable default value. This method allows a string variable default value delimiter to be easily set.
  1163.      * </p>
  1164.      * <p>
  1165.      * If the {@code valueDelimiter} is null or empty string, then the variable default value resolution becomes
  1166.      * disabled.
  1167.      * </p>
  1168.      *
  1169.      * @param valueDelimiter the variable default value delimiter string to use, may be null or empty
  1170.      * @return this, to enable chaining
  1171.      */
  1172.     public StringSubstitutor setValueDelimiter(final String valueDelimiter) {
  1173.         if (valueDelimiter == null || valueDelimiter.isEmpty()) {
  1174.             setValueDelimiterMatcher(null);
  1175.             return this;
  1176.         }
  1177.         return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.stringMatcher(valueDelimiter));
  1178.     }

  1179.     /**
  1180.      * Sets the variable default value delimiter matcher to use.
  1181.      * <p>
  1182.      * The variable default value delimiter is the character or characters that delimit the variable name and the
  1183.      * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
  1184.      * value delimiter matches.
  1185.      * </p>
  1186.      * <p>
  1187.      * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution becomes disabled.
  1188.      * </p>
  1189.      *
  1190.      * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null
  1191.      * @return this, to enable chaining
  1192.      */
  1193.     public StringSubstitutor setValueDelimiterMatcher(final StringMatcher valueDelimiterMatcher) {
  1194.         this.valueDelimiterMatcher = valueDelimiterMatcher;
  1195.         return this;
  1196.     }

  1197.     /**
  1198.      * Sets the variable prefix to use.
  1199.      * <p>
  1200.      * The variable prefix is the character or characters that identify the start of a variable. This method allows a
  1201.      * single character prefix to be easily set.
  1202.      * </p>
  1203.      *
  1204.      * @param prefix the prefix character to use
  1205.      * @return this, to enable chaining
  1206.      */
  1207.     public StringSubstitutor setVariablePrefix(final char prefix) {
  1208.         return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.charMatcher(prefix));
  1209.     }

  1210.     /**
  1211.      * Sets the variable prefix to use.
  1212.      * <p>
  1213.      * The variable prefix is the character or characters that identify the start of a variable. This method allows a
  1214.      * string prefix to be easily set.
  1215.      * </p>
  1216.      *
  1217.      * @param prefix the prefix for variables, not null
  1218.      * @return this, to enable chaining
  1219.      * @throws IllegalArgumentException if the prefix is null
  1220.      */
  1221.     public StringSubstitutor setVariablePrefix(final String prefix) {
  1222.         Validate.isTrue(prefix != null, "Variable prefix must not be null!");
  1223.         return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(prefix));
  1224.     }

  1225.     /**
  1226.      * Sets the variable prefix matcher currently in use.
  1227.      * <p>
  1228.      * The variable prefix is the character or characters that identify the start of a variable. This prefix is
  1229.      * expressed in terms of a matcher allowing advanced prefix matches.
  1230.      * </p>
  1231.      *
  1232.      * @param prefixMatcher the prefix matcher to use, null ignored
  1233.      * @return this, to enable chaining
  1234.      * @throws IllegalArgumentException if the prefix matcher is null
  1235.      */
  1236.     public StringSubstitutor setVariablePrefixMatcher(final StringMatcher prefixMatcher) {
  1237.         Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
  1238.         this.prefixMatcher = prefixMatcher;
  1239.         return this;
  1240.     }

  1241.     /**
  1242.      * Sets the VariableResolver that is used to lookup variables.
  1243.      *
  1244.      * @param variableResolver the VariableResolver
  1245.      * @return this, to enable chaining
  1246.      */
  1247.     public StringSubstitutor setVariableResolver(final StringLookup variableResolver) {
  1248.         this.variableResolver = variableResolver;
  1249.         return this;
  1250.     }

  1251.     /**
  1252.      * Sets the variable suffix to use.
  1253.      * <p>
  1254.      * The variable suffix is the character or characters that identify the end of a variable. This method allows a
  1255.      * single character suffix to be easily set.
  1256.      * </p>
  1257.      *
  1258.      * @param suffix the suffix character to use
  1259.      * @return this, to enable chaining
  1260.      */
  1261.     public StringSubstitutor setVariableSuffix(final char suffix) {
  1262.         return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.charMatcher(suffix));
  1263.     }

  1264.     /**
  1265.      * Sets the variable suffix to use.
  1266.      * <p>
  1267.      * The variable suffix is the character or characters that identify the end of a variable. This method allows a
  1268.      * string suffix to be easily set.
  1269.      * </p>
  1270.      *
  1271.      * @param suffix the suffix for variables, not null
  1272.      * @return this, to enable chaining
  1273.      * @throws IllegalArgumentException if the suffix is null
  1274.      */
  1275.     public StringSubstitutor setVariableSuffix(final String suffix) {
  1276.         Validate.isTrue(suffix != null, "Variable suffix must not be null!");
  1277.         return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(suffix));
  1278.     }

  1279.     /**
  1280.      * Sets the variable suffix matcher currently in use.
  1281.      * <p>
  1282.      * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
  1283.      * in terms of a matcher allowing advanced suffix matches.
  1284.      * </p>
  1285.      *
  1286.      * @param suffixMatcher the suffix matcher to use, null ignored
  1287.      * @return this, to enable chaining
  1288.      * @throws IllegalArgumentException if the suffix matcher is null
  1289.      */
  1290.     public StringSubstitutor setVariableSuffixMatcher(final StringMatcher suffixMatcher) {
  1291.         Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
  1292.         this.suffixMatcher = suffixMatcher;
  1293.         return this;
  1294.     }

  1295.     /**
  1296.      * Internal method that substitutes the variables.
  1297.      * <p>
  1298.      * Most users of this class do not need to call this method. This method will be called automatically by another
  1299.      * (public) method.
  1300.      * </p>
  1301.      * <p>
  1302.      * Writers of subclasses can override this method if they need access to the substitution process at the start or
  1303.      * end.
  1304.      * </p>
  1305.      *
  1306.      * @param builder the string builder to substitute into, not null
  1307.      * @param offset the start offset within the builder, must be valid
  1308.      * @param length the length within the builder to be processed, must be valid
  1309.      * @return true if altered
  1310.      */
  1311.     protected boolean substitute(final TextStringBuilder builder, final int offset, final int length) {
  1312.         return substitute(builder, offset, length, null).altered;
  1313.     }

  1314.     /**
  1315.      * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the
  1316.      * values of all variable references contained in the passed in text.
  1317.      *
  1318.      * @param builder the string builder to substitute into, not null
  1319.      * @param offset the start offset within the builder, must be valid
  1320.      * @param length the length within the builder to be processed, must be valid
  1321.      * @param priorVariables the stack keeping track of the replaced variables, may be null
  1322.      * @return The result.
  1323.      * @throws IllegalArgumentException if variable is not found and <pre>isEnableUndefinedVariableException()==true</pre>
  1324.      * @since 1.9
  1325.      */
  1326.     private Result substitute(final TextStringBuilder builder, final int offset, final int length,
  1327.         List<String> priorVariables) {
  1328.         Objects.requireNonNull(builder, "builder");
  1329.         final StringMatcher prefixMatcher = getVariablePrefixMatcher();
  1330.         final StringMatcher suffixMatcher = getVariableSuffixMatcher();
  1331.         final char escapeCh = getEscapeChar();
  1332.         final StringMatcher valueDelimMatcher = getValueDelimiterMatcher();
  1333.         final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
  1334.         final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
  1335.         final boolean undefinedVariableException = isEnableUndefinedVariableException();
  1336.         final boolean preserveEscapes = isPreserveEscapes();

  1337.         boolean altered = false;
  1338.         int lengthChange = 0;
  1339.         int bufEnd = offset + length;
  1340.         int pos = offset;
  1341.         int escPos = -1;
  1342.         outer: while (pos < bufEnd) {
  1343.             final int startMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
  1344.             if (startMatchLen == 0) {
  1345.                 pos++;
  1346.             } else {
  1347.                 // found variable start marker
  1348.                 if (pos > offset && builder.charAt(pos - 1) == escapeCh) {
  1349.                     // escape detected
  1350.                     if (preserveEscapes) {
  1351.                         // keep escape
  1352.                         pos++;
  1353.                         continue;
  1354.                     }
  1355.                     // mark esc ch for deletion if we find a complete variable
  1356.                     escPos = pos - 1;
  1357.                 }
  1358.                 // find suffix
  1359.                 int startPos = pos;
  1360.                 pos += startMatchLen;
  1361.                 int endMatchLen = 0;
  1362.                 int nestedVarCount = 0;
  1363.                 while (pos < bufEnd) {
  1364.                     if (substitutionInVariablesEnabled && prefixMatcher.isMatch(builder, pos, offset, bufEnd) != 0) {
  1365.                         // found a nested variable start
  1366.                         endMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
  1367.                         nestedVarCount++;
  1368.                         pos += endMatchLen;
  1369.                         continue;
  1370.                     }

  1371.                     endMatchLen = suffixMatcher.isMatch(builder, pos, offset, bufEnd);
  1372.                     if (endMatchLen == 0) {
  1373.                         pos++;
  1374.                     } else {
  1375.                         // found variable end marker
  1376.                         if (nestedVarCount == 0) {
  1377.                             if (escPos >= 0) {
  1378.                                 // delete escape
  1379.                                 builder.deleteCharAt(escPos);
  1380.                                 escPos = -1;
  1381.                                 lengthChange--;
  1382.                                 altered = true;
  1383.                                 bufEnd--;
  1384.                                 pos = startPos + 1;
  1385.                                 startPos--;
  1386.                                 continue outer;
  1387.                             }
  1388.                             // get var name
  1389.                             String varNameExpr = builder.midString(startPos + startMatchLen,
  1390.                                 pos - startPos - startMatchLen);
  1391.                             if (substitutionInVariablesEnabled) {
  1392.                                 final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
  1393.                                 substitute(bufName, 0, bufName.length());
  1394.                                 varNameExpr = bufName.toString();
  1395.                             }
  1396.                             pos += endMatchLen;
  1397.                             final int endPos = pos;

  1398.                             String varName = varNameExpr;
  1399.                             String varDefaultValue = null;

  1400.                             if (valueDelimMatcher != null) {
  1401.                                 final char[] varNameExprChars = varNameExpr.toCharArray();
  1402.                                 int valueDelimiterMatchLen = 0;
  1403.                                 for (int i = 0; i < varNameExprChars.length; i++) {
  1404.                                     // if there's any nested variable when nested variable substitution disabled,
  1405.                                     // then stop resolving name and default value.
  1406.                                     if (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i,
  1407.                                         varNameExprChars.length) != 0) {
  1408.                                         break;
  1409.                                     }
  1410.                                     if (valueDelimMatcher.isMatch(varNameExprChars, i, 0,
  1411.                                         varNameExprChars.length) != 0) {
  1412.                                         valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0,
  1413.                                             varNameExprChars.length);
  1414.                                         varName = varNameExpr.substring(0, i);
  1415.                                         varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
  1416.                                         break;
  1417.                                     }
  1418.                                 }
  1419.                             }

  1420.                             // on the first call initialize priorVariables
  1421.                             if (priorVariables == null) {
  1422.                                 priorVariables = new ArrayList<>();
  1423.                                 priorVariables.add(builder.midString(offset, length));
  1424.                             }

  1425.                             // handle cyclic substitution
  1426.                             checkCyclicSubstitution(varName, priorVariables);
  1427.                             priorVariables.add(varName);

  1428.                             // resolve the variable
  1429.                             String varValue = resolveVariable(varName, builder, startPos, endPos);
  1430.                             if (varValue == null) {
  1431.                                 varValue = varDefaultValue;
  1432.                             }
  1433.                             if (varValue != null) {
  1434.                                 final int varLen = varValue.length();
  1435.                                 builder.replace(startPos, endPos, varValue);
  1436.                                 altered = true;
  1437.                                 int change = 0;
  1438.                                 if (!substitutionInValuesDisabled) { // recursive replace
  1439.                                     change = substitute(builder, startPos, varLen, priorVariables).lengthChange;
  1440.                                 }
  1441.                                 change = change + varLen - (endPos - startPos);
  1442.                                 pos += change;
  1443.                                 bufEnd += change;
  1444.                                 lengthChange += change;
  1445.                             } else if (undefinedVariableException) {
  1446.                                 throw new IllegalArgumentException(
  1447.                                     String.format("Cannot resolve variable '%s' (enableSubstitutionInVariables=%s).",
  1448.                                         varName, substitutionInVariablesEnabled));
  1449.                             }

  1450.                             // remove variable from the cyclic stack
  1451.                             priorVariables.remove(priorVariables.size() - 1);
  1452.                             break;
  1453.                         }
  1454.                         nestedVarCount--;
  1455.                         pos += endMatchLen;
  1456.                     }
  1457.                 }
  1458.             }
  1459.         }
  1460.         return new Result(altered, lengthChange);
  1461.     }

  1462.     /**
  1463.      * Returns a string representation of the object.
  1464.      *
  1465.      * @return a string representation of the object.
  1466.      * @since 1.11.0
  1467.      */
  1468.     @Override
  1469.     public String toString() {
  1470.         // @formatter:off
  1471.         return new StringBuilder()
  1472.             .append("StringSubstitutor [disableSubstitutionInValues=")
  1473.             .append(disableSubstitutionInValues)
  1474.             .append(", enableSubstitutionInVariables=")
  1475.             .append(enableSubstitutionInVariables)
  1476.             .append(", enableUndefinedVariableException=")
  1477.             .append(failOnUndefinedVariable)
  1478.             .append(", escapeChar=")
  1479.             .append(escapeChar)
  1480.             .append(", prefixMatcher=")
  1481.             .append(prefixMatcher)
  1482.             .append(", preserveEscapes=")
  1483.             .append(preserveEscapes)
  1484.             .append(", suffixMatcher=")
  1485.             .append(suffixMatcher)
  1486.             .append(", valueDelimiterMatcher=")
  1487.             .append(valueDelimiterMatcher)
  1488.             .append(", variableResolver=")
  1489.             .append(variableResolver)
  1490.             .append("]")
  1491.             .toString();
  1492.         // @formatter:on
  1493.     }
  1494. }