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 variable name and the variable default value. This delimiter is
423 * expressed in terms of a matcher allowing advanced variable default value delimiter matches.
424 * </p>
425 * <p>
426 * If it returns null, then the variable default value resolution is disabled.
427 * </p>
428 *
429 * @return The variable default value delimiter matcher in use, may be null.
430 */
431 public StrMatcher getValueDelimiterMatcher() {
432 return valueDelimiterMatcher;
433 }
434
435 /**
436 * Gets the variable prefix matcher currently in use.
437 * <p>
438 * The variable prefix is the character or characters that identify the start of a variable. This prefix is expressed in terms of a matcher allowing
439 * advanced prefix matches.
440 * </p>
441 *
442 * @return The prefix matcher in use.
443 */
444 public StrMatcher getVariablePrefixMatcher() {
445 return prefixMatcher;
446 }
447
448 /**
449 * Gets the VariableResolver that is used to lookup variables.
450 *
451 * @return The VariableResolver.
452 */
453 public StrLookup<?> getVariableResolver() {
454 return this.variableResolver;
455 }
456
457 /**
458 * Gets the variable suffix matcher currently in use.
459 * <p>
460 * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed in terms of a matcher allowing advanced
461 * suffix matches.
462 * </p>
463 *
464 * @return The suffix matcher in use.
465 */
466 public StrMatcher getVariableSuffixMatcher() {
467 return suffixMatcher;
468 }
469
470 /**
471 * Returns a flag whether substitution is disabled in variable values.If set to
472 * <strong>true</strong>, the values of variables can contain other variables will not be
473 * processed and substituted original variable is evaluated, e.g.
474 * <pre>
475 * Map<String, String> valuesMap = new HashMap<>();
476 * valuesMap.put("name", "Douglas ${surname}");
477 * valuesMap.put("surname", "Crockford");
478 * String templateString = "Hi ${name}";
479 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
480 * String resolvedString = sub.replace(templateString);
481 * </pre>
482 * <p>
483 * yielding:
484 * </p>
485 * <pre>
486 * Hi Douglas ${surname}
487 * </pre>
488 *
489 * @return The substitution in variable values flag.
490 * @since 1.2
491 */
492 public boolean isDisableSubstitutionInValues() {
493 return disableSubstitutionInValues;
494 }
495
496 /**
497 * Returns a flag whether substitution is done in variable names.
498 *
499 * @return The substitution in variable names flag.
500 */
501 public boolean isEnableSubstitutionInVariables() {
502 return enableSubstitutionInVariables;
503 }
504
505 /**
506 * Returns the flag controlling whether escapes are preserved during substitution.
507 *
508 * @return The preserve escape flag.
509 */
510 public boolean isPreserveEscapes() {
511 return preserveEscapes;
512 }
513
514 /**
515 * Replaces all the occurrences of variables with their matching values from the resolver using the given source array as a template. The array is not
516 * altered by this method.
517 *
518 * @param source the character array to replace in, not altered, null returns null.
519 * @return The result of the replace operation.
520 */
521 public String replace(final char[] source) {
522 if (source == null) {
523 return null;
524 }
525 final StrBuilder buf = new StrBuilder(source.length).append(source);
526 substitute(buf, 0, source.length);
527 return buf.toString();
528 }
529
530 /**
531 * Replaces all the occurrences of variables with their matching values from the resolver using the given source array as a template. The array is not
532 * altered by this method.
533 * <p>
534 * Only the specified portion of the array will be processed. The rest of the array is not processed, and is not returned.
535 * </p>
536 *
537 * @param source the character array to replace in, not altered, null returns null.
538 * @param offset the start offset within the array, must be valid.
539 * @param length the length within the array to be processed, must be valid.
540 * @return The result of the replace operation.
541 */
542 public String replace(final char[] source, final int offset, final int length) {
543 if (source == null) {
544 return null;
545 }
546 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
547 substitute(buf, 0, length);
548 return buf.toString();
549 }
550
551 /**
552 * Replaces all the occurrences of variables with their matching values from the resolver using the given source as a template. The source is not altered by
553 * this method.
554 *
555 * @param source the buffer to use as a template, not changed, null returns null.
556 * @return The result of the replace operation.
557 */
558 public String replace(final CharSequence source) {
559 if (source == null) {
560 return null;
561 }
562 return replace(source, 0, source.length());
563 }
564
565 /**
566 * Replaces all the occurrences of variables with their matching values from the resolver using the given source as a template. The source is not altered by
567 * this method.
568 * <p>
569 * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not returned.
570 * </p>
571 *
572 * @param source the buffer to use as a template, not changed, null returns null.
573 * @param offset the start offset within the array, must be valid.
574 * @param length the length within the array to be processed, must be valid.
575 * @return The result of the replace operation.
576 */
577 public String replace(final CharSequence source, final int offset, final int length) {
578 if (source == null) {
579 return null;
580 }
581 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
582 substitute(buf, 0, length);
583 return buf.toString();
584 }
585
586 /**
587 * Replaces all the occurrences of variables in the given source object with their matching values from the resolver. The input source object is converted
588 * to a string using {@code toString} 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 * Replaces all the occurrences of variables with their matching values from the resolver using the given source builder as a template. The builder is not
604 * altered by this method.
605 *
606 * @param source the builder to use as a template, not changed, null returns null.
607 * @return The result of the replace operation.
608 */
609 public String replace(final StrBuilder source) {
610 if (source == null) {
611 return null;
612 }
613 final StrBuilder buf = new StrBuilder(source.length()).append(source);
614 substitute(buf, 0, buf.length());
615 return buf.toString();
616 }
617
618 /**
619 * Replaces all the occurrences of variables with their matching values from the resolver using the given source builder as a template. The builder is not
620 * altered by this method.
621 * <p>
622 * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not returned.
623 * </p>
624 *
625 * @param source the builder to use as a template, not changed, null returns null.
626 * @param offset the start offset within the array, must be valid.
627 * @param length the length within the array to be processed, must be valid.
628 * @return The result of the replace operation.
629 */
630 public String replace(final StrBuilder source, final int offset, final int length) {
631 if (source == null) {
632 return null;
633 }
634 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
635 substitute(buf, 0, length);
636 return buf.toString();
637 }
638
639 /**
640 * Replaces all the occurrences of variables with their matching values from the resolver using the given source string as a template.
641 *
642 * @param source the string to replace in, null returns null.
643 * @return The result of the replace operation.
644 */
645 public String replace(final String source) {
646 if (source == null) {
647 return null;
648 }
649 final StrBuilder buf = new StrBuilder(source);
650 if (!substitute(buf, 0, source.length())) {
651 return source;
652 }
653 return buf.toString();
654 }
655
656 /**
657 * Replaces all the occurrences of variables with their matching values from the resolver using the given source string as a template.
658 * <p>
659 * Only the specified portion of the string will be processed. The rest of the string is not processed, and is not returned.
660 * </p>
661 *
662 * @param source the string to replace in, null returns null.
663 * @param offset the start offset within the array, must be valid.
664 * @param length the length within the array to be processed, must be valid.
665 * @return The result of the replace operation.
666 */
667 public String replace(final String source, final int offset, final int length) {
668 if (source == null) {
669 return null;
670 }
671 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
672 if (!substitute(buf, 0, length)) {
673 return source.substring(offset, offset + length);
674 }
675 return buf.toString();
676 }
677
678 /**
679 * Replaces all the occurrences of variables with their matching values from the resolver using the given source buffer as a template. The buffer is not
680 * altered by this method.
681 *
682 * @param source the buffer to use as a template, not changed, null returns null.
683 * @return The result of the replace operation.
684 */
685 public String replace(final StringBuffer source) {
686 if (source == null) {
687 return null;
688 }
689 final StrBuilder buf = new StrBuilder(source.length()).append(source);
690 substitute(buf, 0, buf.length());
691 return buf.toString();
692 }
693
694 /**
695 * Replaces all the occurrences of variables with their matching values from the resolver using the given source buffer as a template. The buffer is not
696 * altered by this method.
697 * <p>
698 * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not returned.
699 * </p>
700 *
701 * @param source the buffer to use as a template, not changed, null returns null.
702 * @param offset the start offset within the array, must be valid.
703 * @param length the length within the array to be processed, must be valid.
704 * @return The result of the replace operation.
705 */
706 public String replace(final StringBuffer source, final int offset, final int length) {
707 if (source == null) {
708 return null;
709 }
710 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
711 substitute(buf, 0, length);
712 return buf.toString();
713 }
714
715 /**
716 * Replaces all the occurrences of variables within the given source builder with their matching values from the resolver.
717 *
718 * @param source the builder to replace in, updated, null returns zero.
719 * @return true if altered.
720 */
721 public boolean replaceIn(final StrBuilder source) {
722 if (source == null) {
723 return false;
724 }
725 return substitute(source, 0, source.length());
726 }
727
728 /**
729 * Replaces all the occurrences of variables within the given source builder with their matching values from the resolver.
730 * <p>
731 * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is not deleted.
732 * </p>
733 *
734 * @param source the builder to replace in, null returns zero.
735 * @param offset the start offset within the array, must be valid.
736 * @param length the length within the builder to be processed, must be valid.
737 * @return true if altered.
738 */
739 public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
740 if (source == null) {
741 return false;
742 }
743 return substitute(source, offset, length);
744 }
745
746 /**
747 * Replaces all the occurrences of variables within the given source buffer with their matching values from the resolver. The buffer is updated with the
748 * result.
749 *
750 * @param source the buffer to replace in, updated, null returns zero.
751 * @return true if altered.
752 */
753 public boolean replaceIn(final StringBuffer source) {
754 if (source == null) {
755 return false;
756 }
757 return replaceIn(source, 0, source.length());
758 }
759
760 /**
761 * Replaces all the occurrences of variables within the given source buffer with their matching values from the resolver. The buffer is updated with the
762 * result.
763 * <p>
764 * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is not deleted.
765 * </p>
766 *
767 * @param source the buffer to replace in, updated, null returns zero.
768 * @param offset the start offset within the array, must be valid.
769 * @param length the length within the buffer to be processed, must be valid.
770 * @return true if altered.
771 */
772 public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
773 if (source == null) {
774 return false;
775 }
776 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
777 if (!substitute(buf, 0, length)) {
778 return false;
779 }
780 source.replace(offset, offset + length, buf.toString());
781 return true;
782 }
783
784 /**
785 * Replaces all the occurrences of variables within the given source buffer with their matching values from the resolver. The buffer is updated with the
786 * result.
787 *
788 * @param source the buffer to replace in, updated, null returns zero.
789 * @return true if altered.
790 */
791 public boolean replaceIn(final StringBuilder source) {
792 if (source == null) {
793 return false;
794 }
795 return replaceIn(source, 0, source.length());
796 }
797
798 /**
799 * Replaces all the occurrences of variables within the given source builder with their matching values from the resolver. The builder is updated with the
800 * result.
801 * <p>
802 * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is not deleted.
803 * </p>
804 *
805 * @param source the buffer to replace in, updated, null returns zero.
806 * @param offset the start offset within the array, must be valid.
807 * @param length the length within the buffer to be processed, must be valid.
808 * @return true if altered.
809 */
810 public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
811 if (source == null) {
812 return false;
813 }
814 final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
815 if (!substitute(buf, 0, length)) {
816 return false;
817 }
818 source.replace(offset, offset + length, buf.toString());
819 return true;
820 }
821
822 /**
823 * Internal method that resolves the value of a variable.
824 * <p>
825 * Most users of this class do not need to call this method. This method is called automatically by the substitution process.
826 * </p>
827 * <p>
828 * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is passed the variable's name and must
829 * return the corresponding value. This implementation uses the {@link #getVariableResolver()} with the variable's name as the key.
830 * </p>
831 *
832 * @param variableName the name of the variable, not null.
833 * @param buf the buffer where the substitution is occurring, not null.
834 * @param startPos the start position of the variable including the prefix, valid.
835 * @param endPos the end position of the variable including the suffix, valid.
836 * @return The variable's value or <strong>null</strong> if the variable is unknown.
837 */
838 protected String resolveVariable(final String variableName,
839 final StrBuilder buf,
840 final int startPos,
841 final int endPos) {
842 final StrLookup<?> resolver = getVariableResolver();
843 if (resolver == null) {
844 return null;
845 }
846 return resolver.apply(variableName);
847 }
848
849 /**
850 * Sets a flag whether substitution is done in variable values (recursive).
851 *
852 * @param disableSubstitutionInValues true if substitution in variable value are disabled.
853 * @since 1.2
854 */
855 public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
856 this.disableSubstitutionInValues = disableSubstitutionInValues;
857 }
858
859 /**
860 * Sets a flag whether substitution is done in variable names. If set to <strong>true</strong>, the names of variables can contain other variables which are
861 * processed first before the original variable is evaluated, e.g. {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
862 *
863 * @param enableSubstitutionInVariables the new value of the flag.
864 */
865 public void setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
866 this.enableSubstitutionInVariables = enableSubstitutionInVariables;
867 }
868
869 /**
870 * Sets the escape character. If this character is placed before a variable reference in the source text, this variable will be ignored.
871 *
872 * @param escapeCharacter the escape character (0 for disabling escaping)
873 */
874 public void setEscapeChar(final char escapeCharacter) {
875 this.escapeChar = escapeCharacter;
876 }
877
878 /**
879 * Sets a flag controlling whether escapes are preserved during substitution. If set to <strong>true</strong>, the escape character is retained during
880 * substitution (e.g. {@code $${this-is-escaped}} remains {@code $${this-is-escaped}}). If set to <strong>false</strong>, the escape character is removed
881 * during substitution (e.g. {@code $${this-is-escaped}} becomes {@code ${this-is-escaped}}). The default value is <strong>false</strong>
882 *
883 * @param preserveEscapes true if escapes are to be preserved
884 */
885 public void setPreserveEscapes(final boolean preserveEscapes) {
886 this.preserveEscapes = preserveEscapes;
887 }
888
889 /**
890 * Sets the variable default value delimiter to use.
891 * <p>
892 * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This method allows a
893 * single character variable default value delimiter to be easily set.
894 * </p>
895 *
896 * @param valueDelimiter the variable default value delimiter character to use.
897 * @return {@code this} instance.
898 */
899 public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
900 return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
901 }
902
903 /**
904 * Sets the variable default value delimiter to use.
905 * <p>
906 * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This method allows a
907 * string variable default value delimiter to be easily set.
908 * </p>
909 * <p>
910 * If the {@code valueDelimiter} is null or empty string, then the variable default value resolution becomes disabled.
911 * </p>
912 *
913 * @param valueDelimiter the variable default value delimiter string to use, may be null or empty
914 * @return {@code this} instance.
915 */
916 public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
917 if (valueDelimiter == null || valueDelimiter.isEmpty()) {
918 setValueDelimiterMatcher(null);
919 return this;
920 }
921 return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
922 }
923
924 /**
925 * Sets the variable default value delimiter matcher to use.
926 * <p>
927 * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This delimiter is
928 * expressed in terms of a matcher allowing advanced variable default value delimiter matches.
929 * </p>
930 * <p>
931 * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution becomes disabled.
932 * </p>
933 *
934 * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null
935 * @return {@code this} instance.
936 */
937 public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
938 this.valueDelimiterMatcher = valueDelimiterMatcher;
939 return this;
940 }
941
942 /**
943 * Sets the variable prefix to use.
944 * <p>
945 * The variable prefix is the character or characters that identify the start of a variable. This method allows a single character prefix to be easily set.
946 * </p>
947 *
948 * @param prefix the prefix character to use.
949 * @return {@code this} instance.
950 */
951 public StrSubstitutor setVariablePrefix(final char prefix) {
952 return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
953 }
954
955 /**
956 * Sets the variable prefix to use.
957 * <p>
958 * The variable prefix is the character or characters that identify the start of a variable. This method allows a string prefix to be easily set.
959 * </p>
960 *
961 * @param prefix the prefix for variables, not null.
962 * @return {@code this} instance.
963 * @throws IllegalArgumentException if the prefix is null.
964 */
965 public StrSubstitutor setVariablePrefix(final String prefix) {
966 Validate.isTrue(prefix != null, "Variable prefix must not be null!");
967 return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
968 }
969
970 /**
971 * Sets the variable prefix matcher currently in use.
972 * <p>
973 * The variable prefix is the character or characters that identify the start of a variable. This prefix is expressed in terms of a matcher allowing
974 * advanced prefix matches.
975 * </p>
976 *
977 * @param prefixMatcher the prefix matcher to use, null ignored.
978 * @return {@code this} instance.
979 * @throws IllegalArgumentException if the prefix matcher is null.
980 */
981 public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
982 Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
983 this.prefixMatcher = prefixMatcher;
984 return this;
985 }
986
987 /**
988 * Sets the VariableResolver that is used to lookup variables.
989 *
990 * @param variableResolver the VariableResolver.
991 */
992 public void setVariableResolver(final StrLookup<?> variableResolver) {
993 this.variableResolver = variableResolver;
994 }
995
996 /**
997 * Sets the variable suffix to use.
998 * <p>
999 * The variable suffix is the character or characters that identify the end of a variable. This method allows a single character suffix to be easily set.
1000 * </p>
1001 *
1002 * @param suffix the suffix character to use.
1003 * @return {@code this} instance.
1004 */
1005 public StrSubstitutor setVariableSuffix(final char suffix) {
1006 return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1007 }
1008
1009 /**
1010 * Sets the variable suffix to use.
1011 * <p>
1012 * The variable suffix is the character or characters that identify the end of a variable. This method allows a string suffix to be easily set.
1013 * </p>
1014 *
1015 * @param suffix the suffix for variables, not null.
1016 * @return {@code this} instance.
1017 * @throws IllegalArgumentException if the suffix is null.
1018 */
1019 public StrSubstitutor setVariableSuffix(final String suffix) {
1020 Validate.isTrue(suffix != null, "Variable suffix must not be null!");
1021 return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1022 }
1023
1024 /**
1025 * Sets the variable suffix matcher currently in use.
1026 * <p>
1027 * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed in terms of a matcher allowing advanced
1028 * suffix matches.
1029 * </p>
1030 *
1031 * @param suffixMatcher the suffix matcher to use, null ignored.
1032 * @return {@code this} instance.
1033 * @throws IllegalArgumentException if the suffix matcher is null.
1034 */
1035 public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1036 Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
1037 this.suffixMatcher = suffixMatcher;
1038 return this;
1039 }
1040
1041 /**
1042 * Internal method that substitutes the variables.
1043 * <p>
1044 * Most users of this class do not need to call this method. This method will be called automatically by another (public) method.
1045 * </p>
1046 * <p>
1047 * Writers of subclasses can override this method if they need access to the substitution process at the start or end.
1048 * </p>
1049 *
1050 * @param buf the string builder to substitute into, not null.
1051 * @param offset the start offset within the builder, must be valid.
1052 * @param length the length within the builder to be processed, must be valid.
1053 * @return true if altered.
1054 */
1055 protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
1056 return substitute(buf, offset, length, null) > 0;
1057 }
1058
1059 /**
1060 * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the values of all variable references
1061 * contained in the passed in text.
1062 *
1063 * @param buf the string builder to substitute into, not null.
1064 * @param offset the start offset within the builder, must be valid.
1065 * @param length the length within the builder to be processed, must be valid.
1066 * @param priorVariables the stack keeping track of the replaced variables, may be null.
1067 * @return The length change that occurs, unless priorVariables is null when the int represents a boolean flag as to whether any change occurred.
1068 */
1069 private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
1070 final StrMatcher pfxMatcher = getVariablePrefixMatcher();
1071 final StrMatcher suffMatcher = getVariableSuffixMatcher();
1072 final char escape = getEscapeChar();
1073 final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
1074 final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
1075 final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
1076
1077 final boolean top = priorVariables == null;
1078 boolean altered = false;
1079 int lengthChange = 0;
1080 char[] chars = buf.buffer;
1081 int bufEnd = offset + length;
1082 int pos = offset;
1083 while (pos < bufEnd) {
1084 final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
1085 bufEnd);
1086 if (startMatchLen == 0) {
1087 pos++;
1088 } else // found variable start marker
1089 if (pos > offset && chars[pos - 1] == escape) {
1090 // escaped
1091 if (preserveEscapes) {
1092 pos++;
1093 continue;
1094 }
1095 buf.deleteCharAt(pos - 1);
1096 chars = buf.buffer; // in case buffer was altered
1097 lengthChange--;
1098 altered = true;
1099 bufEnd--;
1100 } else {
1101 // find suffix
1102 final int startPos = pos;
1103 pos += startMatchLen;
1104 int endMatchLen = 0;
1105 int nestedVarCount = 0;
1106 while (pos < bufEnd) {
1107 if (substitutionInVariablesEnabled
1108 && pfxMatcher.isMatch(chars,
1109 pos, offset, bufEnd) != 0) {
1110 // found a nested variable start
1111 endMatchLen = pfxMatcher.isMatch(chars,
1112 pos, offset, bufEnd);
1113 nestedVarCount++;
1114 pos += endMatchLen;
1115 continue;
1116 }
1117
1118 endMatchLen = suffMatcher.isMatch(chars, pos, offset,
1119 bufEnd);
1120 if (endMatchLen == 0) {
1121 pos++;
1122 } else {
1123 // found variable end marker
1124 if (nestedVarCount == 0) {
1125 String varNameExpr = new String(chars, startPos
1126 + startMatchLen, pos - startPos
1127 - startMatchLen);
1128 if (substitutionInVariablesEnabled) {
1129 final StrBuilder bufName = new StrBuilder(varNameExpr);
1130 substitute(bufName, 0, bufName.length());
1131 varNameExpr = bufName.toString();
1132 }
1133 pos += endMatchLen;
1134 final int endPos = pos;
1135
1136 String varName = varNameExpr;
1137 String varDefaultValue = null;
1138
1139 if (valueDelimMatcher != null) {
1140 final char[] varNameExprChars = varNameExpr.toCharArray();
1141 int valueDelimiterMatchLen = 0;
1142 for (int i = 0; i < varNameExprChars.length; i++) {
1143 // if there's any nested variable when nested variable substitution disabled,
1144 // then stop resolving name and default value.
1145 if (!substitutionInVariablesEnabled
1146 && pfxMatcher.isMatch(varNameExprChars,
1147 i,
1148 i,
1149 varNameExprChars.length) != 0) {
1150 break;
1151 }
1152 if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
1153 valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
1154 varName = varNameExpr.substring(0, i);
1155 varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
1156 break;
1157 }
1158 }
1159 }
1160
1161 // on the first call initialize priorVariables
1162 if (priorVariables == null) {
1163 priorVariables = new ArrayList<>();
1164 priorVariables.add(new String(chars,
1165 offset, length));
1166 }
1167
1168 // handle cyclic substitution
1169 checkCyclicSubstitution(varName, priorVariables);
1170 priorVariables.add(varName);
1171
1172 // resolve the variable
1173 String varValue = resolveVariable(varName, buf,
1174 startPos, endPos);
1175 if (varValue == null) {
1176 varValue = varDefaultValue;
1177 }
1178 if (varValue != null) {
1179 final int varLen = varValue.length();
1180 buf.replace(startPos, endPos, varValue);
1181 altered = true;
1182 int change = 0;
1183 if (!substitutionInValuesDisabled) { // recursive replace
1184 change = substitute(buf, startPos,
1185 varLen, priorVariables);
1186 }
1187 change = change
1188 + varLen - (endPos - startPos);
1189 pos += change;
1190 bufEnd += change;
1191 lengthChange += change;
1192 chars = buf.buffer; // in case buffer was
1193 // altered
1194 }
1195
1196 // remove variable from the cyclic stack
1197 priorVariables
1198 .remove(priorVariables.size() - 1);
1199 break;
1200 }
1201 nestedVarCount--;
1202 pos += endMatchLen;
1203 }
1204 }
1205 }
1206 }
1207 if (top) {
1208 return altered ? 1 : 0;
1209 }
1210 return lengthChange;
1211 }
1212 }