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}</code>.
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()</code>
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</code> variable is <code>x</code>):
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</code> 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 = false;
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</code> 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 {
777                // found variable start marker
778                if (pos > offset && chars[pos - 1] == escape) {
779                    // escaped
780                    if (preserveEscapes) {
781                        pos++;
782                        continue;
783                    }
784                    buf.deleteCharAt(pos - 1);
785                    chars = buf.buffer; // in case buffer was altered
786                    lengthChange--;
787                    altered = true;
788                    bufEnd--;
789                } else {
790                    // find suffix
791                    final int startPos = pos;
792                    pos += startMatchLen;
793                    int endMatchLen = 0;
794                    int nestedVarCount = 0;
795                    while (pos < bufEnd) {
796                        if (substitutionInVariablesEnabled
797                                && (endMatchLen = pfxMatcher.isMatch(chars,
798                                        pos, offset, bufEnd)) != 0) {
799                            // found a nested variable start
800                            nestedVarCount++;
801                            pos += endMatchLen;
802                            continue;
803                        }
804
805                        endMatchLen = suffMatcher.isMatch(chars, pos, offset,
806                                bufEnd);
807                        if (endMatchLen == 0) {
808                            pos++;
809                        } else {
810                            // found variable end marker
811                            if (nestedVarCount == 0) {
812                                String varNameExpr = new String(chars, startPos
813                                        + startMatchLen, pos - startPos
814                                        - startMatchLen);
815                                if (substitutionInVariablesEnabled) {
816                                    final StrBuilder bufName = new StrBuilder(varNameExpr);
817                                    substitute(bufName, 0, bufName.length());
818                                    varNameExpr = bufName.toString();
819                                }
820                                pos += endMatchLen;
821                                final int endPos = pos;
822
823                                String varName = varNameExpr;
824                                String varDefaultValue = null;
825
826                                if (valueDelimMatcher != null) {
827                                    final char [] varNameExprChars = varNameExpr.toCharArray();
828                                    int valueDelimiterMatchLen = 0;
829                                    for (int i = 0; i < varNameExprChars.length; i++) {
830                                        // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
831                                        if (!substitutionInVariablesEnabled
832                                                && pfxMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
833                                            break;
834                                        }
835                                        if ((valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i)) != 0) {
836                                            varName = varNameExpr.substring(0, i);
837                                            varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
838                                            break;
839                                        }
840                                    }
841                                }
842
843                                // on the first call initialize priorVariables
844                                if (priorVariables == null) {
845                                    priorVariables = new ArrayList<>();
846                                    priorVariables.add(new String(chars,
847                                            offset, length));
848                                }
849
850                                // handle cyclic substitution
851                                checkCyclicSubstitution(varName, priorVariables);
852                                priorVariables.add(varName);
853
854                                // resolve the variable
855                                String varValue = resolveVariable(varName, buf,
856                                        startPos, endPos);
857                                if (varValue == null) {
858                                    varValue = varDefaultValue;
859                                }
860                                if (varValue != null) {
861                                    // recursive replace
862                                    final int varLen = varValue.length();
863                                    buf.replace(startPos, endPos, varValue);
864                                    altered = true;
865                                    int change = substitute(buf, startPos,
866                                            varLen, priorVariables);
867                                    change = change
868                                            + varLen - (endPos - startPos);
869                                    pos += change;
870                                    bufEnd += change;
871                                    lengthChange += change;
872                                    chars = buf.buffer; // in case buffer was
873                                                        // altered
874                                }
875
876                                // remove variable from the cyclic stack
877                                priorVariables
878                                        .remove(priorVariables.size() - 1);
879                                break;
880                            }
881                            nestedVarCount--;
882                            pos += endMatchLen;
883                        }
884                    }
885                }
886            }
887        }
888        if (top) {
889            return altered ? 1 : 0;
890        }
891        return lengthChange;
892    }
893
894    /**
895     * Checks if the specified variable is already in the stack (list) of variables.
896     *
897     * @param varName  the variable name to check
898     * @param priorVariables  the list of prior variables
899     */
900    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
901        if (priorVariables.contains(varName) == false) {
902            return;
903        }
904        final StrBuilder buf = new StrBuilder(256);
905        buf.append("Infinite loop in property interpolation of ");
906        buf.append(priorVariables.remove(0));
907        buf.append(": ");
908        buf.appendWithSeparators(priorVariables, "->");
909        throw new IllegalStateException(buf.toString());
910    }
911
912    /**
913     * Internal method that resolves the value of a variable.
914     * <p>
915     * Most users of this class do not need to call this method. This method is
916     * called automatically by the substitution process.
917     * <p>
918     * Writers of subclasses can override this method if they need to alter
919     * how each substitution occurs. The method is passed the variable's name
920     * and must return the corresponding value. This implementation uses the
921     * {@link #getVariableResolver()} with the variable's name as the key.
922     *
923     * @param variableName  the name of the variable, not null
924     * @param buf  the buffer where the substitution is occurring, not null
925     * @param startPos  the start position of the variable including the prefix, valid
926     * @param endPos  the end position of the variable including the suffix, valid
927     * @return the variable's value or <b>null</b> if the variable is unknown
928     */
929    protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
930        final StrLookup<?> resolver = getVariableResolver();
931        if (resolver == null) {
932            return null;
933        }
934        return resolver.lookup(variableName);
935    }
936
937    // Escape
938    //-----------------------------------------------------------------------
939    /**
940     * Returns the escape character.
941     *
942     * @return the character used for escaping variable references
943     */
944    public char getEscapeChar() {
945        return this.escapeChar;
946    }
947
948    /**
949     * Sets the escape character.
950     * If this character is placed before a variable reference in the source
951     * text, this variable will be ignored.
952     *
953     * @param escapeCharacter  the escape character (0 for disabling escaping)
954     */
955    public void setEscapeChar(final char escapeCharacter) {
956        this.escapeChar = escapeCharacter;
957    }
958
959    // Prefix
960    //-----------------------------------------------------------------------
961    /**
962     * Gets the variable prefix matcher currently in use.
963     * <p>
964     * The variable prefix is the character or characters that identify the
965     * start of a variable. This prefix is expressed in terms of a matcher
966     * allowing advanced prefix matches.
967     *
968     * @return the prefix matcher in use
969     */
970    public StrMatcher getVariablePrefixMatcher() {
971        return prefixMatcher;
972    }
973
974    /**
975     * Sets the variable prefix matcher currently in use.
976     * <p>
977     * The variable prefix is the character or characters that identify the
978     * start of a variable. This prefix is expressed in terms of a matcher
979     * allowing advanced prefix matches.
980     *
981     * @param prefixMatcher  the prefix matcher to use, null ignored
982     * @return this, to enable chaining
983     * @throws IllegalArgumentException if the prefix matcher is null
984     */
985    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
986        if (prefixMatcher == null) {
987            throw new IllegalArgumentException("Variable prefix matcher must not be null!");
988        }
989        this.prefixMatcher = prefixMatcher;
990        return this;
991    }
992
993    /**
994     * Sets the variable prefix to use.
995     * <p>
996     * The variable prefix is the character or characters that identify the
997     * start of a variable. This method allows a single character prefix to
998     * be easily set.
999     *
1000     * @param prefix  the prefix character to use
1001     * @return this, to enable chaining
1002     */
1003    public StrSubstitutor setVariablePrefix(final char prefix) {
1004        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
1005    }
1006
1007    /**
1008     * Sets the variable prefix to use.
1009     * <p>
1010     * The variable prefix is the character or characters that identify the
1011     * start of a variable. This method allows a string prefix to be easily set.
1012     *
1013     * @param prefix  the prefix for variables, not null
1014     * @return this, to enable chaining
1015     * @throws IllegalArgumentException if the prefix is null
1016     */
1017    public StrSubstitutor setVariablePrefix(final String prefix) {
1018       if (prefix == null) {
1019            throw new IllegalArgumentException("Variable prefix must not be null!");
1020        }
1021        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
1022    }
1023
1024    // Suffix
1025    //-----------------------------------------------------------------------
1026    /**
1027     * Gets the variable suffix matcher currently in use.
1028     * <p>
1029     * The variable suffix is the character or characters that identify the
1030     * end of a variable. This suffix is expressed in terms of a matcher
1031     * allowing advanced suffix matches.
1032     *
1033     * @return the suffix matcher in use
1034     */
1035    public StrMatcher getVariableSuffixMatcher() {
1036        return suffixMatcher;
1037    }
1038
1039    /**
1040     * Sets the variable suffix matcher currently in use.
1041     * <p>
1042     * The variable suffix is the character or characters that identify the
1043     * end of a variable. This suffix is expressed in terms of a matcher
1044     * allowing advanced suffix matches.
1045     *
1046     * @param suffixMatcher  the suffix matcher to use, null ignored
1047     * @return this, to enable chaining
1048     * @throws IllegalArgumentException if the suffix matcher is null
1049     */
1050    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1051        if (suffixMatcher == null) {
1052            throw new IllegalArgumentException("Variable suffix matcher must not be null!");
1053        }
1054        this.suffixMatcher = suffixMatcher;
1055        return this;
1056    }
1057
1058    /**
1059     * Sets the variable suffix to use.
1060     * <p>
1061     * The variable suffix is the character or characters that identify the
1062     * end of a variable. This method allows a single character suffix to
1063     * be easily set.
1064     *
1065     * @param suffix  the suffix character to use
1066     * @return this, to enable chaining
1067     */
1068    public StrSubstitutor setVariableSuffix(final char suffix) {
1069        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1070    }
1071
1072    /**
1073     * Sets the variable suffix to use.
1074     * <p>
1075     * The variable suffix is the character or characters that identify the
1076     * end of a variable. This method allows a string suffix to be easily set.
1077     *
1078     * @param suffix  the suffix for variables, not null
1079     * @return this, to enable chaining
1080     * @throws IllegalArgumentException if the suffix is null
1081     */
1082    public StrSubstitutor setVariableSuffix(final String suffix) {
1083       if (suffix == null) {
1084            throw new IllegalArgumentException("Variable suffix must not be null!");
1085        }
1086        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1087    }
1088
1089    // Variable Default Value Delimiter
1090    //-----------------------------------------------------------------------
1091    /**
1092     * Gets the variable default value delimiter matcher currently in use.
1093     * <p>
1094     * The variable default value delimiter is the character or characters that delimit the
1095     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1096     * allowing advanced variable default value delimiter matches.
1097     * <p>
1098     * If it returns null, then the variable default value resolution is disabled.
1099     *
1100     * @return the variable default value delimiter matcher in use, may be null
1101     * @since 3.2
1102     */
1103    public StrMatcher getValueDelimiterMatcher() {
1104        return valueDelimiterMatcher;
1105    }
1106
1107    /**
1108     * Sets the variable default value delimiter matcher to use.
1109     * <p>
1110     * The variable default value delimiter is the character or characters that delimit the
1111     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1112     * allowing advanced variable default value delimiter matches.
1113     * <p>
1114     * If the <code>valueDelimiterMatcher</code> is null, then the variable default value resolution
1115     * becomes disabled.
1116     *
1117     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
1118     * @return this, to enable chaining
1119     * @since 3.2
1120     */
1121    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
1122        this.valueDelimiterMatcher = valueDelimiterMatcher;
1123        return this;
1124    }
1125
1126    /**
1127     * Sets the variable default value delimiter to use.
1128     * <p>
1129     * The variable default value delimiter is the character or characters that delimit the
1130     * variable name and the variable default value. This method allows a single character
1131     * variable default value delimiter to be easily set.
1132     *
1133     * @param valueDelimiter  the variable default value delimiter character to use
1134     * @return this, to enable chaining
1135     * @since 3.2
1136     */
1137    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
1138        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
1139    }
1140
1141    /**
1142     * Sets the variable default value delimiter to use.
1143     * <p>
1144     * The variable default value delimiter is the character or characters that delimit the
1145     * variable name and the variable default value. This method allows a string
1146     * variable default value delimiter to be easily set.
1147     * <p>
1148     * If the <code>valueDelimiter</code> is null or empty string, then the variable default
1149     * value resolution becomes disabled.
1150     *
1151     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
1152     * @return this, to enable chaining
1153     * @since 3.2
1154     */
1155    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
1156        if (StringUtils.isEmpty(valueDelimiter)) {
1157            setValueDelimiterMatcher(null);
1158            return this;
1159        }
1160        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
1161    }
1162
1163    // Resolver
1164    //-----------------------------------------------------------------------
1165    /**
1166     * Gets the VariableResolver that is used to lookup variables.
1167     *
1168     * @return the VariableResolver
1169     */
1170    public StrLookup<?> getVariableResolver() {
1171        return this.variableResolver;
1172    }
1173
1174    /**
1175     * Sets the VariableResolver that is used to lookup variables.
1176     *
1177     * @param variableResolver  the VariableResolver
1178     */
1179    public void setVariableResolver(final StrLookup<?> variableResolver) {
1180        this.variableResolver = variableResolver;
1181    }
1182
1183    // Substitution support in variable names
1184    //-----------------------------------------------------------------------
1185    /**
1186     * Returns a flag whether substitution is done in variable names.
1187     *
1188     * @return the substitution in variable names flag
1189     * @since 3.0
1190     */
1191    public boolean isEnableSubstitutionInVariables() {
1192        return enableSubstitutionInVariables;
1193    }
1194
1195    /**
1196     * Sets a flag whether substitution is done in variable names. If set to
1197     * <b>true</b>, the names of variables can contain other variables which are
1198     * processed first before the original variable is evaluated, e.g.
1199     * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
1200     *
1201     * @param enableSubstitutionInVariables the new value of the flag
1202     * @since 3.0
1203     */
1204    public void setEnableSubstitutionInVariables(
1205            final boolean enableSubstitutionInVariables) {
1206        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
1207    }
1208
1209    /**
1210     * Returns the flag controlling whether escapes are preserved during
1211     * substitution.
1212     *
1213     * @return the preserve escape flag
1214     * @since 3.5
1215     */
1216    public boolean isPreserveEscapes() {
1217        return preserveEscapes;
1218    }
1219
1220    /**
1221     * Sets a flag controlling whether escapes are preserved during
1222     * substitution.  If set to <b>true</b>, the escape character is retained
1223     * during substitution (e.g. <code>$${this-is-escaped}</code> remains
1224     * <code>$${this-is-escaped}</code>).  If set to <b>false</b>, the escape
1225     * character is removed during substitution (e.g.
1226     * <code>$${this-is-escaped}</code> becomes
1227     * <code>${this-is-escaped}</code>).  The default value is <b>false</b>
1228     *
1229     * @param preserveEscapes true if escapes are to be preserved
1230     * @since 3.5
1231     */
1232    public void setPreserveEscapes(final boolean preserveEscapes) {
1233        this.preserveEscapes = preserveEscapes;
1234    }
1235}