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.lang3.text;
018
019import java.util.ArrayList;
020import java.util.Enumeration;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025
026import org.apache.commons.lang3.StringUtils;
027
028/**
029 * Substitutes variables within a string by values.
030 * <p>
031 * This class takes a piece of text and substitutes all the variables within it.
032 * The default definition of a variable is {@code ${variableName}}.
033 * The prefix and suffix can be changed via constructors and set methods.
034 * <p>
035 * Variable values are typically resolved from a map, but could also be resolved
036 * from system properties, or by supplying a custom variable resolver.
037 * <p>
038 * The simplest example is to use this class to replace Java System properties. For example:
039 * <pre>
040 * StrSubstitutor.replaceSystemProperties(
041 *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
042 * </pre>
043 * <p>
044 * Typical usage of this class follows the following pattern: First an instance is created
045 * and initialized with the map that contains the values for the available variables.
046 * If a prefix and/or suffix for variables should be used other than the default ones,
047 * the appropriate settings can be performed. After that the {@code replace()}
048 * method can be called passing in the source text for interpolation. In the returned
049 * text all variable references (as long as their values are known) will be resolved.
050 * The following example demonstrates this:
051 * <pre>
052 * Map valuesMap = HashMap();
053 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
054 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
055 * String templateString = &quot;The ${animal} jumps over the ${target}.&quot;;
056 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
057 * String resolvedString = sub.replace(templateString);
058 * </pre>
059 * yielding:
060 * <pre>
061 *      The quick brown fox jumps over the lazy dog.
062 * </pre>
063 * <p>
064 * Also, this class allows to set a default value for unresolved variables.
065 * The default value for a variable can be appended to the variable name after the variable
066 * default value delimiter. The default value of the variable default value delimiter is ':-',
067 * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
068 * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
069 * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
070 * The following shows an example with variable default value settings:
071 * <pre>
072 * Map valuesMap = HashMap();
073 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
074 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
075 * String templateString = &quot;The ${animal} jumps over the ${target}. ${undefined.number:-1234567890}.&quot;;
076 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
077 * String resolvedString = sub.replace(templateString);
078 * </pre>
079 * yielding:
080 * <pre>
081 *      The quick brown fox jumps over the lazy dog. 1234567890.
082 * </pre>
083 * <p>
084 * In addition to this usage pattern there are some static convenience methods that
085 * cover the most common use cases. These methods can be used without the need of
086 * manually creating an instance. However if multiple replace operations are to be
087 * performed, creating and reusing an instance of this class will be more efficient.
088 * <p>
089 * Variable replacement works in a recursive way. Thus, if a variable value contains
090 * a variable then that variable will also be replaced. Cyclic replacements are
091 * detected and will cause an exception to be thrown.
092 * <p>
093 * Sometimes the interpolation's result must contain a variable prefix. As an example
094 * take the following source text:
095 * <pre>
096 *   The variable ${${name}} must be used.
097 * </pre>
098 * Here only the variable's name referred to in the text should be replaced resulting
099 * in the text (assuming that the value of the {@code name} variable is {@code x}):
100 * <pre>
101 *   The variable ${x} must be used.
102 * </pre>
103 * To achieve this effect there are two possibilities: Either set a different prefix
104 * and suffix for variables which do not conflict with the result text you want to
105 * produce. The other possibility is to use the escape character, by default '$'.
106 * If this character is placed before a variable reference, this reference is ignored
107 * and won't be replaced. For example:
108 * <pre>
109 *   The variable $${${name}} must be used.
110 * </pre>
111 * <p>
112 * In some complex scenarios you might even want to perform substitution in the
113 * names of variables, for instance
114 * <pre>
115 * ${jre-${java.specification.version}}
116 * </pre>
117 * {@code StrSubstitutor} supports this recursive substitution in variable
118 * names, but it has to be enabled explicitly by setting the
119 * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
120 * property to <b>true</b>.
121 * <p>This class is <b>not</b> thread safe.</p>
122 *
123 * @since 2.2
124 * @deprecated as of 3.6, use commons-text
125 * <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/StringSubstitutor.html">
126 * StringSubstitutor</a> instead
127 */
128@Deprecated
129public class StrSubstitutor {
130
131    /**
132     * Constant for the default escape character.
133     */
134    public static final char DEFAULT_ESCAPE = '$';
135    /**
136     * Constant for the default variable prefix.
137     */
138    public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
139    /**
140     * Constant for the default variable suffix.
141     */
142    public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
143    /**
144     * Constant for the default value delimiter of a variable.
145     * @since 3.2
146     */
147    public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
148
149    /**
150     * Stores the escape character.
151     */
152    private char escapeChar;
153    /**
154     * Stores the variable prefix.
155     */
156    private StrMatcher prefixMatcher;
157    /**
158     * Stores the variable suffix.
159     */
160    private StrMatcher suffixMatcher;
161    /**
162     * Stores the default variable value delimiter
163     */
164    private StrMatcher valueDelimiterMatcher;
165    /**
166     * Variable resolution is delegated to an implementor of VariableResolver.
167     */
168    private StrLookup<?> variableResolver;
169    /**
170     * The flag whether substitution in variable names is enabled.
171     */
172    private boolean enableSubstitutionInVariables;
173    /**
174     * Whether escapes should be preserved.  Default is false;
175     */
176    private boolean preserveEscapes;
177
178    //-----------------------------------------------------------------------
179    /**
180     * Replaces all the occurrences of variables in the given source object with
181     * their matching values from the map.
182     *
183     * @param <V> the type of the values in the map
184     * @param source  the source text containing the variables to substitute, null returns null
185     * @param valueMap  the map with the values, may be null
186     * @return the result of the replace operation
187     */
188    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
189        return new StrSubstitutor(valueMap).replace(source);
190    }
191
192    /**
193     * Replaces all the occurrences of variables in the given source object with
194     * their matching values from the map. This method allows to specify a
195     * custom variable prefix and suffix
196     *
197     * @param <V> the type of the values in the map
198     * @param source  the source text containing the variables to substitute, null returns null
199     * @param valueMap  the map with the values, may be null
200     * @param prefix  the prefix of variables, not null
201     * @param suffix  the suffix of variables, not null
202     * @return the result of the replace operation
203     * @throws IllegalArgumentException if the prefix or suffix is null
204     */
205    public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix, final String suffix) {
206        return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
207    }
208
209    /**
210     * Replaces all the occurrences of variables in the given source object with their matching
211     * values from the properties.
212     *
213     * @param source the source text containing the variables to substitute, null returns null
214     * @param valueProperties the properties with values, may be null
215     * @return the result of the replace operation
216     */
217    public static String replace(final Object source, final Properties valueProperties) {
218        if (valueProperties == null) {
219            return source.toString();
220        }
221        final Map<String, String> valueMap = new HashMap<>();
222        final Enumeration<?> propNames = valueProperties.propertyNames();
223        while (propNames.hasMoreElements()) {
224            final String propName = (String) propNames.nextElement();
225            final String propValue = valueProperties.getProperty(propName);
226            valueMap.put(propName, propValue);
227        }
228        return replace(source, valueMap);
229    }
230
231    /**
232     * Replaces all the occurrences of variables in the given source object with
233     * their matching values from the system properties.
234     *
235     * @param source  the source text containing the variables to substitute, null returns null
236     * @return the result of the replace operation
237     */
238    public static String replaceSystemProperties(final Object source) {
239        return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
240    }
241
242    //-----------------------------------------------------------------------
243    /**
244     * Creates a new instance with defaults for variable prefix and suffix
245     * and the escaping character.
246     */
247    public StrSubstitutor() {
248        this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
249    }
250
251    /**
252     * Creates a new instance and initializes it. Uses defaults for variable
253     * prefix and suffix and the escaping character.
254     *
255     * @param <V> the type of the values in the map
256     * @param valueMap  the map with the variables' values, may be null
257     */
258    public <V> StrSubstitutor(final Map<String, V> valueMap) {
259        this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
260    }
261
262    /**
263     * Creates a new instance and initializes it. Uses a default escaping character.
264     *
265     * @param <V> the type of the values in the map
266     * @param valueMap  the map with the variables' values, may be null
267     * @param prefix  the prefix for variables, not null
268     * @param suffix  the suffix for variables, not null
269     * @throws IllegalArgumentException if the prefix or suffix is null
270     */
271    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
272        this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
273    }
274
275    /**
276     * Creates a new instance and initializes it.
277     *
278     * @param <V> the type of the values in the map
279     * @param valueMap  the map with the variables' values, may be null
280     * @param prefix  the prefix for variables, not null
281     * @param suffix  the suffix for variables, not null
282     * @param escape  the escape character
283     * @throws IllegalArgumentException if the prefix or suffix is null
284     */
285    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
286                              final char escape) {
287        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
288    }
289
290    /**
291     * Creates a new instance and initializes it.
292     *
293     * @param <V> the type of the values in the map
294     * @param valueMap  the map with the variables' values, may be null
295     * @param prefix  the prefix for variables, not null
296     * @param suffix  the suffix for variables, not null
297     * @param escape  the escape character
298     * @param valueDelimiter  the variable default value delimiter, may be null
299     * @throws IllegalArgumentException if the prefix or suffix is null
300     * @since 3.2
301     */
302    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
303                              final char escape, final String valueDelimiter) {
304        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
305    }
306
307    /**
308     * Creates a new instance and initializes it.
309     *
310     * @param variableResolver  the variable resolver, may be null
311     */
312    public StrSubstitutor(final StrLookup<?> variableResolver) {
313        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
314    }
315
316    /**
317     * Creates a new instance and initializes it.
318     *
319     * @param variableResolver  the variable resolver, may be null
320     * @param prefix  the prefix for variables, not null
321     * @param suffix  the suffix for variables, not null
322     * @param escape  the escape character
323     * @throws IllegalArgumentException if the prefix or suffix is null
324     */
325    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
326                          final char escape) {
327        this.setVariableResolver(variableResolver);
328        this.setVariablePrefix(prefix);
329        this.setVariableSuffix(suffix);
330        this.setEscapeChar(escape);
331        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
332    }
333
334    /**
335     * Creates a new instance and initializes it.
336     *
337     * @param variableResolver  the variable resolver, may be null
338     * @param prefix  the prefix for variables, not null
339     * @param suffix  the suffix for variables, not null
340     * @param escape  the escape character
341     * @param valueDelimiter  the variable default value delimiter string, may be null
342     * @throws IllegalArgumentException if the prefix or suffix is null
343     * @since 3.2
344     */
345    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
346                          final char escape, final String valueDelimiter) {
347        this.setVariableResolver(variableResolver);
348        this.setVariablePrefix(prefix);
349        this.setVariableSuffix(suffix);
350        this.setEscapeChar(escape);
351        this.setValueDelimiter(valueDelimiter);
352    }
353
354    /**
355     * Creates a new instance and initializes it.
356     *
357     * @param variableResolver  the variable resolver, may be null
358     * @param prefixMatcher  the prefix for variables, not null
359     * @param suffixMatcher  the suffix for variables, not null
360     * @param escape  the escape character
361     * @throws IllegalArgumentException if the prefix or suffix is null
362     */
363    public StrSubstitutor(
364            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
365            final char escape) {
366        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
367    }
368
369    /**
370     * Creates a new instance and initializes it.
371     *
372     * @param variableResolver  the variable resolver, may be null
373     * @param prefixMatcher  the prefix for variables, not null
374     * @param suffixMatcher  the suffix for variables, not null
375     * @param escape  the escape character
376     * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
377     * @throws IllegalArgumentException if the prefix or suffix is null
378     * @since 3.2
379     */
380    public StrSubstitutor(
381            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
382            final char escape, final StrMatcher valueDelimiterMatcher) {
383        this.setVariableResolver(variableResolver);
384        this.setVariablePrefixMatcher(prefixMatcher);
385        this.setVariableSuffixMatcher(suffixMatcher);
386        this.setEscapeChar(escape);
387        this.setValueDelimiterMatcher(valueDelimiterMatcher);
388    }
389
390    //-----------------------------------------------------------------------
391    /**
392     * Replaces all the occurrences of variables with their matching values
393     * from the resolver using the given source string as a template.
394     *
395     * @param source  the string to replace in, null returns null
396     * @return the result of the replace operation
397     */
398    public String replace(final String source) {
399        if (source == null) {
400            return null;
401        }
402        final StrBuilder buf = new StrBuilder(source);
403        if (substitute(buf, 0, source.length()) == false) {
404            return source;
405        }
406        return buf.toString();
407    }
408
409    /**
410     * Replaces all the occurrences of variables with their matching values
411     * from the resolver using the given source string as a template.
412     * <p>
413     * Only the specified portion of the string will be processed.
414     * The rest of the string is not processed, and is not returned.
415     *
416     * @param source  the string to replace in, null returns null
417     * @param offset  the start offset within the array, must be valid
418     * @param length  the length within the array to be processed, must be valid
419     * @return the result of the replace operation
420     */
421    public String replace(final String source, final int offset, final int length) {
422        if (source == null) {
423            return null;
424        }
425        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
426        if (substitute(buf, 0, length) == false) {
427            return source.substring(offset, offset + length);
428        }
429        return buf.toString();
430    }
431
432    //-----------------------------------------------------------------------
433    /**
434     * Replaces all the occurrences of variables with their matching values
435     * from the resolver using the given source array as a template.
436     * The array is not altered by this method.
437     *
438     * @param source  the character array to replace in, not altered, null returns null
439     * @return the result of the replace operation
440     */
441    public String replace(final char[] source) {
442        if (source == null) {
443            return null;
444        }
445        final StrBuilder buf = new StrBuilder(source.length).append(source);
446        substitute(buf, 0, source.length);
447        return buf.toString();
448    }
449
450    /**
451     * Replaces all the occurrences of variables with their matching values
452     * from the resolver using the given source array as a template.
453     * The array is not altered by this method.
454     * <p>
455     * Only the specified portion of the array will be processed.
456     * The rest of the array is not processed, and is not returned.
457     *
458     * @param source  the character array to replace in, not altered, null returns null
459     * @param offset  the start offset within the array, must be valid
460     * @param length  the length within the array to be processed, must be valid
461     * @return the result of the replace operation
462     */
463    public String replace(final char[] source, final int offset, final int length) {
464        if (source == null) {
465            return null;
466        }
467        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
468        substitute(buf, 0, length);
469        return buf.toString();
470    }
471
472    //-----------------------------------------------------------------------
473    /**
474     * Replaces all the occurrences of variables with their matching values
475     * from the resolver using the given source buffer as a template.
476     * The buffer is not altered by this method.
477     *
478     * @param source  the buffer to use as a template, not changed, null returns null
479     * @return the result of the replace operation
480     */
481    public String replace(final StringBuffer source) {
482        if (source == null) {
483            return null;
484        }
485        final StrBuilder buf = new StrBuilder(source.length()).append(source);
486        substitute(buf, 0, buf.length());
487        return buf.toString();
488    }
489
490    /**
491     * Replaces all the occurrences of variables with their matching values
492     * from the resolver using the given source buffer as a template.
493     * The buffer is not altered by this method.
494     * <p>
495     * Only the specified portion of the buffer will be processed.
496     * The rest of the buffer is not processed, and is not returned.
497     *
498     * @param source  the buffer to use as a template, not changed, null returns null
499     * @param offset  the start offset within the array, must be valid
500     * @param length  the length within the array to be processed, must be valid
501     * @return the result of the replace operation
502     */
503    public String replace(final StringBuffer source, final int offset, final int length) {
504        if (source == null) {
505            return null;
506        }
507        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
508        substitute(buf, 0, length);
509        return buf.toString();
510    }
511
512    /**
513     * Replaces all the occurrences of variables with their matching values
514     * from the resolver using the given source as a template.
515     * The source is not altered by this method.
516     *
517     * @param source  the buffer to use as a template, not changed, null returns null
518     * @return the result of the replace operation
519     * @since 3.2
520     */
521    public String replace(final CharSequence source) {
522        if (source == null) {
523            return null;
524        }
525        return replace(source, 0, source.length());
526    }
527
528    /**
529     * Replaces all the occurrences of variables with their matching values
530     * from the resolver using the given source as a template.
531     * The source is not altered by this method.
532     * <p>
533     * Only the specified portion of the buffer will be processed.
534     * The rest of the buffer is not processed, and is not returned.
535     *
536     * @param source  the buffer to use as a template, not changed, null returns null
537     * @param offset  the start offset within the array, must be valid
538     * @param length  the length within the array to be processed, must be valid
539     * @return the result of the replace operation
540     * @since 3.2
541     */
542    public String replace(final CharSequence source, final int offset, final int length) {
543        if (source == null) {
544            return null;
545        }
546        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
547        substitute(buf, 0, length);
548        return buf.toString();
549    }
550
551    //-----------------------------------------------------------------------
552    /**
553     * Replaces all the occurrences of variables with their matching values
554     * from the resolver using the given source builder as a template.
555     * The builder is not altered by this method.
556     *
557     * @param source  the builder to use as a template, not changed, null returns null
558     * @return the result of the replace operation
559     */
560    public String replace(final StrBuilder source) {
561        if (source == null) {
562            return null;
563        }
564        final StrBuilder buf = new StrBuilder(source.length()).append(source);
565        substitute(buf, 0, buf.length());
566        return buf.toString();
567    }
568
569    /**
570     * Replaces all the occurrences of variables with their matching values
571     * from the resolver using the given source builder as a template.
572     * The builder is not altered by this method.
573     * <p>
574     * Only the specified portion of the builder will be processed.
575     * The rest of the builder is not processed, and is not returned.
576     *
577     * @param source  the builder to use as a template, not changed, null returns null
578     * @param offset  the start offset within the array, must be valid
579     * @param length  the length within the array to be processed, must be valid
580     * @return the result of the replace operation
581     */
582    public String replace(final StrBuilder source, final int offset, final int length) {
583        if (source == null) {
584            return null;
585        }
586        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
587        substitute(buf, 0, length);
588        return buf.toString();
589    }
590
591    //-----------------------------------------------------------------------
592    /**
593     * Replaces all the occurrences of variables in the given source object with
594     * their matching values from the resolver. The input source object is
595     * converted to a string using {@code toString} and is not altered.
596     *
597     * @param source  the source to replace in, null returns null
598     * @return the result of the replace operation
599     */
600    public String replace(final Object source) {
601        if (source == null) {
602            return null;
603        }
604        final StrBuilder buf = new StrBuilder().append(source);
605        substitute(buf, 0, buf.length());
606        return buf.toString();
607    }
608
609    //-----------------------------------------------------------------------
610    /**
611     * Replaces all the occurrences of variables within the given source buffer
612     * with their matching values from the resolver.
613     * The buffer is updated with the result.
614     *
615     * @param source  the buffer to replace in, updated, null returns zero
616     * @return true if altered
617     */
618    public boolean replaceIn(final StringBuffer source) {
619        if (source == null) {
620            return false;
621        }
622        return replaceIn(source, 0, source.length());
623    }
624
625    /**
626     * Replaces all the occurrences of variables within the given source buffer
627     * with their matching values from the resolver.
628     * The buffer is updated with the result.
629     * <p>
630     * Only the specified portion of the buffer will be processed.
631     * The rest of the buffer is not processed, but it is not deleted.
632     *
633     * @param source  the buffer to replace in, updated, null returns zero
634     * @param offset  the start offset within the array, must be valid
635     * @param length  the length within the buffer to be processed, must be valid
636     * @return true if altered
637     */
638    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
639        if (source == null) {
640            return false;
641        }
642        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
643        if (substitute(buf, 0, length) == false) {
644            return false;
645        }
646        source.replace(offset, offset + length, buf.toString());
647        return true;
648    }
649
650  //-----------------------------------------------------------------------
651    /**
652     * Replaces all the occurrences of variables within the given source buffer
653     * with their matching values from the resolver.
654     * The buffer is updated with the result.
655     *
656     * @param source  the buffer to replace in, updated, null returns zero
657     * @return true if altered
658     * @since 3.2
659     */
660    public boolean replaceIn(final StringBuilder source) {
661        if (source == null) {
662            return false;
663        }
664        return replaceIn(source, 0, source.length());
665    }
666
667    /**
668     * Replaces all the occurrences of variables within the given source builder
669     * with their matching values from the resolver.
670     * The builder is updated with the result.
671     * <p>
672     * Only the specified portion of the buffer will be processed.
673     * The rest of the buffer is not processed, but it is not deleted.
674     *
675     * @param source  the buffer to replace in, updated, null returns zero
676     * @param offset  the start offset within the array, must be valid
677     * @param length  the length within the buffer to be processed, must be valid
678     * @return true if altered
679     * @since 3.2
680     */
681    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
682        if (source == null) {
683            return false;
684        }
685        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
686        if (substitute(buf, 0, length) == false) {
687            return false;
688        }
689        source.replace(offset, offset + length, buf.toString());
690        return true;
691    }
692
693    //-----------------------------------------------------------------------
694    /**
695     * Replaces all the occurrences of variables within the given source
696     * builder with their matching values from the resolver.
697     *
698     * @param source  the builder to replace in, updated, null returns zero
699     * @return true if altered
700     */
701    public boolean replaceIn(final StrBuilder source) {
702        if (source == null) {
703            return false;
704        }
705        return substitute(source, 0, source.length());
706    }
707
708    /**
709     * Replaces all the occurrences of variables within the given source
710     * builder with their matching values from the resolver.
711     * <p>
712     * Only the specified portion of the builder will be processed.
713     * The rest of the builder is not processed, but it is not deleted.
714     *
715     * @param source  the builder to replace in, null returns zero
716     * @param offset  the start offset within the array, must be valid
717     * @param length  the length within the builder to be processed, must be valid
718     * @return true if altered
719     */
720    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
721        if (source == null) {
722            return false;
723        }
724        return substitute(source, offset, length);
725    }
726
727    //-----------------------------------------------------------------------
728    /**
729     * Internal method that substitutes the variables.
730     * <p>
731     * Most users of this class do not need to call this method. This method will
732     * be called automatically by another (public) method.
733     * <p>
734     * Writers of subclasses can override this method if they need access to
735     * the substitution process at the start or end.
736     *
737     * @param buf  the string builder to substitute into, not null
738     * @param offset  the start offset within the builder, must be valid
739     * @param length  the length within the builder to be processed, must be valid
740     * @return true if altered
741     */
742    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
743        return substitute(buf, offset, length, null) > 0;
744    }
745
746    /**
747     * Recursive handler for multiple levels of interpolation. This is the main
748     * interpolation method, which resolves the values of all variable references
749     * contained in the passed in text.
750     *
751     * @param buf  the string builder to substitute into, not null
752     * @param offset  the start offset within the builder, must be valid
753     * @param length  the length within the builder to be processed, must be valid
754     * @param priorVariables  the stack keeping track of the replaced variables, may be null
755     * @return the length change that occurs, unless priorVariables is null when the int
756     *  represents a boolean flag as to whether any change occurred.
757     */
758    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
759        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
760        final StrMatcher suffMatcher = getVariableSuffixMatcher();
761        final char escape = getEscapeChar();
762        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
763        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
764
765        final boolean top = priorVariables == null;
766        boolean altered = false;
767        int lengthChange = 0;
768        char[] chars = buf.buffer;
769        int bufEnd = offset + length;
770        int pos = offset;
771        while (pos < bufEnd) {
772            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
773                    bufEnd);
774            if (startMatchLen == 0) {
775                pos++;
776            } else // found variable start marker
777            if (pos > offset && chars[pos - 1] == escape) {
778                // escaped
779                if (preserveEscapes) {
780                    pos++;
781                    continue;
782                }
783                buf.deleteCharAt(pos - 1);
784                chars = buf.buffer; // in case buffer was altered
785                lengthChange--;
786                altered = true;
787                bufEnd--;
788            } else {
789                // find suffix
790                final int startPos = pos;
791                pos += startMatchLen;
792                int endMatchLen = 0;
793                int nestedVarCount = 0;
794                while (pos < bufEnd) {
795                    if (substitutionInVariablesEnabled
796                            && (endMatchLen = pfxMatcher.isMatch(chars,
797                                    pos, offset, bufEnd)) != 0) {
798                        // found a nested variable start
799                        nestedVarCount++;
800                        pos += endMatchLen;
801                        continue;
802                    }
803
804                    endMatchLen = suffMatcher.isMatch(chars, pos, offset,
805                            bufEnd);
806                    if (endMatchLen == 0) {
807                        pos++;
808                    } else {
809                        // found variable end marker
810                        if (nestedVarCount == 0) {
811                            String varNameExpr = new String(chars, startPos
812                                    + startMatchLen, pos - startPos
813                                    - startMatchLen);
814                            if (substitutionInVariablesEnabled) {
815                                final StrBuilder bufName = new StrBuilder(varNameExpr);
816                                substitute(bufName, 0, bufName.length());
817                                varNameExpr = bufName.toString();
818                            }
819                            pos += endMatchLen;
820                            final int endPos = pos;
821
822                            String varName = varNameExpr;
823                            String varDefaultValue = null;
824
825                            if (valueDelimMatcher != null) {
826                                final char [] varNameExprChars = varNameExpr.toCharArray();
827                                int valueDelimiterMatchLen = 0;
828                                for (int i = 0; i < varNameExprChars.length; i++) {
829                                    // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
830                                    if (!substitutionInVariablesEnabled
831                                            && pfxMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
832                                        break;
833                                    }
834                                    if ((valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i)) != 0) {
835                                        varName = varNameExpr.substring(0, i);
836                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
837                                        break;
838                                    }
839                                }
840                            }
841
842                            // on the first call initialize priorVariables
843                            if (priorVariables == null) {
844                                priorVariables = new ArrayList<>();
845                                priorVariables.add(new String(chars,
846                                        offset, length));
847                            }
848
849                            // handle cyclic substitution
850                            checkCyclicSubstitution(varName, priorVariables);
851                            priorVariables.add(varName);
852
853                            // resolve the variable
854                            String varValue = resolveVariable(varName, buf,
855                                    startPos, endPos);
856                            if (varValue == null) {
857                                varValue = varDefaultValue;
858                            }
859                            if (varValue != null) {
860                                // recursive replace
861                                final int varLen = varValue.length();
862                                buf.replace(startPos, endPos, varValue);
863                                altered = true;
864                                int change = substitute(buf, startPos,
865                                        varLen, priorVariables);
866                                change = change
867                                        + varLen - (endPos - startPos);
868                                pos += change;
869                                bufEnd += change;
870                                lengthChange += change;
871                                chars = buf.buffer; // in case buffer was
872                                                    // altered
873                            }
874
875                            // remove variable from the cyclic stack
876                            priorVariables
877                                    .remove(priorVariables.size() - 1);
878                            break;
879                        }
880                        nestedVarCount--;
881                        pos += endMatchLen;
882                    }
883                }
884            }
885        }
886        if (top) {
887            return altered ? 1 : 0;
888        }
889        return lengthChange;
890    }
891
892    /**
893     * Checks if the specified variable is already in the stack (list) of variables.
894     *
895     * @param varName  the variable name to check
896     * @param priorVariables  the list of prior variables
897     */
898    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
899        if (priorVariables.contains(varName) == false) {
900            return;
901        }
902        final StrBuilder buf = new StrBuilder(256);
903        buf.append("Infinite loop in property interpolation of ");
904        buf.append(priorVariables.remove(0));
905        buf.append(": ");
906        buf.appendWithSeparators(priorVariables, "->");
907        throw new IllegalStateException(buf.toString());
908    }
909
910    /**
911     * Internal method that resolves the value of a variable.
912     * <p>
913     * Most users of this class do not need to call this method. This method is
914     * called automatically by the substitution process.
915     * <p>
916     * Writers of subclasses can override this method if they need to alter
917     * how each substitution occurs. The method is passed the variable's name
918     * and must return the corresponding value. This implementation uses the
919     * {@link #getVariableResolver()} with the variable's name as the key.
920     *
921     * @param variableName  the name of the variable, not null
922     * @param buf  the buffer where the substitution is occurring, not null
923     * @param startPos  the start position of the variable including the prefix, valid
924     * @param endPos  the end position of the variable including the suffix, valid
925     * @return the variable's value or <b>null</b> if the variable is unknown
926     */
927    protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
928        final StrLookup<?> resolver = getVariableResolver();
929        if (resolver == null) {
930            return null;
931        }
932        return resolver.lookup(variableName);
933    }
934
935    // Escape
936    //-----------------------------------------------------------------------
937    /**
938     * Returns the escape character.
939     *
940     * @return the character used for escaping variable references
941     */
942    public char getEscapeChar() {
943        return this.escapeChar;
944    }
945
946    /**
947     * Sets the escape character.
948     * If this character is placed before a variable reference in the source
949     * text, this variable will be ignored.
950     *
951     * @param escapeCharacter  the escape character (0 for disabling escaping)
952     */
953    public void setEscapeChar(final char escapeCharacter) {
954        this.escapeChar = escapeCharacter;
955    }
956
957    // Prefix
958    //-----------------------------------------------------------------------
959    /**
960     * Gets the variable prefix matcher currently in use.
961     * <p>
962     * The variable prefix is the character or characters that identify the
963     * start of a variable. This prefix is expressed in terms of a matcher
964     * allowing advanced prefix matches.
965     *
966     * @return the prefix matcher in use
967     */
968    public StrMatcher getVariablePrefixMatcher() {
969        return prefixMatcher;
970    }
971
972    /**
973     * Sets the variable prefix matcher currently in use.
974     * <p>
975     * The variable prefix is the character or characters that identify the
976     * start of a variable. This prefix is expressed in terms of a matcher
977     * allowing advanced prefix matches.
978     *
979     * @param prefixMatcher  the prefix matcher to use, null ignored
980     * @return this, to enable chaining
981     * @throws IllegalArgumentException if the prefix matcher is null
982     */
983    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
984        if (prefixMatcher == null) {
985            throw new IllegalArgumentException("Variable prefix matcher must not be null.");
986        }
987        this.prefixMatcher = prefixMatcher;
988        return this;
989    }
990
991    /**
992     * Sets the variable prefix to use.
993     * <p>
994     * The variable prefix is the character or characters that identify the
995     * start of a variable. This method allows a single character prefix to
996     * be easily set.
997     *
998     * @param prefix  the prefix character to use
999     * @return this, to enable chaining
1000     */
1001    public StrSubstitutor setVariablePrefix(final char prefix) {
1002        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
1003    }
1004
1005    /**
1006     * Sets the variable prefix to use.
1007     * <p>
1008     * The variable prefix is the character or characters that identify the
1009     * start of a variable. This method allows a string prefix to be easily set.
1010     *
1011     * @param prefix  the prefix for variables, not null
1012     * @return this, to enable chaining
1013     * @throws IllegalArgumentException if the prefix is null
1014     */
1015    public StrSubstitutor setVariablePrefix(final String prefix) {
1016       if (prefix == null) {
1017            throw new IllegalArgumentException("Variable prefix must not be null.");
1018        }
1019        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
1020    }
1021
1022    // Suffix
1023    //-----------------------------------------------------------------------
1024    /**
1025     * Gets the variable suffix matcher currently in use.
1026     * <p>
1027     * The variable suffix is the character or characters that identify the
1028     * end of a variable. This suffix is expressed in terms of a matcher
1029     * allowing advanced suffix matches.
1030     *
1031     * @return the suffix matcher in use
1032     */
1033    public StrMatcher getVariableSuffixMatcher() {
1034        return suffixMatcher;
1035    }
1036
1037    /**
1038     * Sets the variable suffix matcher currently in use.
1039     * <p>
1040     * The variable suffix is the character or characters that identify the
1041     * end of a variable. This suffix is expressed in terms of a matcher
1042     * allowing advanced suffix matches.
1043     *
1044     * @param suffixMatcher  the suffix matcher to use, null ignored
1045     * @return this, to enable chaining
1046     * @throws IllegalArgumentException if the suffix matcher is null
1047     */
1048    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1049        if (suffixMatcher == null) {
1050            throw new IllegalArgumentException("Variable suffix matcher must not be null.");
1051        }
1052        this.suffixMatcher = suffixMatcher;
1053        return this;
1054    }
1055
1056    /**
1057     * Sets the variable suffix to use.
1058     * <p>
1059     * The variable suffix is the character or characters that identify the
1060     * end of a variable. This method allows a single character suffix to
1061     * be easily set.
1062     *
1063     * @param suffix  the suffix character to use
1064     * @return this, to enable chaining
1065     */
1066    public StrSubstitutor setVariableSuffix(final char suffix) {
1067        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1068    }
1069
1070    /**
1071     * Sets the variable suffix to use.
1072     * <p>
1073     * The variable suffix is the character or characters that identify the
1074     * end of a variable. This method allows a string suffix to be easily set.
1075     *
1076     * @param suffix  the suffix for variables, not null
1077     * @return this, to enable chaining
1078     * @throws IllegalArgumentException if the suffix is null
1079     */
1080    public StrSubstitutor setVariableSuffix(final String suffix) {
1081       if (suffix == null) {
1082            throw new IllegalArgumentException("Variable suffix must not be null.");
1083        }
1084        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1085    }
1086
1087    // Variable Default Value Delimiter
1088    //-----------------------------------------------------------------------
1089    /**
1090     * Gets the variable default value delimiter matcher currently in use.
1091     * <p>
1092     * The variable default value delimiter is the character or characters that delimit the
1093     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1094     * allowing advanced variable default value delimiter matches.
1095     * <p>
1096     * If it returns null, then the variable default value resolution is disabled.
1097     *
1098     * @return the variable default value delimiter matcher in use, may be null
1099     * @since 3.2
1100     */
1101    public StrMatcher getValueDelimiterMatcher() {
1102        return valueDelimiterMatcher;
1103    }
1104
1105    /**
1106     * Sets the variable default value delimiter matcher to use.
1107     * <p>
1108     * The variable default value delimiter is the character or characters that delimit the
1109     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1110     * allowing advanced variable default value delimiter matches.
1111     * <p>
1112     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution
1113     * becomes disabled.
1114     *
1115     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
1116     * @return this, to enable chaining
1117     * @since 3.2
1118     */
1119    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
1120        this.valueDelimiterMatcher = valueDelimiterMatcher;
1121        return this;
1122    }
1123
1124    /**
1125     * Sets the variable default value delimiter to use.
1126     * <p>
1127     * The variable default value delimiter is the character or characters that delimit the
1128     * variable name and the variable default value. This method allows a single character
1129     * variable default value delimiter to be easily set.
1130     *
1131     * @param valueDelimiter  the variable default value delimiter character to use
1132     * @return this, to enable chaining
1133     * @since 3.2
1134     */
1135    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
1136        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
1137    }
1138
1139    /**
1140     * Sets the variable default value delimiter to use.
1141     * <p>
1142     * The variable default value delimiter is the character or characters that delimit the
1143     * variable name and the variable default value. This method allows a string
1144     * variable default value delimiter to be easily set.
1145     * <p>
1146     * If the {@code valueDelimiter} is null or empty string, then the variable default
1147     * value resolution becomes disabled.
1148     *
1149     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
1150     * @return this, to enable chaining
1151     * @since 3.2
1152     */
1153    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
1154        if (StringUtils.isEmpty(valueDelimiter)) {
1155            setValueDelimiterMatcher(null);
1156            return this;
1157        }
1158        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
1159    }
1160
1161    // Resolver
1162    //-----------------------------------------------------------------------
1163    /**
1164     * Gets the VariableResolver that is used to lookup variables.
1165     *
1166     * @return the VariableResolver
1167     */
1168    public StrLookup<?> getVariableResolver() {
1169        return this.variableResolver;
1170    }
1171
1172    /**
1173     * Sets the VariableResolver that is used to lookup variables.
1174     *
1175     * @param variableResolver  the VariableResolver
1176     */
1177    public void setVariableResolver(final StrLookup<?> variableResolver) {
1178        this.variableResolver = variableResolver;
1179    }
1180
1181    // Substitution support in variable names
1182    //-----------------------------------------------------------------------
1183    /**
1184     * Returns a flag whether substitution is done in variable names.
1185     *
1186     * @return the substitution in variable names flag
1187     * @since 3.0
1188     */
1189    public boolean isEnableSubstitutionInVariables() {
1190        return enableSubstitutionInVariables;
1191    }
1192
1193    /**
1194     * Sets a flag whether substitution is done in variable names. If set to
1195     * <b>true</b>, the names of variables can contain other variables which are
1196     * processed first before the original variable is evaluated, e.g.
1197     * {@code ${jre-${java.version}}}. The default value is <b>false</b>.
1198     *
1199     * @param enableSubstitutionInVariables the new value of the flag
1200     * @since 3.0
1201     */
1202    public void setEnableSubstitutionInVariables(
1203            final boolean enableSubstitutionInVariables) {
1204        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
1205    }
1206
1207    /**
1208     * Returns the flag controlling whether escapes are preserved during
1209     * substitution.
1210     *
1211     * @return the preserve escape flag
1212     * @since 3.5
1213     */
1214    public boolean isPreserveEscapes() {
1215        return preserveEscapes;
1216    }
1217
1218    /**
1219     * Sets a flag controlling whether escapes are preserved during
1220     * substitution.  If set to <b>true</b>, the escape character is retained
1221     * during substitution (e.g. {@code $${this-is-escaped}} remains
1222     * {@code $${this-is-escaped}}).  If set to <b>false</b>, the escape
1223     * character is removed during substitution (e.g.
1224     * {@code $${this-is-escaped}} becomes
1225     * {@code ${this-is-escaped}}).  The default value is <b>false</b>
1226     *
1227     * @param preserveEscapes true if escapes are to be preserved
1228     * @since 3.5
1229     */
1230    public void setPreserveEscapes(final boolean preserveEscapes) {
1231        this.preserveEscapes = preserveEscapes;
1232    }
1233}