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