001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.text;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Map;
022import java.util.Objects;
023import java.util.Properties;
024import java.util.function.Function;
025import java.util.stream.Collectors;
026
027import org.apache.commons.lang3.Validate;
028import org.apache.commons.text.lookup.StringLookup;
029import org.apache.commons.text.lookup.StringLookupFactory;
030import org.apache.commons.text.matcher.StringMatcher;
031import org.apache.commons.text.matcher.StringMatcherFactory;
032
033/**
034 * Substitutes variables within a string by values.
035 * <p>
036 * This class takes a piece of text and substitutes all the variables within it. The default definition of a variable is
037 * {@code ${variableName}}. The prefix and suffix can be changed via constructors and set methods.
038 * </p>
039 * <p>
040 * Variable values are typically resolved from a map, but could also be resolved from system properties, or by supplying
041 * a custom variable resolver.
042 * </p>
043 * <h2>Using System Properties</h2>
044 * <p>
045 * The simplest example is to use this class to replace Java System properties. For example:
046 * </p>
047 *
048 * <pre>
049 * StringSubstitutor
050 *     .replaceSystemProperties("You are running with java.version = ${java.version} and os.name = ${os.name}.");
051 * </pre>
052 *
053 * <h2>Using a Custom Map</h2>
054 * <p>
055 * Typical usage of this class follows the following pattern:
056 * </p>
057 * <ul>
058 * <li>Create and initialize a StringSubstitutor with the map that contains the values for the variables you want to
059 * make available.</li>
060 * <li>Optionally set attributes like variable prefix, variable suffix, default value delimiter, and so on.</li>
061 * <li>Call the {@code replace()} method with in the source text for interpolation.</li>
062 * <li>The returned text contains all variable references (as long as their values are known) as resolved.</li>
063 * </ul>
064 * <p>
065 * For example:
066 * </p>
067 *
068 * <pre>
069 * // Build map
070 * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
071 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
072 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
073 * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
074 *
075 * // Build StringSubstitutor
076 * StringSubstitutor sub = new StringSubstitutor(valuesMap);
077 *
078 * // Replace
079 * String resolvedString = sub.replace(templateString);
080 * </pre>
081 *
082 * <p>
083 * yielding:
084 * </p>
085 *
086 * <pre>
087 * "The quick brown fox jumped over the lazy dog."
088 * </pre>
089 *
090 * <h2>Providing Default Values</h2>
091 * <p>
092 * You can set a default value for unresolved variables. The default value for a variable can be appended to the
093 * variable name after the variable default value delimiter. The default value of the variable default value delimiter
094 * is ":-", as in bash and other *nix shells.
095 * </p>
096 * <p>
097 * You can set the variable value delimiter with {@link #setValueDelimiterMatcher(StringMatcher)},
098 * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
099 * </p>
100 * <p>
101 * For example:
102 * </p>
103 *
104 * <pre>
105 * // Build map
106 * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
107 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
108 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
109 * String templateString = &quot;The ${animal} jumped over the ${target} ${undefined.number:-1234567890} times.&quot;;
110 *
111 * // Build StringSubstitutor
112 * StringSubstitutor sub = new StringSubstitutor(valuesMap);
113 *
114 * // Replace
115 * String resolvedString = sub.replace(templateString);
116 * </pre>
117 *
118 * <p>
119 * yielding:
120 * </p>
121 *
122 * <pre>
123 * "The quick brown fox jumped over the lazy dog 1234567890 times."
124 * </pre>
125 *
126 * <p>
127 * {@code StringSubstitutor} supports throwing exceptions for unresolved variables, you enable this by setting calling
128 * {@link #setEnableUndefinedVariableException(boolean)} with {@code true}.
129 * </p>
130 *
131 * <h2>Reusing Instances</h2>
132 * <p>
133 * Static shortcut methods cover the most common use cases. If multiple replace operations are to be performed, creating
134 * and reusing an instance of this class will be more efficient.
135 * </p>
136 *
137 * <h2>Using Interpolation</h2>
138 * <p>
139 * The default interpolator lets you use string lookups like:
140 * </p>
141 *
142 * <pre>
143 * final StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
144 * final String text = interpolator.replace(
145 *       "Base64 Decoder:        ${base64Decoder:SGVsbG9Xb3JsZCE=}\n"
146 *     + "Base64 Encoder:        ${base64Encoder:HelloWorld!}\n"
147 *     + "Java Constant:         ${const:java.awt.event.KeyEvent.VK_ESCAPE}\n"
148 *     + "Date:                  ${date:yyyy-MM-dd}\n"
149 *     + "Environment Variable:  ${env:USERNAME}\n"
150 *     + "File Content:          ${file:UTF-8:src/test/resources/document.properties}\n"
151 *     + "Java:                  ${java:version}\n"
152 *     + "Localhost:             ${localhost:canonical-name}\n"
153 *     + "Properties File:       ${properties:src/test/resources/document.properties::mykey}\n"
154 *     + "Resource Bundle:       ${resourceBundle:org.apache.commons.text.example.testResourceBundleLookup:mykey}\n"
155 *     + "System Property:       ${sys:user.dir}\n"
156 *     + "URL Decoder:           ${urlDecoder:Hello%20World%21}\n"
157 *     + "URL Encoder:           ${urlEncoder:Hello World!}\n"
158 *     + "XML XPath:             ${xml:src/test/resources/document.xml:/root/path/to/node}\n");
159 * </pre>
160 * <p>
161 * For documentation and a full list of available lookups, see {@link StringLookupFactory}.
162 * </p>
163 * <p><strong>NOTE:</strong> The list of lookups available by default in {@link #createInterpolator()} changed
164 * in version {@code 1.10.0}. See the {@link StringLookupFactory} documentation for details and an explanation
165 * on how to reproduce the previous functionality.
166 * </p>
167 *
168 * <h2>Using Recursive Variable Replacement</h2>
169 * <p>
170 * Variable replacement can work recursively by calling {@link #setEnableSubstitutionInVariables(boolean)} with
171 * {@code true}. If a variable value contains a variable then that variable will also be replaced. Cyclic replacements
172 * are detected and will throw an exception.
173 * </p>
174 * <p>
175 * You can get the replace result to contain a variable prefix. For example:
176 * </p>
177 *
178 * <pre>
179 * "The variable ${${name}} must be used."
180 * </pre>
181 *
182 * <p>
183 * If the value of the "name" variable is "x", then only the variable "name" is replaced resulting in:
184 * </p>
185 *
186 * <pre>
187 * "The variable ${x} must be used."
188 * </pre>
189 *
190 * <p>
191 * To achieve this effect there are two possibilities: Either set a different prefix and suffix for variables which do
192 * not conflict with the result text you want to produce. The other possibility is to use the escape character, by
193 * default '$'. If this character is placed before a variable reference, this reference is ignored and won't be
194 * replaced. For example:
195 * </p>
196 *
197 * <pre>
198 * "The variable $${${name}} must be used."
199 * </pre>
200 * <p>
201 * In some complex scenarios you might even want to perform substitution in the names of variables, for instance
202 * </p>
203 *
204 * <pre>
205 * ${jre-${java.specification.version}}
206 * </pre>
207 *
208 * <p>
209 * {@code StringSubstitutor} supports this recursive substitution in variable names, but it has to be enabled explicitly
210 * by calling {@link #setEnableSubstitutionInVariables(boolean)} with {@code true}.
211 * </p>
212 *
213 * <h2>Thread Safety</h2>
214 * <p>
215 * This class is <b>not</b> thread safe.
216 * </p>
217 *
218 * @since 1.3
219 */
220public class StringSubstitutor {
221
222    /**
223     * The low-level result of a substitution.
224     *
225     * @since 1.9
226     */
227    private static final class Result {
228
229        /** Whether the buffer is altered. */
230        public final boolean altered;
231
232        /** The length of change. */
233        public final int lengthChange;
234
235        private Result(final boolean altered, final int lengthChange) {
236            this.altered = altered;
237            this.lengthChange = lengthChange;
238        }
239
240        @Override
241        public String toString() {
242            return "Result [altered=" + altered + ", lengthChange=" + lengthChange + "]";
243        }
244    }
245
246    /**
247     * Constant for the default escape character.
248     */
249    public static final char DEFAULT_ESCAPE = '$';
250
251    /**
252     * The default variable default separator.
253     *
254     * @since 1.5.
255     */
256    public static final String DEFAULT_VAR_DEFAULT = ":-";
257
258    /**
259     * The default variable end separator.
260     *
261     * @since 1.5.
262     */
263    public static final String DEFAULT_VAR_END = "}";
264
265    /**
266     * The default variable start separator.
267     *
268     * @since 1.5.
269     */
270    public static final String DEFAULT_VAR_START = "${";
271
272    /**
273     * Constant for the default variable prefix.
274     */
275    public static final StringMatcher DEFAULT_PREFIX = StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_START);
276
277    /**
278     * Constant for the default variable suffix.
279     */
280    public static final StringMatcher DEFAULT_SUFFIX = StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_END);
281
282    /**
283     * Constant for the default value delimiter of a variable.
284     */
285    public static final StringMatcher DEFAULT_VALUE_DELIMITER = StringMatcherFactory.INSTANCE
286        .stringMatcher(DEFAULT_VAR_DEFAULT);
287
288    /**
289     * Creates a new instance using the interpolator string lookup
290     * {@link StringLookupFactory#interpolatorStringLookup()}.
291     * <p>
292     * This StringSubstitutor lets you perform substitutions like:
293     * </p>
294     *
295     * <pre>
296     * StringSubstitutor.createInterpolator().replace(
297     *   "OS name: ${sys:os.name}, user: ${env:USER}");
298     * </pre>
299     *
300     * <p>The table below lists the lookups available by default in the returned instance. These
301     * may be modified through the use of the
302     * {@value org.apache.commons.text.lookup.StringLookupFactory#DEFAULT_STRING_LOOKUPS_PROPERTY}
303     * system property, as described in the {@link StringLookupFactory} documentation.</p>
304     *
305     * <p><strong>NOTE:</strong> The list of lookups available by default changed in version {@code 1.10.0}.
306     * Configuration via system property (as mentioned above) may be necessary to reproduce previous functionality.
307     * </p>
308     *
309     * <table>
310     * <caption>Default Lookups</caption>
311     * <tr>
312     * <th>Key</th>
313     * <th>Lookup</th>
314     * </tr>
315     * <tr>
316     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_BASE64_DECODER}</td>
317     * <td>{@link StringLookupFactory#base64DecoderStringLookup()}</td>
318     * </tr>
319     * <tr>
320     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_BASE64_ENCODER}</td>
321     * <td>{@link StringLookupFactory#base64EncoderStringLookup()}</td>
322     * </tr>
323     * <tr>
324     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_CONST}</td>
325     * <td>{@link StringLookupFactory#constantStringLookup()}</td>
326     * </tr>
327     * <tr>
328     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_DATE}</td>
329     * <td>{@link StringLookupFactory#dateStringLookup()}</td>
330     * </tr>
331     * <tr>
332     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_ENV}</td>
333     * <td>{@link StringLookupFactory#environmentVariableStringLookup()}</td>
334     * </tr>
335     * <tr>
336     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_FILE}</td>
337     * <td>{@link StringLookupFactory#fileStringLookup()}</td>
338     * </tr>
339     * <tr>
340     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_JAVA}</td>
341     * <td>{@link StringLookupFactory#javaPlatformStringLookup()}</td>
342     * </tr>
343     * <tr>
344     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_LOCALHOST}</td>
345     * <td>{@link StringLookupFactory#localHostStringLookup()}</td>
346     * </tr>
347     * <tr>
348     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_PROPERTIES}</td>
349     * <td>{@link StringLookupFactory#propertiesStringLookup()}</td>
350     * </tr>
351     * <tr>
352     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_RESOURCE_BUNDLE}</td>
353     * <td>{@link StringLookupFactory#resourceBundleStringLookup()}</td>
354     * </tr>
355     * <tr>
356     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_SYS}</td>
357     * <td>{@link StringLookupFactory#systemPropertyStringLookup()}</td>
358     * </tr>
359     * <tr>
360     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_URL_DECODER}</td>
361     * <td>{@link StringLookupFactory#urlDecoderStringLookup()}</td>
362     * </tr>
363     * <tr>
364     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_URL_ENCODER}</td>
365     * <td>{@link StringLookupFactory#urlEncoderStringLookup()}</td>
366     * </tr>
367     * <tr>
368     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML}</td>
369     * <td>{@link StringLookupFactory#xmlStringLookup()}</td>
370     * </tr>
371     * <tr>
372     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML_DECODER}</td>
373     * <td>{@link StringLookupFactory#xmlDecoderStringLookup()}</td>
374     * </tr>
375     * <tr>
376     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML_ENCODER}</td>
377     * <td>{@link StringLookupFactory#xmlEncoderStringLookup()}</td>
378     * </tr>
379     * </table>
380     *
381     * @return a new instance using the interpolator string lookup.
382     * @see StringLookupFactory#interpolatorStringLookup()
383     * @since 1.8
384     */
385    public static StringSubstitutor createInterpolator() {
386        return new StringSubstitutor(StringLookupFactory.INSTANCE.interpolatorStringLookup());
387    }
388
389    /**
390     * Replaces all the occurrences of variables in the given source object with their matching values from the map.
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     * @return The result of the replace operation
396     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
397     */
398    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
399        return new StringSubstitutor(valueMap).replace(source);
400    }
401
402    /**
403     * Replaces all the occurrences of variables in the given source object with their matching values from the map.
404     * This method allows to specify a custom variable prefix and suffix
405     *
406     * @param <V> the type of the values in the map
407     * @param source the source text containing the variables to substitute, null returns null
408     * @param valueMap the map with the values, may be null
409     * @param prefix the prefix of variables, not null
410     * @param suffix the suffix of variables, not null
411     * @return The result of the replace operation
412     * @throws IllegalArgumentException if the prefix or suffix is null
413     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
414     */
415    public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix,
416        final String suffix) {
417        return new StringSubstitutor(valueMap, prefix, suffix).replace(source);
418    }
419
420    /**
421     * Replaces all the occurrences of variables in the given source object with their matching values from the
422     * properties.
423     *
424     * @param source the source text containing the variables to substitute, null returns null
425     * @param valueProperties the properties with values, may be 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 replace(final Object source, final Properties valueProperties) {
430        if (valueProperties == null) {
431            return source.toString();
432        }
433        return StringSubstitutor.replace(source,
434                valueProperties.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), valueProperties::getProperty)));
435    }
436
437    /**
438     * Replaces all the occurrences of variables in the given source object with their matching values from the system
439     * properties.
440     *
441     * @param source the source text containing the variables to substitute, null returns null
442     * @return The result of the replace operation
443     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
444     */
445    public static String replaceSystemProperties(final Object source) {
446        return new StringSubstitutor(StringLookupFactory.INSTANCE.systemPropertyStringLookup()).replace(source);
447    }
448
449    /**
450     * The flag whether substitution in variable values is disabled.
451     */
452    private boolean disableSubstitutionInValues;
453
454    /**
455     * The flag whether substitution in variable names is enabled.
456     */
457    private boolean enableSubstitutionInVariables;
458
459    /**
460     * The flag whether exception should be thrown on undefined variable.
461     */
462    private boolean enableUndefinedVariableException;
463
464    /**
465     * Stores the escape character.
466     */
467    private char escapeChar;
468
469    /**
470     * Stores the variable prefix.
471     */
472    private StringMatcher prefixMatcher;
473
474    /**
475     * Whether escapes should be preserved. Default is false;
476     */
477    private boolean preserveEscapes;
478
479    /**
480     * Stores the variable suffix.
481     */
482    private StringMatcher suffixMatcher;
483
484    /**
485     * Stores the default variable value delimiter.
486     */
487    private StringMatcher valueDelimiterMatcher;
488
489    /**
490     * Variable resolution is delegated to an implementor of {@link StringLookup}.
491     */
492    private StringLookup variableResolver;
493
494    /**
495     * Constructs a new instance with defaults for variable prefix and suffix and the escaping character.
496     */
497    public StringSubstitutor() {
498        this((StringLookup) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
499    }
500
501    /**
502     * Constructs a new initialized instance. Uses defaults for variable prefix and suffix and the escaping
503     * character.
504     *
505     * @param <V> the type of the values in the map
506     * @param valueMap the map with the variables' values, may be null
507     */
508    public <V> StringSubstitutor(final Map<String, V> valueMap) {
509        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
510    }
511
512    /**
513     * Constructs a new initialized instance. Uses a default escaping character.
514     *
515     * @param <V> the type of the values in the map
516     * @param valueMap the map with the variables' values, may be null
517     * @param prefix the prefix for variables, not null
518     * @param suffix the suffix for variables, not 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        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
523    }
524
525    /**
526     * Constructs a new initialized instance.
527     *
528     * @param <V> the type of the values in the map
529     * @param valueMap the map with the variables' values, may be null
530     * @param prefix the prefix for variables, not null
531     * @param suffix the suffix for variables, not null
532     * @param escape the escape character
533     * @throws IllegalArgumentException if the prefix or suffix is null
534     */
535    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
536        final char escape) {
537        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape);
538    }
539
540    /**
541     * Constructs a new initialized instance.
542     *
543     * @param <V> the type of the values in the map
544     * @param valueMap the map with the variables' values, may be null
545     * @param prefix the prefix for variables, not null
546     * @param suffix the suffix for variables, not null
547     * @param escape the escape character
548     * @param valueDelimiter the variable default value delimiter, may be null
549     * @throws IllegalArgumentException if the prefix or suffix is null
550     */
551    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
552        final char escape, final String valueDelimiter) {
553        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape, valueDelimiter);
554    }
555
556    /**
557     * Constructs a new initialized instance.
558     *
559     * @param variableResolver the variable resolver, may be null
560     */
561    public StringSubstitutor(final StringLookup variableResolver) {
562        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
563    }
564
565    /**
566     * Constructs a new initialized instance.
567     *
568     * @param variableResolver the variable resolver, may be null
569     * @param prefix the prefix for variables, not null
570     * @param suffix the suffix for variables, not null
571     * @param escape the escape character
572     * @throws IllegalArgumentException if the prefix or suffix is null
573     */
574    public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
575        final char escape) {
576        this.setVariableResolver(variableResolver);
577        this.setVariablePrefix(prefix);
578        this.setVariableSuffix(suffix);
579        this.setEscapeChar(escape);
580        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
581    }
582
583    /**
584     * Constructs a new initialized instance.
585     *
586     * @param variableResolver the variable resolver, may be null
587     * @param prefix the prefix for variables, not null
588     * @param suffix the suffix for variables, not null
589     * @param escape the escape character
590     * @param valueDelimiter the variable default value delimiter string, may be null
591     * @throws IllegalArgumentException if the prefix or suffix is null
592     */
593    public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
594        final char escape, final String valueDelimiter) {
595        this.setVariableResolver(variableResolver);
596        this.setVariablePrefix(prefix);
597        this.setVariableSuffix(suffix);
598        this.setEscapeChar(escape);
599        this.setValueDelimiter(valueDelimiter);
600    }
601
602    /**
603     * Constructs a new initialized instance.
604     *
605     * @param variableResolver the variable resolver, may be null
606     * @param prefixMatcher the prefix for variables, not null
607     * @param suffixMatcher the suffix for variables, not null
608     * @param escape the escape character
609     * @throws IllegalArgumentException if the prefix or suffix is null
610     */
611    public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
612        final StringMatcher suffixMatcher, final char escape) {
613        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
614    }
615
616    /**
617     * Constructs a new initialized instance.
618     *
619     * @param variableResolver the variable resolver, may be null
620     * @param prefixMatcher the prefix for variables, not null
621     * @param suffixMatcher the suffix for variables, not null
622     * @param escape the escape character
623     * @param valueDelimiterMatcher the variable default value delimiter matcher, may be null
624     * @throws IllegalArgumentException if the prefix or suffix is null
625     */
626    public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
627        final StringMatcher suffixMatcher, final char escape, final StringMatcher valueDelimiterMatcher) {
628        this.setVariableResolver(variableResolver);
629        this.setVariablePrefixMatcher(prefixMatcher);
630        this.setVariableSuffixMatcher(suffixMatcher);
631        this.setEscapeChar(escape);
632        this.setValueDelimiterMatcher(valueDelimiterMatcher);
633    }
634
635    /**
636     * Creates a new instance based on the given StringSubstitutor.
637     *
638     * @param other The StringSubstitutor used as the source.
639     * @since 1.9
640     */
641    public StringSubstitutor(final StringSubstitutor other) {
642        disableSubstitutionInValues = other.isDisableSubstitutionInValues();
643        enableSubstitutionInVariables = other.isEnableSubstitutionInVariables();
644        enableUndefinedVariableException = other.isEnableUndefinedVariableException();
645        escapeChar = other.getEscapeChar();
646        prefixMatcher = other.getVariablePrefixMatcher();
647        preserveEscapes = other.isPreserveEscapes();
648        suffixMatcher = other.getVariableSuffixMatcher();
649        valueDelimiterMatcher = other.getValueDelimiterMatcher();
650        variableResolver = other.getStringLookup();
651    }
652
653    /**
654     * Checks if the specified variable is already in the stack (list) of variables.
655     *
656     * @param varName the variable name to check
657     * @param priorVariables the list of prior variables
658     */
659    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
660        if (!priorVariables.contains(varName)) {
661            return;
662        }
663        final TextStringBuilder buf = new TextStringBuilder(256);
664        buf.append("Infinite loop in property interpolation of ");
665        buf.append(priorVariables.remove(0));
666        buf.append(": ");
667        buf.appendWithSeparators(priorVariables, "->");
668        throw new IllegalStateException(buf.toString());
669    }
670
671    // Escape
672    /**
673     * Returns the escape character.
674     *
675     * @return The character used for escaping variable references
676     */
677    public char getEscapeChar() {
678        return this.escapeChar;
679    }
680
681    /**
682     * Gets the StringLookup that is used to lookup variables.
683     *
684     * @return The StringLookup
685     */
686    public StringLookup getStringLookup() {
687        return this.variableResolver;
688    }
689
690    /**
691     * Gets the variable default value delimiter matcher currently in use.
692     * <p>
693     * The variable default value delimiter is the character or characters that delimit the variable name and the
694     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
695     * value delimiter matches.
696     * </p>
697     * <p>
698     * If it returns null, then the variable default value resolution is disabled.
699     *
700     * @return The variable default value delimiter matcher in use, may be null
701     */
702    public StringMatcher getValueDelimiterMatcher() {
703        return valueDelimiterMatcher;
704    }
705
706    /**
707     * Gets the variable prefix matcher currently in use.
708     * <p>
709     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
710     * expressed in terms of a matcher allowing advanced prefix matches.
711     * </p>
712     *
713     * @return The prefix matcher in use
714     */
715    public StringMatcher getVariablePrefixMatcher() {
716        return prefixMatcher;
717    }
718
719    /**
720     * Gets the variable suffix matcher currently in use.
721     * <p>
722     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
723     * in terms of a matcher allowing advanced suffix matches.
724     * </p>
725     *
726     * @return The suffix matcher in use
727     */
728    public StringMatcher getVariableSuffixMatcher() {
729        return suffixMatcher;
730    }
731
732    /**
733     * Returns a flag whether substitution is disabled in variable values.If set to <b>true</b>, the values of variables
734     * can contain other variables will not be processed and substituted original variable is evaluated, e.g.
735     *
736     * <pre>
737     * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
738     * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
739     * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
740     * String templateString = &quot;Hi ${name}&quot;;
741     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
742     * String resolvedString = sub.replace(templateString);
743     * </pre>
744     *
745     * yielding:
746     *
747     * <pre>
748     *      Hi Douglas ${surname}
749     * </pre>
750     *
751     * @return The substitution in variable values flag
752     */
753    public boolean isDisableSubstitutionInValues() {
754        return disableSubstitutionInValues;
755    }
756
757    /**
758     * Returns a flag whether substitution is done in variable names.
759     *
760     * @return The substitution in variable names flag
761     */
762    public boolean isEnableSubstitutionInVariables() {
763        return enableSubstitutionInVariables;
764    }
765
766    /**
767     * Returns a flag whether exception can be thrown upon undefined variable.
768     *
769     * @return The fail on undefined variable flag
770     */
771    public boolean isEnableUndefinedVariableException() {
772        return enableUndefinedVariableException;
773    }
774
775    /**
776     * Returns the flag controlling whether escapes are preserved during substitution.
777     *
778     * @return The preserve escape flag
779     */
780    public boolean isPreserveEscapes() {
781        return preserveEscapes;
782    }
783
784    /**
785     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
786     * array as a template. The array is not altered by this method.
787     *
788     * @param source the character array to replace in, not altered, null returns null
789     * @return The result of the replace operation
790     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
791     */
792    public String replace(final char[] source) {
793        if (source == null) {
794            return null;
795        }
796        final TextStringBuilder buf = new TextStringBuilder(source.length).append(source);
797        substitute(buf, 0, source.length);
798        return buf.toString();
799    }
800
801    /**
802     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
803     * array as a template. The array is not altered by this method.
804     * <p>
805     * Only the specified portion of the array will be processed. The rest of the array is not processed, and is not
806     * returned.
807     * </p>
808     *
809     * @param source the character array to replace in, not altered, null returns null
810     * @param offset the start offset within the array, must be valid
811     * @param length the length within the array to be processed, must be valid
812     * @return The result of the replace operation
813     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
814     * @throws StringIndexOutOfBoundsException if {@code offset} is not in the
815     *  range {@code 0 <= offset <= chars.length}
816     * @throws StringIndexOutOfBoundsException if {@code length < 0}
817     * @throws StringIndexOutOfBoundsException if {@code offset + length > chars.length}
818     */
819    public String replace(final char[] source, final int offset, final int length) {
820        if (source == null) {
821            return null;
822        }
823        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
824        substitute(buf, 0, length);
825        return buf.toString();
826    }
827
828    /**
829     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
830     * a template. The source is not altered by this method.
831     *
832     * @param source the buffer to use as a template, not changed, null returns null
833     * @return The result of the replace operation
834     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
835     */
836    public String replace(final CharSequence source) {
837        if (source == null) {
838            return null;
839        }
840        return replace(source, 0, source.length());
841    }
842
843    /**
844     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
845     * a template. The source is not altered by this method.
846     * <p>
847     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
848     * returned.
849     * </p>
850     *
851     * @param source the buffer to use as a template, not changed, null returns null
852     * @param offset the start offset within the array, must be valid
853     * @param length the length within the array to be processed, must be valid
854     * @return The result of the replace operation
855     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
856     */
857    public String replace(final CharSequence source, final int offset, final int length) {
858        if (source == null) {
859            return null;
860        }
861        final TextStringBuilder buf = new TextStringBuilder(length).append(source.toString(), offset, length);
862        substitute(buf, 0, length);
863        return buf.toString();
864    }
865
866    /**
867     * Replaces all the occurrences of variables in the given source object with their matching values from the
868     * resolver. The input source object is converted to a string using {@code toString} and is not altered.
869     *
870     * @param source the source to replace in, null returns null
871     * @return The result of the replace operation
872     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
873     */
874    public String replace(final Object source) {
875        if (source == null) {
876            return null;
877        }
878        final TextStringBuilder buf = new TextStringBuilder().append(source);
879        substitute(buf, 0, buf.length());
880        return buf.toString();
881    }
882
883    /**
884     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
885     * string as a template.
886     *
887     * @param source the string to replace in, null returns null
888     * @return The result of the replace operation
889     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
890     */
891    public String replace(final String source) {
892        if (source == null) {
893            return null;
894        }
895        final TextStringBuilder buf = new TextStringBuilder(source);
896        if (!substitute(buf, 0, source.length())) {
897            return source;
898        }
899        return buf.toString();
900    }
901
902    /**
903     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
904     * string as a template.
905     * <p>
906     * Only the specified portion of the string will be processed. The rest of the string is not processed, and is not
907     * returned.
908     * </p>
909     *
910     * @param source the string to replace in, null returns null
911     * @param offset the start offset within the source, must be valid
912     * @param length the length within the source to be processed, must be valid
913     * @return The result of the replace operation
914     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
915     * @throws StringIndexOutOfBoundsException if {@code offset} is not in the
916     *  range {@code 0 <= offset <= source.length()}
917     * @throws StringIndexOutOfBoundsException if {@code length < 0}
918     * @throws StringIndexOutOfBoundsException if {@code offset + length > source.length()}
919     */
920    public String replace(final String source, final int offset, final int length) {
921        if (source == null) {
922            return null;
923        }
924        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
925        if (!substitute(buf, 0, length)) {
926            return source.substring(offset, offset + length);
927        }
928        return buf.toString();
929    }
930
931    /**
932     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
933     * buffer as a template. The buffer is not altered by this method.
934     *
935     * @param source the buffer to use as a template, not changed, null returns null
936     * @return The result of the replace operation
937     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
938     */
939    public String replace(final StringBuffer source) {
940        if (source == null) {
941            return null;
942        }
943        final TextStringBuilder buf = new TextStringBuilder(source.length()).append(source);
944        substitute(buf, 0, buf.length());
945        return buf.toString();
946    }
947
948    /**
949     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
950     * buffer as a template. The buffer is not altered by this method.
951     * <p>
952     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
953     * returned.
954     * </p>
955     *
956     * @param source the buffer to use as a template, not changed, null returns null
957     * @param offset the start offset within the source, must be valid
958     * @param length the length within the source to be processed, must be valid
959     * @return The result of the replace operation
960     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
961     */
962    public String replace(final StringBuffer source, final int offset, final int length) {
963        if (source == null) {
964            return null;
965        }
966        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
967        substitute(buf, 0, length);
968        return buf.toString();
969    }
970
971    /**
972     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
973     * builder as a template. The builder is not altered by this method.
974     *
975     * @param source the builder to use as a template, not changed, null returns null
976     * @return The result of the replace operation
977     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
978     */
979    public String replace(final TextStringBuilder source) {
980        if (source == null) {
981            return null;
982        }
983        final TextStringBuilder builder = new TextStringBuilder(source.length()).append(source);
984        substitute(builder, 0, builder.length());
985        return builder.toString();
986    }
987
988    /**
989     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
990     * builder as a template. The builder is not altered by this method.
991     * <p>
992     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not
993     * returned.
994     * </p>
995     *
996     * @param source the builder to use as a template, not changed, null returns null
997     * @param offset the start offset within the source, must be valid
998     * @param length the length within the source to be processed, must be valid
999     * @return The result of the replace operation
1000     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
1001     */
1002    public String replace(final TextStringBuilder source, final int offset, final int length) {
1003        if (source == null) {
1004            return null;
1005        }
1006        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
1007        substitute(buf, 0, length);
1008        return buf.toString();
1009    }
1010
1011    /**
1012     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
1013     * resolver. The buffer is updated with the result.
1014     *
1015     * @param source the buffer to replace in, updated, null returns zero
1016     * @return true if altered
1017     */
1018    public boolean replaceIn(final StringBuffer source) {
1019        if (source == null) {
1020            return false;
1021        }
1022        return replaceIn(source, 0, source.length());
1023    }
1024
1025    /**
1026     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
1027     * resolver. The buffer is updated with the result.
1028     * <p>
1029     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
1030     * not deleted.
1031     * </p>
1032     *
1033     * @param source the buffer to replace in, updated, null returns zero
1034     * @param offset the start offset within the source, must be valid
1035     * @param length the length within the source to be processed, must be valid
1036     * @return true if altered
1037     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
1038     */
1039    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
1040        if (source == null) {
1041            return false;
1042        }
1043        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
1044        if (!substitute(buf, 0, length)) {
1045            return false;
1046        }
1047        source.replace(offset, offset + length, buf.toString());
1048        return true;
1049    }
1050
1051    /**
1052     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
1053     * resolver. The buffer is updated with the result.
1054     *
1055     * @param source the buffer to replace in, updated, null returns zero
1056     * @return true if altered
1057     */
1058    public boolean replaceIn(final StringBuilder source) {
1059        if (source == null) {
1060            return false;
1061        }
1062        return replaceIn(source, 0, source.length());
1063    }
1064
1065    /**
1066     * Replaces all the occurrences of variables within the given source builder with their matching values from the
1067     * resolver. The builder is updated with the result.
1068     * <p>
1069     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
1070     * not deleted.
1071     * </p>
1072     *
1073     * @param source the buffer to replace in, updated, null returns zero
1074     * @param offset the start offset within the source, must be valid
1075     * @param length the length within the source to be processed, must be valid
1076     * @return true if altered
1077     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
1078     */
1079    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
1080        if (source == null) {
1081            return false;
1082        }
1083        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
1084        if (!substitute(buf, 0, length)) {
1085            return false;
1086        }
1087        source.replace(offset, offset + length, buf.toString());
1088        return true;
1089    }
1090
1091    /**
1092     * Replaces all the occurrences of variables within the given source builder with their matching values from the
1093     * resolver.
1094     *
1095     * @param source the builder to replace in, updated, null returns zero
1096     * @return true if altered
1097     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
1098     */
1099    public boolean replaceIn(final TextStringBuilder source) {
1100        if (source == null) {
1101            return false;
1102        }
1103        return substitute(source, 0, source.length());
1104    }
1105
1106    /**
1107     * Replaces all the occurrences of variables within the given source builder with their matching values from the
1108     * resolver.
1109     * <p>
1110     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is
1111     * not deleted.
1112     * </p>
1113     *
1114     * @param source the builder to replace in, null returns zero
1115     * @param offset the start offset within the source, must be valid
1116     * @param length the length within the source to be processed, must be valid
1117     * @return true if altered
1118     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
1119     */
1120    public boolean replaceIn(final TextStringBuilder source, final int offset, final int length) {
1121        if (source == null) {
1122            return false;
1123        }
1124        return substitute(source, offset, length);
1125    }
1126
1127    /**
1128     * Internal method that resolves the value of a variable.
1129     * <p>
1130     * Most users of this class do not need to call this method. This method is called automatically by the substitution
1131     * process.
1132     * </p>
1133     * <p>
1134     * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is
1135     * passed the variable's name and must return the corresponding value. This implementation uses the
1136     * {@link #getStringLookup()} with the variable's name as the key.
1137     * </p>
1138     *
1139     * @param variableName the name of the variable, not null
1140     * @param buf the buffer where the substitution is occurring, not null
1141     * @param startPos the start position of the variable including the prefix, valid
1142     * @param endPos the end position of the variable including the suffix, valid
1143     * @return The variable's value or <b>null</b> if the variable is unknown
1144     */
1145    protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos,
1146        final int endPos) {
1147        final StringLookup resolver = getStringLookup();
1148        if (resolver == null) {
1149            return null;
1150        }
1151        return resolver.lookup(variableName);
1152    }
1153
1154    /**
1155     * Sets a flag whether substitution is done in variable values (recursive).
1156     *
1157     * @param disableSubstitutionInValues true if substitution in variable value are disabled
1158     * @return this, to enable chaining
1159     */
1160    public StringSubstitutor setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
1161        this.disableSubstitutionInValues = disableSubstitutionInValues;
1162        return this;
1163    }
1164
1165    /**
1166     * Sets a flag whether substitution is done in variable names. If set to <b>true</b>, the names of variables can
1167     * contain other variables which are processed first before the original variable is evaluated, e.g.
1168     * {@code ${jre-${java.version}}}. The default value is <b>false</b>.
1169     *
1170     * @param enableSubstitutionInVariables the new value of the flag
1171     * @return this, to enable chaining
1172     */
1173    public StringSubstitutor setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
1174        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
1175        return this;
1176    }
1177
1178    /**
1179     * Sets a flag whether exception should be thrown if any variable is undefined.
1180     *
1181     * @param failOnUndefinedVariable true if exception should be thrown on undefined variable
1182     * @return this, to enable chaining
1183     */
1184    public StringSubstitutor setEnableUndefinedVariableException(final boolean failOnUndefinedVariable) {
1185        this.enableUndefinedVariableException = failOnUndefinedVariable;
1186        return this;
1187    }
1188
1189    /**
1190     * Sets the escape character. If this character is placed before a variable reference in the source text, this
1191     * variable will be ignored.
1192     *
1193     * @param escapeCharacter the escape character (0 for disabling escaping)
1194     * @return this, to enable chaining
1195     */
1196    public StringSubstitutor setEscapeChar(final char escapeCharacter) {
1197        this.escapeChar = escapeCharacter;
1198        return this;
1199    }
1200
1201    /**
1202     * Sets a flag controlling whether escapes are preserved during substitution. If set to <b>true</b>, the escape
1203     * character is retained during substitution (e.g. {@code $${this-is-escaped}} remains {@code $${this-is-escaped}}).
1204     * If set to <b>false</b>, the escape character is removed during substitution (e.g. {@code $${this-is-escaped}}
1205     * becomes {@code ${this-is-escaped}}). The default value is <b>false</b>
1206     *
1207     * @param preserveEscapes true if escapes are to be preserved
1208     * @return this, to enable chaining
1209     */
1210    public StringSubstitutor setPreserveEscapes(final boolean preserveEscapes) {
1211        this.preserveEscapes = preserveEscapes;
1212        return this;
1213    }
1214
1215    /**
1216     * Sets the variable default value delimiter to use.
1217     * <p>
1218     * The variable default value delimiter is the character or characters that delimit the variable name and the
1219     * variable default value. This method allows a single character variable default value delimiter to be easily set.
1220     * </p>
1221     *
1222     * @param valueDelimiter the variable default value delimiter character to use
1223     * @return this, to enable chaining
1224     */
1225    public StringSubstitutor setValueDelimiter(final char valueDelimiter) {
1226        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.charMatcher(valueDelimiter));
1227    }
1228
1229    /**
1230     * Sets the variable default value delimiter to use.
1231     * <p>
1232     * The variable default value delimiter is the character or characters that delimit the variable name and the
1233     * variable default value. This method allows a string variable default value delimiter to be easily set.
1234     * </p>
1235     * <p>
1236     * If the {@code valueDelimiter} is null or empty string, then the variable default value resolution becomes
1237     * disabled.
1238     * </p>
1239     *
1240     * @param valueDelimiter the variable default value delimiter string to use, may be null or empty
1241     * @return this, to enable chaining
1242     */
1243    public StringSubstitutor setValueDelimiter(final String valueDelimiter) {
1244        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
1245            setValueDelimiterMatcher(null);
1246            return this;
1247        }
1248        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.stringMatcher(valueDelimiter));
1249    }
1250
1251    /**
1252     * Sets the variable default value delimiter matcher to use.
1253     * <p>
1254     * The variable default value delimiter is the character or characters that delimit the variable name and the
1255     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
1256     * value delimiter matches.
1257     * </p>
1258     * <p>
1259     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution becomes disabled.
1260     * </p>
1261     *
1262     * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null
1263     * @return this, to enable chaining
1264     */
1265    public StringSubstitutor setValueDelimiterMatcher(final StringMatcher valueDelimiterMatcher) {
1266        this.valueDelimiterMatcher = valueDelimiterMatcher;
1267        return this;
1268    }
1269
1270    /**
1271     * Sets the variable prefix to use.
1272     * <p>
1273     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
1274     * single character prefix to be easily set.
1275     * </p>
1276     *
1277     * @param prefix the prefix character to use
1278     * @return this, to enable chaining
1279     */
1280    public StringSubstitutor setVariablePrefix(final char prefix) {
1281        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.charMatcher(prefix));
1282    }
1283
1284    /**
1285     * Sets the variable prefix to use.
1286     * <p>
1287     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
1288     * string prefix to be easily set.
1289     * </p>
1290     *
1291     * @param prefix the prefix for variables, not null
1292     * @return this, to enable chaining
1293     * @throws IllegalArgumentException if the prefix is null
1294     */
1295    public StringSubstitutor setVariablePrefix(final String prefix) {
1296        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
1297        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(prefix));
1298    }
1299
1300    /**
1301     * Sets the variable prefix matcher currently in use.
1302     * <p>
1303     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
1304     * expressed in terms of a matcher allowing advanced prefix matches.
1305     * </p>
1306     *
1307     * @param prefixMatcher the prefix matcher to use, null ignored
1308     * @return this, to enable chaining
1309     * @throws IllegalArgumentException if the prefix matcher is null
1310     */
1311    public StringSubstitutor setVariablePrefixMatcher(final StringMatcher prefixMatcher) {
1312        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
1313        this.prefixMatcher = prefixMatcher;
1314        return this;
1315    }
1316
1317    /**
1318     * Sets the VariableResolver that is used to lookup variables.
1319     *
1320     * @param variableResolver the VariableResolver
1321     * @return this, to enable chaining
1322     */
1323    public StringSubstitutor setVariableResolver(final StringLookup variableResolver) {
1324        this.variableResolver = variableResolver;
1325        return this;
1326    }
1327
1328    /**
1329     * Sets the variable suffix to use.
1330     * <p>
1331     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
1332     * single character suffix to be easily set.
1333     * </p>
1334     *
1335     * @param suffix the suffix character to use
1336     * @return this, to enable chaining
1337     */
1338    public StringSubstitutor setVariableSuffix(final char suffix) {
1339        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.charMatcher(suffix));
1340    }
1341
1342    /**
1343     * Sets the variable suffix to use.
1344     * <p>
1345     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
1346     * string suffix to be easily set.
1347     * </p>
1348     *
1349     * @param suffix the suffix for variables, not null
1350     * @return this, to enable chaining
1351     * @throws IllegalArgumentException if the suffix is null
1352     */
1353    public StringSubstitutor setVariableSuffix(final String suffix) {
1354        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
1355        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(suffix));
1356    }
1357
1358    /**
1359     * Sets the variable suffix matcher currently in use.
1360     * <p>
1361     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
1362     * in terms of a matcher allowing advanced suffix matches.
1363     * </p>
1364     *
1365     * @param suffixMatcher the suffix matcher to use, null ignored
1366     * @return this, to enable chaining
1367     * @throws IllegalArgumentException if the suffix matcher is null
1368     */
1369    public StringSubstitutor setVariableSuffixMatcher(final StringMatcher suffixMatcher) {
1370        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
1371        this.suffixMatcher = suffixMatcher;
1372        return this;
1373    }
1374
1375    /**
1376     * Internal method that substitutes the variables.
1377     * <p>
1378     * Most users of this class do not need to call this method. This method will be called automatically by another
1379     * (public) method.
1380     * </p>
1381     * <p>
1382     * Writers of subclasses can override this method if they need access to the substitution process at the start or
1383     * end.
1384     * </p>
1385     *
1386     * @param builder the string builder to substitute into, not null
1387     * @param offset the start offset within the builder, must be valid
1388     * @param length the length within the builder to be processed, must be valid
1389     * @return true if altered
1390     */
1391    protected boolean substitute(final TextStringBuilder builder, final int offset, final int length) {
1392        return substitute(builder, offset, length, null).altered;
1393    }
1394
1395    /**
1396     * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the
1397     * values of all variable references contained in the passed in text.
1398     *
1399     * @param builder the string builder to substitute into, not null
1400     * @param offset the start offset within the builder, must be valid
1401     * @param length the length within the builder to be processed, must be valid
1402     * @param priorVariables the stack keeping track of the replaced variables, may be null
1403     * @return The result.
1404     * @throws IllegalArgumentException if variable is not found and <pre>isEnableUndefinedVariableException()==true</pre>
1405     * @since 1.9
1406     */
1407    private Result substitute(final TextStringBuilder builder, final int offset, final int length,
1408        List<String> priorVariables) {
1409        Objects.requireNonNull(builder, "builder");
1410        final StringMatcher prefixMatcher = getVariablePrefixMatcher();
1411        final StringMatcher suffixMatcher = getVariableSuffixMatcher();
1412        final char escapeCh = getEscapeChar();
1413        final StringMatcher valueDelimMatcher = getValueDelimiterMatcher();
1414        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
1415        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
1416        final boolean undefinedVariableException = isEnableUndefinedVariableException();
1417        final boolean preserveEscapes = isPreserveEscapes();
1418
1419        boolean altered = false;
1420        int lengthChange = 0;
1421        int bufEnd = offset + length;
1422        int pos = offset;
1423        int escPos = -1;
1424        outer: while (pos < bufEnd) {
1425            final int startMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
1426            if (startMatchLen == 0) {
1427                pos++;
1428            } else {
1429                // found variable start marker
1430                if (pos > offset && builder.charAt(pos - 1) == escapeCh) {
1431                    // escape detected
1432                    if (preserveEscapes) {
1433                        // keep escape
1434                        pos++;
1435                        continue;
1436                    }
1437                    // mark esc ch for deletion if we find a complete variable
1438                    escPos = pos - 1;
1439                }
1440                // find suffix
1441                int startPos = pos;
1442                pos += startMatchLen;
1443                int endMatchLen = 0;
1444                int nestedVarCount = 0;
1445                while (pos < bufEnd) {
1446                    if (substitutionInVariablesEnabled && prefixMatcher.isMatch(builder, pos, offset, bufEnd) != 0) {
1447                        // found a nested variable start
1448                        endMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
1449                        nestedVarCount++;
1450                        pos += endMatchLen;
1451                        continue;
1452                    }
1453
1454                    endMatchLen = suffixMatcher.isMatch(builder, pos, offset, bufEnd);
1455                    if (endMatchLen == 0) {
1456                        pos++;
1457                    } else {
1458                        // found variable end marker
1459                        if (nestedVarCount == 0) {
1460                            if (escPos >= 0) {
1461                                // delete escape
1462                                builder.deleteCharAt(escPos);
1463                                escPos = -1;
1464                                lengthChange--;
1465                                altered = true;
1466                                bufEnd--;
1467                                pos = startPos + 1;
1468                                startPos--;
1469                                continue outer;
1470                            }
1471                            // get var name
1472                            String varNameExpr = builder.midString(startPos + startMatchLen,
1473                                pos - startPos - startMatchLen);
1474                            if (substitutionInVariablesEnabled) {
1475                                final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
1476                                substitute(bufName, 0, bufName.length());
1477                                varNameExpr = bufName.toString();
1478                            }
1479                            pos += endMatchLen;
1480                            final int endPos = pos;
1481
1482                            String varName = varNameExpr;
1483                            String varDefaultValue = null;
1484
1485                            if (valueDelimMatcher != null) {
1486                                final char[] varNameExprChars = varNameExpr.toCharArray();
1487                                int valueDelimiterMatchLen = 0;
1488                                for (int i = 0; i < varNameExprChars.length; i++) {
1489                                    // if there's any nested variable when nested variable substitution disabled,
1490                                    // then stop resolving name and default value.
1491                                    if (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i,
1492                                        varNameExprChars.length) != 0) {
1493                                        break;
1494                                    }
1495                                    if (valueDelimMatcher.isMatch(varNameExprChars, i, 0,
1496                                        varNameExprChars.length) != 0) {
1497                                        valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0,
1498                                            varNameExprChars.length);
1499                                        varName = varNameExpr.substring(0, i);
1500                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
1501                                        break;
1502                                    }
1503                                }
1504                            }
1505
1506                            // on the first call initialize priorVariables
1507                            if (priorVariables == null) {
1508                                priorVariables = new ArrayList<>();
1509                                priorVariables.add(builder.midString(offset, length));
1510                            }
1511
1512                            // handle cyclic substitution
1513                            checkCyclicSubstitution(varName, priorVariables);
1514                            priorVariables.add(varName);
1515
1516                            // resolve the variable
1517                            String varValue = resolveVariable(varName, builder, startPos, endPos);
1518                            if (varValue == null) {
1519                                varValue = varDefaultValue;
1520                            }
1521                            if (varValue != null) {
1522                                final int varLen = varValue.length();
1523                                builder.replace(startPos, endPos, varValue);
1524                                altered = true;
1525                                int change = 0;
1526                                if (!substitutionInValuesDisabled) { // recursive replace
1527                                    change = substitute(builder, startPos, varLen, priorVariables).lengthChange;
1528                                }
1529                                change = change + varLen - (endPos - startPos);
1530                                pos += change;
1531                                bufEnd += change;
1532                                lengthChange += change;
1533                            } else if (undefinedVariableException) {
1534                                throw new IllegalArgumentException(
1535                                    String.format("Cannot resolve variable '%s' (enableSubstitutionInVariables=%s).",
1536                                        varName, substitutionInVariablesEnabled));
1537                            }
1538
1539                            // remove variable from the cyclic stack
1540                            priorVariables.remove(priorVariables.size() - 1);
1541                            break;
1542                        }
1543                        nestedVarCount--;
1544                        pos += endMatchLen;
1545                    }
1546                }
1547            }
1548        }
1549        return new Result(altered, lengthChange);
1550    }
1551
1552    /**
1553     * Returns a string representation of the object.
1554     *
1555     * @return a string representation of the object.
1556     * @since 1.11.0
1557     */
1558    @Override
1559    public String toString() {
1560        final StringBuilder builder = new StringBuilder();
1561        builder.append("StringSubstitutor [disableSubstitutionInValues=").append(disableSubstitutionInValues).append(", enableSubstitutionInVariables=")
1562                .append(enableSubstitutionInVariables).append(", enableUndefinedVariableException=").append(enableUndefinedVariableException)
1563                .append(", escapeChar=").append(escapeChar).append(", prefixMatcher=").append(prefixMatcher).append(", preserveEscapes=")
1564                .append(preserveEscapes).append(", suffixMatcher=").append(suffixMatcher).append(", valueDelimiterMatcher=").append(valueDelimiterMatcher)
1565                .append(", variableResolver=").append(variableResolver).append("]");
1566        return builder.toString();
1567    }
1568}