001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.text;
018
019import java.util.ArrayList;
020import java.util.Enumeration;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025
026import org.apache.commons.lang3.Validate;
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} jumped 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 jumped 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} jumped 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 jumped 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 1.0
124 */
125public class StrSubstitutor {
126
127    /**
128     * Constant for the default escape character.
129     */
130    public static final char DEFAULT_ESCAPE = '$';
131    /**
132     * Constant for the default variable prefix.
133     */
134    public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
135    /**
136     * Constant for the default variable suffix.
137     */
138    public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
139    /**
140     * Constant for the default value delimiter of a variable.
141     */
142    public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
143
144    /**
145     * Stores the escape character.
146     */
147    private char escapeChar;
148    /**
149     * Stores the variable prefix.
150     */
151    private StrMatcher prefixMatcher;
152    /**
153     * Stores the variable suffix.
154     */
155    private StrMatcher suffixMatcher;
156    /**
157     * Stores the default variable value delimiter.
158     */
159    private StrMatcher valueDelimiterMatcher;
160    /**
161     * Variable resolution is delegated to an implementor of VariableResolver.
162     */
163    private StrLookup<?> variableResolver;
164    /**
165     * The flag whether substitution in variable names is enabled.
166     */
167    private boolean enableSubstitutionInVariables;
168    /**
169     * Whether escapes should be preserved.  Default is false;
170     */
171    private boolean preserveEscapes = false;
172
173    //-----------------------------------------------------------------------
174    /**
175     * Replaces all the occurrences of variables in the given source object with
176     * their matching values from the map.
177     *
178     * @param <V> the type of the values in the map
179     * @param source  the source text containing the variables to substitute, null returns null
180     * @param valueMap  the map with the values, may be null
181     * @return the result of the replace operation
182     */
183    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
184        return new StrSubstitutor(valueMap).replace(source);
185    }
186
187    /**
188     * Replaces all the occurrences of variables in the given source object with
189     * their matching values from the map. This method allows to specify a
190     * custom variable prefix and suffix
191     *
192     * @param <V> the type of the values in the map
193     * @param source  the source text containing the variables to substitute, null returns null
194     * @param valueMap  the map with the values, may be null
195     * @param prefix  the prefix of variables, not null
196     * @param suffix  the suffix of variables, not null
197     * @return the result of the replace operation
198     * @throws IllegalArgumentException if the prefix or suffix is null
199     */
200    public static <V> String replace(final Object source,
201                                     final Map<String, V> valueMap,
202                                     final String prefix,
203                                     final String suffix) {
204        return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
205    }
206
207    /**
208     * Replaces all the occurrences of variables in the given source object with their matching
209     * values from the properties.
210     *
211     * @param source the source text containing the variables to substitute, null returns null
212     * @param valueProperties the properties with values, may be null
213     * @return the result of the replace operation
214     */
215    public static String replace(final Object source, final Properties valueProperties) {
216        if (valueProperties == null) {
217            return source.toString();
218        }
219        final Map<String, String> valueMap = new HashMap<>();
220        final Enumeration<?> propNames = valueProperties.propertyNames();
221        while (propNames.hasMoreElements()) {
222            final String propName = (String) propNames.nextElement();
223            final String propValue = valueProperties.getProperty(propName);
224            valueMap.put(propName, propValue);
225        }
226        return StrSubstitutor.replace(source, valueMap);
227    }
228
229    /**
230     * Replaces all the occurrences of variables in the given source object with
231     * their matching values from the system properties.
232     *
233     * @param source  the source text containing the variables to substitute, null returns null
234     * @return the result of the replace operation
235     */
236    public static String replaceSystemProperties(final Object source) {
237        return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
238    }
239
240    //-----------------------------------------------------------------------
241    /**
242     * Creates a new instance with defaults for variable prefix and suffix
243     * and the escaping character.
244     */
245    public StrSubstitutor() {
246        this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
247    }
248
249    /**
250     * Creates a new instance and initializes it. Uses defaults for variable
251     * prefix and suffix and the escaping character.
252     *
253     * @param <V> the type of the values in the map
254     * @param valueMap  the map with the variables' values, may be null
255     */
256    public <V> StrSubstitutor(final Map<String, V> valueMap) {
257        this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
258    }
259
260    /**
261     * Creates a new instance and initializes it. Uses a default escaping character.
262     *
263     * @param <V> the type of the values in the map
264     * @param valueMap  the map with the variables' values, may be null
265     * @param prefix  the prefix for variables, not null
266     * @param suffix  the suffix for variables, not null
267     * @throws IllegalArgumentException if the prefix or suffix is null
268     */
269    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
270        this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
271    }
272
273    /**
274     * Creates a new instance and initializes it.
275     *
276     * @param <V> the type of the values in the map
277     * @param valueMap  the map with the variables' values, may be null
278     * @param prefix  the prefix for variables, not null
279     * @param suffix  the suffix for variables, not null
280     * @param escape  the escape character
281     * @throws IllegalArgumentException if the prefix or suffix is null
282     */
283    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
284                              final char escape) {
285        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
286    }
287
288    /**
289     * Creates a new instance and initializes it.
290     *
291     * @param <V> the type of the values in the map
292     * @param valueMap  the map with the variables' values, may be null
293     * @param prefix  the prefix for variables, not null
294     * @param suffix  the suffix for variables, not null
295     * @param escape  the escape character
296     * @param valueDelimiter  the variable default value delimiter, may be null
297     * @throws IllegalArgumentException if the prefix or suffix is null
298     */
299    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
300                              final char escape, final String valueDelimiter) {
301        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
302    }
303
304    /**
305     * Creates a new instance and initializes it.
306     *
307     * @param variableResolver  the variable resolver, may be null
308     */
309    public StrSubstitutor(final StrLookup<?> variableResolver) {
310        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
311    }
312
313    /**
314     * Creates a new instance and initializes it.
315     *
316     * @param variableResolver  the variable resolver, may be null
317     * @param prefix  the prefix for variables, not null
318     * @param suffix  the suffix for variables, not null
319     * @param escape  the escape character
320     * @throws IllegalArgumentException if the prefix or suffix is null
321     */
322    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
323                          final char escape) {
324        this.setVariableResolver(variableResolver);
325        this.setVariablePrefix(prefix);
326        this.setVariableSuffix(suffix);
327        this.setEscapeChar(escape);
328        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
329    }
330
331    /**
332     * Creates a new instance and initializes it.
333     *
334     * @param variableResolver  the variable resolver, may be null
335     * @param prefix  the prefix for variables, not null
336     * @param suffix  the suffix for variables, not null
337     * @param escape  the escape character
338     * @param valueDelimiter  the variable default value delimiter string, may be null
339     * @throws IllegalArgumentException if the prefix or suffix is null
340     */
341    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
342                          final char escape, final String valueDelimiter) {
343        this.setVariableResolver(variableResolver);
344        this.setVariablePrefix(prefix);
345        this.setVariableSuffix(suffix);
346        this.setEscapeChar(escape);
347        this.setValueDelimiter(valueDelimiter);
348    }
349
350    /**
351     * Creates a new instance and initializes it.
352     *
353     * @param variableResolver  the variable resolver, may be null
354     * @param prefixMatcher  the prefix for variables, not null
355     * @param suffixMatcher  the suffix for variables, not null
356     * @param escape  the escape character
357     * @throws IllegalArgumentException if the prefix or suffix is null
358     */
359    public StrSubstitutor(
360            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
361            final char escape) {
362        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
363    }
364
365    /**
366     * Creates a new instance and initializes it.
367     *
368     * @param variableResolver  the variable resolver, may be null
369     * @param prefixMatcher  the prefix for variables, not null
370     * @param suffixMatcher  the suffix for variables, not null
371     * @param escape  the escape character
372     * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
373     * @throws IllegalArgumentException if the prefix or suffix is null
374     */
375    public StrSubstitutor(
376            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
377            final char escape, final StrMatcher valueDelimiterMatcher) {
378        this.setVariableResolver(variableResolver);
379        this.setVariablePrefixMatcher(prefixMatcher);
380        this.setVariableSuffixMatcher(suffixMatcher);
381        this.setEscapeChar(escape);
382        this.setValueDelimiterMatcher(valueDelimiterMatcher);
383    }
384
385    //-----------------------------------------------------------------------
386    /**
387     * Replaces all the occurrences of variables with their matching values
388     * from the resolver using the given source string as a template.
389     *
390     * @param source  the string to replace in, null returns null
391     * @return the result of the replace operation
392     */
393    public String replace(final String source) {
394        if (source == null) {
395            return null;
396        }
397        final StrBuilder buf = new StrBuilder(source);
398        if (!substitute(buf, 0, source.length())) {
399            return source;
400        }
401        return buf.toString();
402    }
403
404    /**
405     * Replaces all the occurrences of variables with their matching values
406     * from the resolver using the given source string as a template.
407     * <p>
408     * Only the specified portion of the string will be processed.
409     * The rest of the string is not processed, and is not returned.
410     *
411     * @param source  the string to replace in, null returns null
412     * @param offset  the start offset within the array, must be valid
413     * @param length  the length within the array to be processed, must be valid
414     * @return the result of the replace operation
415     */
416    public String replace(final String source, final int offset, final int length) {
417        if (source == null) {
418            return null;
419        }
420        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
421        if (!substitute(buf, 0, length)) {
422            return source.substring(offset, offset + length);
423        }
424        return buf.toString();
425    }
426
427    //-----------------------------------------------------------------------
428    /**
429     * Replaces all the occurrences of variables with their matching values
430     * from the resolver using the given source array as a template.
431     * The array is not altered by this method.
432     *
433     * @param source  the character array to replace in, not altered, null returns null
434     * @return the result of the replace operation
435     */
436    public String replace(final char[] source) {
437        if (source == null) {
438            return null;
439        }
440        final StrBuilder buf = new StrBuilder(source.length).append(source);
441        substitute(buf, 0, source.length);
442        return buf.toString();
443    }
444
445    /**
446     * Replaces all the occurrences of variables with their matching values
447     * from the resolver using the given source array as a template.
448     * The array is not altered by this method.
449     * <p>
450     * Only the specified portion of the array will be processed.
451     * The rest of the array is not processed, and is not returned.
452     *
453     * @param source  the character array to replace in, not altered, null returns null
454     * @param offset  the start offset within the array, must be valid
455     * @param length  the length within the array to be processed, must be valid
456     * @return the result of the replace operation
457     */
458    public String replace(final char[] source, final int offset, final int length) {
459        if (source == null) {
460            return null;
461        }
462        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
463        substitute(buf, 0, length);
464        return buf.toString();
465    }
466
467    //-----------------------------------------------------------------------
468    /**
469     * Replaces all the occurrences of variables with their matching values
470     * from the resolver using the given source buffer as a template.
471     * The buffer is not altered by this method.
472     *
473     * @param source  the buffer to use as a template, not changed, null returns null
474     * @return the result of the replace operation
475     */
476    public String replace(final StringBuffer source) {
477        if (source == null) {
478            return null;
479        }
480        final StrBuilder buf = new StrBuilder(source.length()).append(source);
481        substitute(buf, 0, buf.length());
482        return buf.toString();
483    }
484
485    /**
486     * Replaces all the occurrences of variables with their matching values
487     * from the resolver using the given source buffer as a template.
488     * The buffer is not altered by this method.
489     * <p>
490     * Only the specified portion of the buffer will be processed.
491     * The rest of the buffer is not processed, and is not returned.
492     *
493     * @param source  the buffer to use as a template, not changed, null returns null
494     * @param offset  the start offset within the array, must be valid
495     * @param length  the length within the array to be processed, must be valid
496     * @return the result of the replace operation
497     */
498    public String replace(final StringBuffer source, final int offset, final int length) {
499        if (source == null) {
500            return null;
501        }
502        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
503        substitute(buf, 0, length);
504        return buf.toString();
505    }
506
507    /**
508     * Replaces all the occurrences of variables with their matching values
509     * from the resolver using the given source as a template.
510     * The source is not altered by this method.
511     *
512     * @param source  the buffer to use as a template, not changed, null returns null
513     * @return the result of the replace operation
514     */
515    public String replace(final CharSequence source) {
516        if (source == null) {
517            return null;
518        }
519        return replace(source, 0, source.length());
520    }
521
522    /**
523     * Replaces all the occurrences of variables with their matching values
524     * from the resolver using the given source as a template.
525     * The source is not altered by this method.
526     * <p>
527     * Only the specified portion of the buffer will be processed.
528     * The rest of the buffer is not processed, and is not returned.
529     *
530     * @param source  the buffer to use as a template, not changed, null returns null
531     * @param offset  the start offset within the array, must be valid
532     * @param length  the length within the array to be processed, must be valid
533     * @return the result of the replace operation
534     */
535    public String replace(final CharSequence source, final int offset, final int length) {
536        if (source == null) {
537            return null;
538        }
539        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
540        substitute(buf, 0, length);
541        return buf.toString();
542    }
543
544    //-----------------------------------------------------------------------
545    /**
546     * Replaces all the occurrences of variables with their matching values
547     * from the resolver using the given source builder as a template.
548     * The builder is not altered by this method.
549     *
550     * @param source  the builder to use as a template, not changed, null returns null
551     * @return the result of the replace operation
552     */
553    public String replace(final StrBuilder source) {
554        if (source == null) {
555            return null;
556        }
557        final StrBuilder buf = new StrBuilder(source.length()).append(source);
558        substitute(buf, 0, buf.length());
559        return buf.toString();
560    }
561
562    /**
563     * Replaces all the occurrences of variables with their matching values
564     * from the resolver using the given source builder as a template.
565     * The builder is not altered by this method.
566     * <p>
567     * Only the specified portion of the builder will be processed.
568     * The rest of the builder is not processed, and is not returned.
569     *
570     * @param source  the builder to use as a template, not changed, null returns null
571     * @param offset  the start offset within the array, must be valid
572     * @param length  the length within the array to be processed, must be valid
573     * @return the result of the replace operation
574     */
575    public String replace(final StrBuilder source, final int offset, final int length) {
576        if (source == null) {
577            return null;
578        }
579        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
580        substitute(buf, 0, length);
581        return buf.toString();
582    }
583
584    //-----------------------------------------------------------------------
585    /**
586     * Replaces all the occurrences of variables in the given source object with
587     * their matching values from the resolver. The input source object is
588     * converted to a string using <code>toString</code> and is not altered.
589     *
590     * @param source  the source to replace in, null returns null
591     * @return the result of the replace operation
592     */
593    public String replace(final Object source) {
594        if (source == null) {
595            return null;
596        }
597        final StrBuilder buf = new StrBuilder().append(source);
598        substitute(buf, 0, buf.length());
599        return buf.toString();
600    }
601
602    //-----------------------------------------------------------------------
603    /**
604     * Replaces all the occurrences of variables within the given source buffer
605     * with their matching values from the resolver.
606     * The buffer is updated with the result.
607     *
608     * @param source  the buffer to replace in, updated, null returns zero
609     * @return true if altered
610     */
611    public boolean replaceIn(final StringBuffer source) {
612        if (source == null) {
613            return false;
614        }
615        return replaceIn(source, 0, source.length());
616    }
617
618    /**
619     * Replaces all the occurrences of variables within the given source buffer
620     * with their matching values from the resolver.
621     * The buffer is updated with the result.
622     * <p>
623     * Only the specified portion of the buffer will be processed.
624     * The rest of the buffer is not processed, but it is not deleted.
625     *
626     * @param source  the buffer to replace in, updated, null returns zero
627     * @param offset  the start offset within the array, must be valid
628     * @param length  the length within the buffer to be processed, must be valid
629     * @return true if altered
630     */
631    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
632        if (source == null) {
633            return false;
634        }
635        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
636        if (!substitute(buf, 0, length)) {
637            return false;
638        }
639        source.replace(offset, offset + length, buf.toString());
640        return true;
641    }
642
643  //-----------------------------------------------------------------------
644    /**
645     * Replaces all the occurrences of variables within the given source buffer
646     * with their matching values from the resolver.
647     * The buffer is updated with the result.
648     *
649     * @param source  the buffer to replace in, updated, null returns zero
650     * @return true if altered
651     */
652    public boolean replaceIn(final StringBuilder source) {
653        if (source == null) {
654            return false;
655        }
656        return replaceIn(source, 0, source.length());
657    }
658
659    /**
660     * Replaces all the occurrences of variables within the given source builder
661     * with their matching values from the resolver.
662     * The builder is updated with the result.
663     * <p>
664     * Only the specified portion of the buffer will be processed.
665     * The rest of the buffer is not processed, but it is not deleted.
666     *
667     * @param source  the buffer to replace in, updated, null returns zero
668     * @param offset  the start offset within the array, must be valid
669     * @param length  the length within the buffer to be processed, must be valid
670     * @return true if altered
671     */
672    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
673        if (source == null) {
674            return false;
675        }
676        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
677        if (!substitute(buf, 0, length)) {
678            return false;
679        }
680        source.replace(offset, offset + length, buf.toString());
681        return true;
682    }
683
684    //-----------------------------------------------------------------------
685    /**
686     * Replaces all the occurrences of variables within the given source
687     * builder with their matching values from the resolver.
688     *
689     * @param source  the builder to replace in, updated, null returns zero
690     * @return true if altered
691     */
692    public boolean replaceIn(final StrBuilder source) {
693        if (source == null) {
694            return false;
695        }
696        return substitute(source, 0, source.length());
697    }
698
699    /**
700     * Replaces all the occurrences of variables within the given source
701     * builder with their matching values from the resolver.
702     * <p>
703     * Only the specified portion of the builder will be processed.
704     * The rest of the builder is not processed, but it is not deleted.
705     *
706     * @param source  the builder to replace in, null returns zero
707     * @param offset  the start offset within the array, must be valid
708     * @param length  the length within the builder to be processed, must be valid
709     * @return true if altered
710     */
711    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
712        if (source == null) {
713            return false;
714        }
715        return substitute(source, offset, length);
716    }
717
718    //-----------------------------------------------------------------------
719    /**
720     * Internal method that substitutes the variables.
721     * <p>
722     * Most users of this class do not need to call this method. This method will
723     * be called automatically by another (public) method.
724     * <p>
725     * Writers of subclasses can override this method if they need access to
726     * the substitution process at the start or end.
727     *
728     * @param buf  the string builder to substitute into, not null
729     * @param offset  the start offset within the builder, must be valid
730     * @param length  the length within the builder to be processed, must be valid
731     * @return true if altered
732     */
733    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
734        return substitute(buf, offset, length, null) > 0;
735    }
736
737    /**
738     * Recursive handler for multiple levels of interpolation. This is the main
739     * interpolation method, which resolves the values of all variable references
740     * contained in the passed in text.
741     *
742     * @param buf  the string builder to substitute into, not null
743     * @param offset  the start offset within the builder, must be valid
744     * @param length  the length within the builder to be processed, must be valid
745     * @param priorVariables  the stack keeping track of the replaced variables, may be null
746     * @return the length change that occurs, unless priorVariables is null when the int
747     *  represents a boolean flag as to whether any change occurred.
748     */
749    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
750        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
751        final StrMatcher suffMatcher = getVariableSuffixMatcher();
752        final char escape = getEscapeChar();
753        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
754        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
755
756        final boolean top = priorVariables == null;
757        boolean altered = false;
758        int lengthChange = 0;
759        char[] chars = buf.buffer;
760        int bufEnd = offset + length;
761        int pos = offset;
762        while (pos < bufEnd) {
763            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
764                    bufEnd);
765            if (startMatchLen == 0) {
766                pos++;
767            } else {
768                // found variable start marker
769                if (pos > offset && chars[pos - 1] == escape) {
770                    // escaped
771                    if (preserveEscapes) {
772                        pos++;
773                        continue;
774                    }
775                    buf.deleteCharAt(pos - 1);
776                    chars = buf.buffer; // in case buffer was altered
777                    lengthChange--;
778                    altered = true;
779                    bufEnd--;
780                } else {
781                    // find suffix
782                    final int startPos = pos;
783                    pos += startMatchLen;
784                    int endMatchLen = 0;
785                    int nestedVarCount = 0;
786                    while (pos < bufEnd) {
787                        if (substitutionInVariablesEnabled
788                                && pfxMatcher.isMatch(chars,
789                                        pos, offset, bufEnd) != 0) {
790                            // found a nested variable start
791                            endMatchLen = pfxMatcher.isMatch(chars,
792                                    pos, offset, bufEnd);
793                            nestedVarCount++;
794                            pos += endMatchLen;
795                            continue;
796                        }
797
798                        endMatchLen = suffMatcher.isMatch(chars, pos, offset,
799                                bufEnd);
800                        if (endMatchLen == 0) {
801                            pos++;
802                        } else {
803                            // found variable end marker
804                            if (nestedVarCount == 0) {
805                                String varNameExpr = new String(chars, startPos
806                                        + startMatchLen, pos - startPos
807                                        - startMatchLen);
808                                if (substitutionInVariablesEnabled) {
809                                    final StrBuilder bufName = new StrBuilder(varNameExpr);
810                                    substitute(bufName, 0, bufName.length());
811                                    varNameExpr = bufName.toString();
812                                }
813                                pos += endMatchLen;
814                                final int endPos = pos;
815
816                                String varName = varNameExpr;
817                                String varDefaultValue = null;
818
819                                if (valueDelimMatcher != null) {
820                                    final char [] varNameExprChars = varNameExpr.toCharArray();
821                                    int valueDelimiterMatchLen = 0;
822                                    for (int i = 0; i < varNameExprChars.length; i++) {
823                                        // if there's any nested variable when nested variable substitution disabled,
824                                        // then stop resolving name and default value.
825                                        if (!substitutionInVariablesEnabled
826                                                && pfxMatcher.isMatch(varNameExprChars,
827                                                                        i,
828                                                                        i,
829                                                                        varNameExprChars.length) != 0) {
830                                            break;
831                                        }
832                                        if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
833                                            valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
834                                            varName = varNameExpr.substring(0, i);
835                                            varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
836                                            break;
837                                        }
838                                    }
839                                }
840
841                                // on the first call initialize priorVariables
842                                if (priorVariables == null) {
843                                    priorVariables = new ArrayList<>();
844                                    priorVariables.add(new String(chars,
845                                            offset, length));
846                                }
847
848                                // handle cyclic substitution
849                                checkCyclicSubstitution(varName, priorVariables);
850                                priorVariables.add(varName);
851
852                                // resolve the variable
853                                String varValue = resolveVariable(varName, buf,
854                                        startPos, endPos);
855                                if (varValue == null) {
856                                    varValue = varDefaultValue;
857                                }
858                                if (varValue != null) {
859                                    // recursive replace
860                                    final int varLen = varValue.length();
861                                    buf.replace(startPos, endPos, varValue);
862                                    altered = true;
863                                    int change = substitute(buf, startPos,
864                                            varLen, priorVariables);
865                                    change = change
866                                            + varLen - (endPos - startPos);
867                                    pos += change;
868                                    bufEnd += change;
869                                    lengthChange += change;
870                                    chars = buf.buffer; // in case buffer was
871                                                        // altered
872                                }
873
874                                // remove variable from the cyclic stack
875                                priorVariables
876                                        .remove(priorVariables.size() - 1);
877                                break;
878                            }
879                            nestedVarCount--;
880                            pos += endMatchLen;
881                        }
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)) {
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,
928                                     final StrBuilder buf,
929                                     final int startPos,
930                                     final int endPos) {
931        final StrLookup<?> resolver = getVariableResolver();
932        if (resolver == null) {
933            return null;
934        }
935        return resolver.lookup(variableName);
936    }
937
938    // Escape
939    //-----------------------------------------------------------------------
940    /**
941     * Returns the escape character.
942     *
943     * @return the character used for escaping variable references
944     */
945    public char getEscapeChar() {
946        return this.escapeChar;
947    }
948
949    /**
950     * Sets the escape character.
951     * If this character is placed before a variable reference in the source
952     * text, this variable will be ignored.
953     *
954     * @param escapeCharacter  the escape character (0 for disabling escaping)
955     */
956    public void setEscapeChar(final char escapeCharacter) {
957        this.escapeChar = escapeCharacter;
958    }
959
960    // Prefix
961    //-----------------------------------------------------------------------
962    /**
963     * Gets the variable prefix matcher currently in use.
964     * <p>
965     * The variable prefix is the character or characters that identify the
966     * start of a variable. This prefix is expressed in terms of a matcher
967     * allowing advanced prefix matches.
968     *
969     * @return the prefix matcher in use
970     */
971    public StrMatcher getVariablePrefixMatcher() {
972        return prefixMatcher;
973    }
974
975    /**
976     * Sets the variable prefix matcher currently in use.
977     * <p>
978     * The variable prefix is the character or characters that identify the
979     * start of a variable. This prefix is expressed in terms of a matcher
980     * allowing advanced prefix matches.
981     *
982     * @param prefixMatcher  the prefix matcher to use, null ignored
983     * @return this, to enable chaining
984     * @throws IllegalArgumentException if the prefix matcher is null
985     */
986    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
987        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
988        this.prefixMatcher = prefixMatcher;
989        return this;
990    }
991
992    /**
993     * Sets the variable prefix to use.
994     * <p>
995     * The variable prefix is the character or characters that identify the
996     * start of a variable. This method allows a single character prefix to
997     * be easily set.
998     *
999     * @param prefix  the prefix character to use
1000     * @return this, to enable chaining
1001     */
1002    public StrSubstitutor setVariablePrefix(final char prefix) {
1003        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
1004    }
1005
1006    /**
1007     * Sets the variable prefix to use.
1008     * <p>
1009     * The variable prefix is the character or characters that identify the
1010     * start of a variable. This method allows a string prefix to be easily set.
1011     *
1012     * @param prefix  the prefix for variables, not null
1013     * @return this, to enable chaining
1014     * @throws IllegalArgumentException if the prefix is null
1015     */
1016    public StrSubstitutor setVariablePrefix(final String prefix) {
1017        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
1018        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
1019    }
1020
1021    // Suffix
1022    //-----------------------------------------------------------------------
1023    /**
1024     * Gets the variable suffix matcher currently in use.
1025     * <p>
1026     * The variable suffix is the character or characters that identify the
1027     * end of a variable. This suffix is expressed in terms of a matcher
1028     * allowing advanced suffix matches.
1029     *
1030     * @return the suffix matcher in use
1031     */
1032    public StrMatcher getVariableSuffixMatcher() {
1033        return suffixMatcher;
1034    }
1035
1036    /**
1037     * Sets the variable suffix matcher currently in use.
1038     * <p>
1039     * The variable suffix is the character or characters that identify the
1040     * end of a variable. This suffix is expressed in terms of a matcher
1041     * allowing advanced suffix matches.
1042     *
1043     * @param suffixMatcher  the suffix matcher to use, null ignored
1044     * @return this, to enable chaining
1045     * @throws IllegalArgumentException if the suffix matcher is null
1046     */
1047    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1048        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
1049        this.suffixMatcher = suffixMatcher;
1050        return this;
1051    }
1052
1053    /**
1054     * Sets the variable suffix to use.
1055     * <p>
1056     * The variable suffix is the character or characters that identify the
1057     * end of a variable. This method allows a single character suffix to
1058     * be easily set.
1059     *
1060     * @param suffix  the suffix character to use
1061     * @return this, to enable chaining
1062     */
1063    public StrSubstitutor setVariableSuffix(final char suffix) {
1064        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1065    }
1066
1067    /**
1068     * Sets the variable suffix to use.
1069     * <p>
1070     * The variable suffix is the character or characters that identify the
1071     * end of a variable. This method allows a string suffix to be easily set.
1072     *
1073     * @param suffix  the suffix for variables, not null
1074     * @return this, to enable chaining
1075     * @throws IllegalArgumentException if the suffix is null
1076     */
1077    public StrSubstitutor setVariableSuffix(final String suffix) {
1078        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
1079        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1080    }
1081
1082    // Variable Default Value Delimiter
1083    //-----------------------------------------------------------------------
1084    /**
1085     * Gets the variable default value delimiter matcher currently in use.
1086     * <p>
1087     * The variable default value delimiter is the character or characters that delimite the
1088     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1089     * allowing advanced variable default value delimiter matches.
1090     * <p>
1091     * If it returns null, then the variable default value resolution is disabled.
1092     *
1093     * @return the variable default value delimiter matcher in use, may be null
1094     */
1095    public StrMatcher getValueDelimiterMatcher() {
1096        return valueDelimiterMatcher;
1097    }
1098
1099    /**
1100     * Sets the variable default value delimiter matcher to use.
1101     * <p>
1102     * The variable default value delimiter is the character or characters that delimite the
1103     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1104     * allowing advanced variable default value delimiter matches.
1105     * <p>
1106     * If the <code>valueDelimiterMatcher</code> is null, then the variable default value resolution
1107     * becomes disabled.
1108     *
1109     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
1110     * @return this, to enable chaining
1111     */
1112    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
1113        this.valueDelimiterMatcher = valueDelimiterMatcher;
1114        return this;
1115    }
1116
1117    /**
1118     * Sets the variable default value delimiter to use.
1119     * <p>
1120     * The variable default value delimiter is the character or characters that delimite the
1121     * variable name and the variable default value. This method allows a single character
1122     * variable default value delimiter to be easily set.
1123     *
1124     * @param valueDelimiter  the variable default value delimiter character to use
1125     * @return this, to enable chaining
1126     */
1127    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
1128        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
1129    }
1130
1131    /**
1132     * Sets the variable default value delimiter to use.
1133     * <p>
1134     * The variable default value delimiter is the character or characters that delimite the
1135     * variable name and the variable default value. This method allows a string
1136     * variable default value delimiter to be easily set.
1137     * <p>
1138     * If the <code>valueDelimiter</code> is null or empty string, then the variable default
1139     * value resolution becomes disabled.
1140     *
1141     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
1142     * @return this, to enable chaining
1143     */
1144    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
1145        if (valueDelimiter == null || valueDelimiter.length() == 0) {
1146            setValueDelimiterMatcher(null);
1147            return this;
1148        }
1149        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
1150    }
1151
1152    // Resolver
1153    //-----------------------------------------------------------------------
1154    /**
1155     * Gets the VariableResolver that is used to lookup variables.
1156     *
1157     * @return the VariableResolver
1158     */
1159    public StrLookup<?> getVariableResolver() {
1160        return this.variableResolver;
1161    }
1162
1163    /**
1164     * Sets the VariableResolver that is used to lookup variables.
1165     *
1166     * @param variableResolver  the VariableResolver
1167     */
1168    public void setVariableResolver(final StrLookup<?> variableResolver) {
1169        this.variableResolver = variableResolver;
1170    }
1171
1172    // Substitution support in variable names
1173    //-----------------------------------------------------------------------
1174    /**
1175     * Returns a flag whether substitution is done in variable names.
1176     *
1177     * @return the substitution in variable names flag
1178     */
1179    public boolean isEnableSubstitutionInVariables() {
1180        return enableSubstitutionInVariables;
1181    }
1182
1183    /**
1184     * Sets a flag whether substitution is done in variable names. If set to
1185     * <b>true</b>, the names of variables can contain other variables which are
1186     * processed first before the original variable is evaluated, e.g.
1187     * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
1188     *
1189     * @param enableSubstitutionInVariables the new value of the flag
1190     */
1191    public void setEnableSubstitutionInVariables(
1192            final boolean enableSubstitutionInVariables) {
1193        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
1194    }
1195
1196    /**
1197     * Returns the flag controlling whether escapes are preserved during
1198     * substitution.
1199     *
1200     * @return the preserve escape flag
1201     */
1202    public boolean isPreserveEscapes() {
1203        return preserveEscapes;
1204    }
1205
1206    /**
1207     * Sets a flag controlling whether escapes are preserved during
1208     * substitution.  If set to <b>true</b>, the escape character is retained
1209     * during substitution (e.g. <code>$${this-is-escaped}</code> remains
1210     * <code>$${this-is-escaped}</code>).  If set to <b>false</b>, the escape
1211     * character is removed during substitution (e.g.
1212     * <code>$${this-is-escaped}</code> becomes
1213     * <code>${this-is-escaped}</code>).  The default value is <b>false</b>
1214     *
1215     * @param preserveEscapes true if escapes are to be preserved
1216     */
1217    public void setPreserveEscapes(final boolean preserveEscapes) {
1218        this.preserveEscapes = preserveEscapes;
1219    }
1220}