View Javadoc
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  
18  package org.apache.commons.text;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertSame;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  
27  import java.io.IOException;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.Properties;
31  
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.commons.lang3.SystemProperties;
34  import org.apache.commons.lang3.mutable.MutableObject;
35  import org.apache.commons.text.lookup.StringLookup;
36  import org.apache.commons.text.lookup.StringLookupFactory;
37  import org.apache.commons.text.matcher.StringMatcher;
38  import org.apache.commons.text.matcher.StringMatcherFactory;
39  import org.junit.jupiter.api.AfterEach;
40  import org.junit.jupiter.api.BeforeEach;
41  import org.junit.jupiter.api.Disabled;
42  import org.junit.jupiter.api.MethodOrderer;
43  import org.junit.jupiter.api.Test;
44  import org.junit.jupiter.api.TestMethodOrder;
45  
46  /**
47   * Test class for {@link StringSubstitutor}.
48   */
49  @TestMethodOrder(MethodOrderer.MethodName.class) // temp, for my sanity during dev
50  public class StringSubstitutorTest {
51  
52      private static final String ACTUAL_ANIMAL = "quick brown fox";
53      private static final String ACTUAL_TARGET = "lazy dog";
54      private static final String CLASSIC_RESULT = "The quick brown fox jumps over the lazy dog.";
55      private static final String CLASSIC_TEMPLATE = "The ${animal} jumps over the ${target}.";
56      private static final String EMPTY_EXPR = "${}";
57  
58      protected Map<String, String> values;
59  
60      private void assertEqualsCharSeq(final CharSequence expected, final CharSequence actual) {
61          assertEquals(expected, actual,
62                  () -> String.format("expected.length()=%,d, actual.length()=%,d", StringUtils.length(expected), StringUtils.length(actual)));
63      }
64  
65      protected void doNotReplace(final String replaceTemplate) throws IOException {
66          doTestNoReplace(new StringSubstitutor(values), replaceTemplate);
67      }
68  
69      protected void doReplace(final String expectedResult, final String replaceTemplate, final boolean substring) throws IOException {
70          doTestReplace(new StringSubstitutor(values), expectedResult, replaceTemplate, substring);
71      }
72  
73      protected void doTestNoReplace(final StringSubstitutor substitutor, final String replaceTemplate) throws IOException {
74          if (replaceTemplate == null) {
75              assertNull(replace(substitutor, (String) null));
76              assertNull(substitutor.replace((String) null, 0, 100));
77              assertNull(substitutor.replace((char[]) null));
78              assertNull(substitutor.replace((char[]) null, 0, 100));
79              assertNull(substitutor.replace((StringBuffer) null));
80              assertNull(substitutor.replace((StringBuffer) null, 0, 100));
81              assertNull(substitutor.replace((TextStringBuilder) null));
82              assertNull(substitutor.replace((TextStringBuilder) null, 0, 100));
83              assertNull(substitutor.replace((Object) null));
84              assertFalse(substitutor.replaceIn((StringBuffer) null));
85              assertFalse(substitutor.replaceIn((StringBuffer) null, 0, 100));
86              assertFalse(substitutor.replaceIn((TextStringBuilder) null));
87              assertFalse(substitutor.replaceIn((TextStringBuilder) null, 0, 100));
88          } else {
89              assertEquals(replaceTemplate, replace(substitutor, replaceTemplate));
90              final TextStringBuilder builder = new TextStringBuilder(replaceTemplate);
91              assertFalse(substitutor.replaceIn(builder));
92              assertEquals(replaceTemplate, builder.toString());
93          }
94      }
95  
96      protected void doTestReplace(final StringSubstitutor sub, final String expectedResult, final String replaceTemplate, final boolean substring)
97              throws IOException {
98          final String expectedShortResult = substring ? expectedResult.substring(1, expectedResult.length() - 1) : expectedResult;
99  
100         // replace using String
101         final String actual = replace(sub, replaceTemplate);
102         assertEquals(expectedResult, actual, () -> String.format("Index of difference: %,d", StringUtils.indexOfDifference(expectedResult, actual)));
103         if (substring) {
104             assertEquals(expectedShortResult, sub.replace(replaceTemplate, 1, replaceTemplate.length() - 2));
105         }
106 
107         // replace using char[]
108         final char[] chars = replaceTemplate.toCharArray();
109         assertEquals(expectedResult, sub.replace(chars));
110         if (substring) {
111             assertEquals(expectedShortResult, sub.replace(chars, 1, chars.length - 2));
112         }
113 
114         // replace using StringBuffer
115         StringBuffer buf = new StringBuffer(replaceTemplate);
116         assertEquals(expectedResult, sub.replace(buf));
117         if (substring) {
118             assertEquals(expectedShortResult, sub.replace(buf, 1, buf.length() - 2));
119         }
120 
121         // replace using StringBuilder
122         StringBuilder builder = new StringBuilder(replaceTemplate);
123         assertEquals(expectedResult, sub.replace(builder));
124         if (substring) {
125             assertEquals(expectedShortResult, sub.replace(builder, 1, builder.length() - 2));
126         }
127 
128         // replace using TextStringBuilder
129         TextStringBuilder bld = new TextStringBuilder(replaceTemplate);
130         assertEquals(expectedResult, sub.replace(bld));
131         if (substring) {
132             assertEquals(expectedShortResult, sub.replace(bld, 1, bld.length() - 2));
133         }
134 
135         // replace using object
136         final MutableObject<String> obj = new MutableObject<>(replaceTemplate); // toString returns template
137         assertEquals(expectedResult, sub.replace(obj));
138 
139         // replace in StringBuffer
140         buf = new StringBuffer(replaceTemplate);
141         assertTrue(sub.replaceIn(buf), replaceTemplate);
142         assertEquals(expectedResult, buf.toString());
143         if (substring) {
144             buf = new StringBuffer(replaceTemplate);
145             assertTrue(sub.replaceIn(buf, 1, buf.length() - 2));
146             assertEquals(expectedResult, buf.toString()); // expect full result as remainder is untouched
147         }
148 
149         // replace in StringBuilder
150         builder = new StringBuilder(replaceTemplate);
151         assertTrue(sub.replaceIn(builder));
152         assertEquals(expectedResult, builder.toString());
153         if (substring) {
154             builder = new StringBuilder(replaceTemplate);
155             assertTrue(sub.replaceIn(builder, 1, builder.length() - 2));
156             assertEquals(expectedResult, builder.toString()); // expect full result as remainder is untouched
157         }
158 
159         // replace in TextStringBuilder
160         bld = new TextStringBuilder(replaceTemplate);
161         assertTrue(sub.replaceIn(bld));
162         assertEquals(expectedResult, bld.toString());
163         if (substring) {
164             bld = new TextStringBuilder(replaceTemplate);
165             assertTrue(sub.replaceIn(bld, 1, bld.length() - 2));
166             assertEquals(expectedResult, bld.toString()); // expect full result as remainder is untouched
167         }
168     }
169 
170     /**
171      * For subclasses to override.
172      *
173      * @throws IOException Thrown by subclasses.
174      */
175     protected String replace(final StringSubstitutor stringSubstitutor, final String template) throws IOException {
176         return stringSubstitutor.replace(template);
177     }
178 
179     @BeforeEach
180     public void setUp() throws Exception {
181         values = new HashMap<>();
182         // shortest key and value.
183         values.put("a", "1");
184         values.put("aa", "11");
185         values.put("aaa", "111");
186         values.put("b", "2");
187         values.put("bb", "22");
188         values.put("bbb", "222");
189         values.put("a2b", "b");
190         // normal key and value.
191         values.put("animal", ACTUAL_ANIMAL);
192         values.put("target", ACTUAL_TARGET);
193     }
194 
195     @AfterEach
196     public void tearDown() throws Exception {
197         values = null;
198     }
199 
200     @Test
201     void testConstructorNullMap() {
202         final Map<String, Object> parameters = null;
203         final StringSubstitutor s = new StringSubstitutor(parameters, "prefix", "suffix");
204         assertNull(s.getStringLookup().apply("X"));
205         assertNull(s.getStringLookup().lookup("X"));
206     }
207 
208     @Test
209     void testConstructorStringSubstitutor() {
210         final StringSubstitutor source = new StringSubstitutor();
211         source.setDisableSubstitutionInValues(true);
212         source.setEnableSubstitutionInVariables(true);
213         source.setEnableUndefinedVariableException(true);
214         source.setEscapeChar('e');
215         source.setValueDelimiter('d');
216         source.setVariablePrefix('p');
217         source.setVariableResolver(StringLookupFactory.INSTANCE.nullStringLookup());
218         source.setVariableSuffix('s');
219         //
220         final StringSubstitutor target = new StringSubstitutor(source);
221         //
222         assertTrue(target.isDisableSubstitutionInValues());
223         assertTrue(target.isEnableSubstitutionInVariables());
224         assertTrue(target.isEnableUndefinedVariableException());
225         assertEquals('e', target.getEscapeChar());
226         assertTrue(target.getValueDelimiterMatcher().toString().endsWith("['d']"), target.getValueDelimiterMatcher().toString());
227         assertTrue(target.getVariablePrefixMatcher().toString().endsWith("['p']"), target.getValueDelimiterMatcher().toString());
228         assertTrue(target.getVariableSuffixMatcher().toString().endsWith("['s']"), target.getValueDelimiterMatcher().toString());
229     }
230 
231     @Test
232     void testDetectsCyclicSubstitution() {
233         final Map<String, String> map = new HashMap<>();
234         map.put("name", "<name>");
235         assertThrows(IllegalStateException.class, () -> StringSubstitutor.replace("Hi <name>!", map, "<", ">"));
236     }
237 
238     /**
239      * Tests get set.
240      */
241     @Test
242     void testGetSetEscape() {
243         final StringSubstitutor sub = new StringSubstitutor();
244         assertEquals('$', sub.getEscapeChar());
245         sub.setEscapeChar('<');
246         assertEquals('<', sub.getEscapeChar());
247     }
248 
249     /**
250      * Test for LANG-1055: StringSubstitutor.replaceSystemProperties does not work consistently
251      */
252     @Test
253     void testLANG1055() {
254         System.setProperty("test_key", "test_value");
255 
256         final String expected = StringSubstitutor.replace("test_key=${test_key}", System.getProperties());
257         final String actual = StringSubstitutor.replaceSystemProperties("test_key=${test_key}");
258         assertEquals(expected, actual);
259     }
260 
261     /**
262      * Tests interpolation with weird boundary patterns.
263      */
264     @Test
265     void testReplace_JiraText178_WeirdPatterns1() throws IOException {
266         doNotReplace("$${");
267         doNotReplace("$${a");
268         doNotReplace("$$${");
269         doNotReplace("$$${a");
270         doNotReplace("$${${a");
271         doNotReplace("${${a}"); // "${a" is not a registered variable name.
272         doNotReplace("${$${a}");
273     }
274 
275     /**
276      * Tests interpolation with weird boundary patterns.
277      */
278     @Test
279     void testReplace_JiraText178_WeirdPatterns2() throws IOException {
280         doReplace("${1}", "$${${a}}", false);
281     }
282 
283     /**
284      * Tests interpolation with weird boundary patterns.
285      */
286     @Test
287     @Disabled
288     void testReplace_JiraText178_WeirdPatterns3() throws IOException {
289         doReplace("${${a}", "$${${a}", false); // not "$${1" or "${1"
290     }
291 
292     /**
293      * Tests adjacent keys.
294      */
295     @Test
296     void testReplaceAdjacentAtEnd() throws IOException {
297         values.put("code", "GBP");
298         values.put("amount", "12.50");
299         final StringSubstitutor sub = new StringSubstitutor(values);
300         assertEqualsCharSeq("Amount is GBP12.50", replace(sub, "Amount is ${code}${amount}"));
301     }
302 
303     /**
304      * Tests adjacent keys.
305      */
306     @Test
307     void testReplaceAdjacentAtStart() throws IOException {
308         values.put("code", "GBP");
309         values.put("amount", "12.50");
310         final StringSubstitutor sub = new StringSubstitutor(values);
311         assertEqualsCharSeq("GBP12.50 charged", replace(sub, "${code}${amount} charged"));
312     }
313 
314     /**
315      * Tests key replace changing map after initialization (not recommended).
316      */
317     @Test
318     void testReplaceChangedMap() throws IOException {
319         final StringSubstitutor sub = new StringSubstitutor(values);
320         // no map change
321         final String template = CLASSIC_TEMPLATE;
322         assertEqualsCharSeq(CLASSIC_RESULT, replace(sub, template));
323         // map change
324         values.put("target", "moon");
325         assertEqualsCharSeq("The quick brown fox jumps over the moon.", replace(sub, template));
326     }
327 
328     /**
329      * Tests complex escaping.
330      */
331     @Test
332     void testReplaceComplexEscaping() throws IOException {
333         doReplace("${1}", "$${${a}}", false);
334         doReplace("${11}", "$${${aa}}", false);
335         doReplace("${111}", "$${${aaa}}", false);
336         doReplace("${quick brown fox}", "$${${animal}}", false);
337         doReplace("The ${quick brown fox} jumps over the lazy dog.", "The $${${animal}} jumps over the ${target}.", true);
338         doReplace("${${a}}", "$${$${a}}", false);
339         doReplace("${${aa}}", "$${$${aa}}", false);
340         doReplace("${${aaa}}", "$${$${aaa}}", false);
341         doReplace("${${animal}}", "$${$${animal}}", false);
342         doReplace(".${${animal}}", ".$${$${animal}}", false);
343         doReplace("${${animal}}.", "$${$${animal}}.", false);
344         doReplace(".${${animal}}.", ".$${$${animal}}.", false);
345         doReplace("The ${${animal}} jumps over the lazy dog.", "The $${$${animal}} jumps over the ${target}.", true);
346         doReplace("The ${quick brown fox} jumps over the lazy dog. ${1234567890}.",
347                 "The $${${animal}} jumps over the ${target}. $${${undefined.number:-1234567890}}.", true);
348     }
349 
350     /**
351      * Tests when no variable name.
352      */
353     @Test
354     void testReplaceEmptyKey() throws IOException {
355         doReplace("The ${} jumps over the lazy dog.", "The ${} jumps over the ${target}.", true);
356     }
357 
358     /**
359      * Tests when no variable name.
360      */
361     @Test
362     void testReplaceEmptyKeyExtraFirst() throws IOException {
363         assertEqualsCharSeq("." + EMPTY_EXPR, replace(new StringSubstitutor(values), "." + EMPTY_EXPR));
364     }
365 
366     /**
367      * Tests when no variable name.
368      */
369     @Test
370     void testReplaceEmptyKeyExtraLast() throws IOException {
371         assertEqualsCharSeq(EMPTY_EXPR + ".", replace(new StringSubstitutor(values), EMPTY_EXPR + "."));
372     }
373 
374     /**
375      * Tests when no variable name.
376      */
377     @Test
378     void testReplaceEmptyKeyOnly() throws IOException {
379         assertEquals(EMPTY_EXPR, replace(new StringSubstitutor(values), EMPTY_EXPR));
380     }
381 
382     /**
383      * Tests when no variable name.
384      */
385     @Test
386     void testReplaceEmptyKeyShortest() throws IOException {
387         doNotReplace(EMPTY_EXPR);
388     }
389 
390     /**
391      * Tests when no variable name.
392      */
393     @Test
394     void testReplaceEmptyKeyWithDefault() throws IOException {
395         doReplace("The animal jumps over the lazy dog.", "The ${:-animal} jumps over the ${target}.", true);
396     }
397 
398     /**
399      * Tests when no variable name.
400      */
401     @Test
402     void testReplaceEmptyKeyWithDefaultOnly() throws IOException {
403         doReplace("animal", "${:-animal}", false);
404     }
405 
406     /**
407      * Tests when no variable name.
408      */
409     @Test
410     void testReplaceEmptyKeyWithDefaultOnlyEmpty() throws IOException {
411         doReplace("", "${:-}", false);
412     }
413 
414     /**
415      * Tests when no variable name.
416      */
417     @Test
418     void testReplaceEmptyKeyWithDefaultOnlyShortest() throws IOException {
419         doReplace("a", "${:-a}", false);
420     }
421 
422     /**
423      * Tests replace with null.
424      */
425     @Test
426     void testReplaceEmptyString() throws IOException {
427         doNotReplace(StringUtils.EMPTY);
428     }
429 
430     /**
431      * Tests escaping.
432      */
433     @Test
434     void testReplaceEscaping() throws IOException {
435         doReplace("The ${animal} jumps over the lazy dog.", "The $${animal} jumps over the ${target}.", true);
436         doReplace("${a}", "$${a}", false);
437         doReplace("${a${a}}", "$${a$${a}}", false);
438         doReplace("${a${a${a}}}", "$${a$${a$${a}}}", false);
439     }
440 
441     /**
442      * Tests replace with fail on undefined variable.
443      */
444     @Test
445     void testReplaceFailOnUndefinedVariable() throws IOException {
446         values.put("animal.1", "fox");
447         values.put("animal.2", "mouse");
448         values.put("species", "2");
449         final StringSubstitutor sub = new StringSubstitutor(values);
450         sub.setEnableUndefinedVariableException(true);
451 
452         assertEquals("Cannot resolve variable 'animal.${species' (enableSubstitutionInVariables=false).",
453                 assertThrows(IllegalArgumentException.class, () -> replace(sub, "The ${animal.${species}} jumps over the ${target}.")).getMessage());
454 
455         assertEquals("Cannot resolve variable 'animal.${species:-1' (enableSubstitutionInVariables=false).",
456                 assertThrows(IllegalArgumentException.class, () -> replace(sub, "The ${animal.${species:-1}} jumps over the ${target}.")).getMessage());
457 
458         assertEquals("Cannot resolve variable 'unknown' (enableSubstitutionInVariables=false).",
459                 assertThrows(IllegalArgumentException.class, () -> replace(sub, "The ${test:-statement} is a sample for missing ${unknown}.")).getMessage());
460 
461         // if default value is available, exception will not be thrown
462         assertEqualsCharSeq("The statement is a sample for missing variable.",
463                 replace(sub, "The ${test:-statement} is a sample for missing ${unknown:-variable}."));
464 
465         assertEqualsCharSeq("The fox jumps over the lazy dog.", replace(sub, "The ${animal.1} jumps over the ${target}."));
466     }
467 
468     /**
469      * Tests whether replace with fail on undefined variable with substitution in variable names enabled.
470      */
471     @Test
472     void testReplaceFailOnUndefinedVariableWithReplaceInVariable() throws IOException {
473         values.put("animal.1", "fox");
474         values.put("animal.2", "mouse");
475         values.put("species", "2");
476         values.put("statement.1", "2");
477         values.put("recursive", "1");
478         values.put("word", "variable");
479         values.put("testok.2", "statement");
480         final StringSubstitutor sub = new StringSubstitutor(values);
481         sub.setEnableUndefinedVariableException(true);
482         sub.setEnableSubstitutionInVariables(true);
483 
484         assertEqualsCharSeq("The mouse jumps over the lazy dog.", replace(sub, "The ${animal.${species}} jumps over the ${target}."));
485         values.put("species", "1");
486         assertEqualsCharSeq("The fox jumps over the lazy dog.", replace(sub, "The ${animal.${species}} jumps over the ${target}."));
487 
488         // exception is thrown here because variable with name test.1 is missing
489         assertEquals("Cannot resolve variable 'statement' (enableSubstitutionInVariables=true).",
490                 assertThrows(IllegalArgumentException.class, () -> replace(sub, "The ${test.${statement}} is a sample for missing ${word}.")).getMessage());
491 
492         // exception is thrown here because variable with name test.2 is missing
493         assertEquals("Cannot resolve variable 'test.2' (enableSubstitutionInVariables=true).",
494                 assertThrows(IllegalArgumentException.class, () -> replace(sub, "The ${test.${statement.${recursive}}} is a sample for missing ${word}."))
495                         .getMessage());
496 
497         assertEqualsCharSeq("statement", replace(sub, "${testok.${statement.${recursive}}}"));
498 
499         assertEqualsCharSeq("${testok.2}", replace(sub, "$${testok.${statement.${recursive}}}"));
500 
501         assertEqualsCharSeq("The statement is a sample for missing variable.",
502                 replace(sub, "The ${testok.${statement.${recursive}}} is a sample for missing ${word}."));
503     }
504 
505     /**
506      * Tests when no incomplete prefix.
507      */
508     @Test
509     void testReplaceIncompletePrefix() throws IOException {
510         doReplace("The {animal} jumps over the lazy dog.", "The {animal} jumps over the ${target}.", true);
511     }
512 
513     @Test
514     void testReplaceInTakingStringBufferWithNonNull() {
515         final StringSubstitutor strSubstitutor = new StringSubstitutor(new HashMap<>(), "WV@i#y?N*[", "WV@i#y?N*[", '*');
516 
517         assertFalse(strSubstitutor.isPreserveEscapes());
518         assertFalse(strSubstitutor.replaceIn(new StringBuffer("WV@i#y?N*[")));
519         assertEquals('*', strSubstitutor.getEscapeChar());
520     }
521 
522     @Test
523     void testReplaceInTakingStringBuilderWithNonNull() {
524         final StringLookup strLookup = StringLookupFactory.INSTANCE.systemPropertyStringLookup();
525         final StringSubstitutor strSubstitutor = new StringSubstitutor(strLookup, "b<H", "b<H", '\'');
526         final StringBuilder stringBuilder = new StringBuilder((CharSequence) "b<H");
527 
528         assertEquals('\'', strSubstitutor.getEscapeChar());
529         assertFalse(strSubstitutor.replaceIn(stringBuilder));
530     }
531 
532     @Test
533     void testReplaceInTakingStringBuilderWithNull() {
534         final Map<String, Object> map = new HashMap<>();
535         final StringSubstitutor strSubstitutor = new StringSubstitutor(map, StringUtils.EMPTY, StringUtils.EMPTY, 'T', "K+<'f");
536 
537         assertFalse(strSubstitutor.replaceIn((StringBuilder) null));
538     }
539 
540     @Test
541     void testReplaceInTakingTwoAndThreeIntsReturningFalse() {
542         final Map<String, Object> hashMap = new HashMap<>();
543         final StringLookup mapStringLookup = StringLookupFactory.INSTANCE.mapStringLookup(hashMap);
544         final StringMatcher strMatcher = StringMatcherFactory.INSTANCE.tabMatcher();
545         final StringSubstitutor strSubstitutor = new StringSubstitutor(mapStringLookup, strMatcher, strMatcher, 'b', strMatcher);
546 
547         assertFalse(strSubstitutor.replaceIn((StringBuilder) null, 1315, -1369));
548         assertEquals('b', strSubstitutor.getEscapeChar());
549         assertFalse(strSubstitutor.isPreserveEscapes());
550     }
551 
552     /**
553      * Tests whether a variable can be replaced in a variable name.
554      */
555     @Test
556     void testReplaceInVariable() throws IOException {
557         values.put("animal.1", "fox");
558         values.put("animal.2", "mouse");
559         values.put("species", "2");
560         final StringSubstitutor sub = new StringSubstitutor(values);
561         sub.setEnableSubstitutionInVariables(true);
562         assertEqualsCharSeq("The mouse jumps over the lazy dog.", replace(sub, "The ${animal.${species}} jumps over the ${target}."));
563         values.put("species", "1");
564         assertEqualsCharSeq("The fox jumps over the lazy dog.", replace(sub, "The ${animal.${species}} jumps over the ${target}."));
565         assertEqualsCharSeq("The fox jumps over the lazy dog.",
566                 replace(sub, "The ${unknown.animal.${unknown.species:-1}:-fox} jumps over the ${unknow.target:-lazy dog}."));
567     }
568 
569     /**
570      * Tests whether substitution in variable names is disabled per default.
571      */
572     @Test
573     void testReplaceInVariableDisabled() throws IOException {
574         values.put("animal.1", "fox");
575         values.put("animal.2", "mouse");
576         values.put("species", "2");
577         final StringSubstitutor sub = new StringSubstitutor(values);
578         assertEqualsCharSeq("The ${animal.${species}} jumps over the lazy dog.", replace(sub, "The ${animal.${species}} jumps over the ${target}."));
579         assertEqualsCharSeq("The ${animal.${species:-1}} jumps over the lazy dog.", replace(sub, "The ${animal.${species:-1}} jumps over the ${target}."));
580     }
581 
582     /**
583      * Tests complex and recursive substitution in variable names.
584      */
585     @Test
586     void testReplaceInVariableRecursive() throws IOException {
587         values.put("animal.2", "brown fox");
588         values.put("animal.1", "white mouse");
589         values.put("color", "white");
590         values.put("species.white", "1");
591         values.put("species.brown", "2");
592         final StringSubstitutor sub = new StringSubstitutor(values);
593         sub.setEnableSubstitutionInVariables(true);
594         assertEqualsCharSeq("white mouse", replace(sub, "${animal.${species.${color}}}"));
595         assertEqualsCharSeq("The white mouse jumps over the lazy dog.", replace(sub, "The ${animal.${species.${color}}} jumps over the ${target}."));
596         assertEqualsCharSeq("The brown fox jumps over the lazy dog.",
597                 replace(sub, "The ${animal.${species.${unknownColor:-brown}}} jumps over the ${target}."));
598     }
599 
600     /**
601      * Tests when no variable name.
602      */
603     @Test
604     void testReplaceKeyStartChars() throws IOException {
605         final String substring = StringSubstitutor.DEFAULT_VAR_START + "a";
606         assertEqualsCharSeq(substring, replace(new StringSubstitutor(values), substring));
607     }
608 
609     /**
610      * Tests when no variable name.
611      */
612     @Test
613     void testReplaceKeyStartChars1Only() throws IOException {
614         final String substring = StringSubstitutor.DEFAULT_VAR_START.substring(0, 1);
615         assertEqualsCharSeq(substring, replace(new StringSubstitutor(values), substring));
616     }
617 
618     /**
619      * Tests when no variable name.
620      */
621     @Test
622     void testReplaceKeyStartChars2Only() throws IOException {
623         final String substring = StringSubstitutor.DEFAULT_VAR_START.substring(0, 2);
624         assertEqualsCharSeq(substring, replace(new StringSubstitutor(values), substring));
625     }
626 
627     /**
628      * Tests when no prefix or suffix.
629      */
630     @Test
631     void testReplaceNoPrefixNoSuffix() throws IOException {
632         doReplace("The animal jumps over the lazy dog.", "The animal jumps over the ${target}.", true);
633     }
634 
635     /**
636      * Tests when suffix but no prefix.
637      */
638     @Test
639     void testReplaceNoPrefixSuffix() throws IOException {
640         doReplace("The animal} jumps over the lazy dog.", "The animal} jumps over the ${target}.", true);
641     }
642 
643     /**
644      * Tests replace with no variables.
645      */
646     @Test
647     void testReplaceNoVariables() throws IOException {
648         doNotReplace("The balloon arrived.");
649     }
650 
651     /**
652      * Tests replace with null.
653      */
654     @Test
655     void testReplaceNull() throws IOException {
656         doNotReplace(null);
657     }
658 
659     /**
660      * Tests simple key replace.
661      */
662     @Test
663     void testReplacePartialString_noReplace() {
664         final StringSubstitutor sub = new StringSubstitutor();
665         assertEqualsCharSeq("${animal} jumps", sub.replace(CLASSIC_TEMPLATE, 4, 15));
666     }
667 
668     /**
669      * Tests when prefix but no suffix.
670      */
671     @Test
672     void testReplacePrefixNoSuffix() throws IOException {
673         doReplace("The ${animal jumps over the ${target} lazy dog.", "The ${animal jumps over the ${target} ${target}.", true);
674     }
675 
676     /**
677      * Tests simple recursive replace.
678      */
679     @Test
680     void testReplaceRecursive() throws IOException {
681         values.put("animal", "${critter}");
682         values.put("target", "${pet}");
683         values.put("pet", "${petCharacteristic} dog");
684         values.put("petCharacteristic", "lazy");
685         values.put("critter", "${critterSpeed} ${critterColor} ${critterType}");
686         values.put("critterSpeed", "quick");
687         values.put("critterColor", "brown");
688         values.put("critterType", "fox");
689         doReplace(CLASSIC_RESULT, CLASSIC_TEMPLATE, true);
690 
691         values.put("pet", "${petCharacteristicUnknown:-lazy} dog");
692         doReplace(CLASSIC_RESULT, CLASSIC_TEMPLATE, true);
693     }
694 
695     /**
696      * Tests simple key replace.
697      */
698     @Test
699     void testReplaceSimple() throws IOException {
700         doReplace(CLASSIC_RESULT, CLASSIC_TEMPLATE, true);
701     }
702 
703     /**
704      * Tests simple key replace.
705      */
706     @Test
707     void testReplaceSimpleKeySize1() throws IOException {
708         doReplace("1", "${a}", false);
709     }
710 
711     /**
712      * Tests simple key replace.
713      */
714     @Test
715     void testReplaceSimpleKeySize2() throws IOException {
716         doReplace("11", "${aa}", false);
717     }
718 
719     /**
720      * Tests simple key replace.
721      */
722     @Test
723     void testReplaceSimpleKeySize3() throws IOException {
724         doReplace("111", "${aaa}", false);
725     }
726 
727     @Test
728     void testReplaceTakingCharSequenceReturningNull() {
729         final StringSubstitutor strSubstitutor = new StringSubstitutor((StringLookup) null);
730 
731         assertNull(strSubstitutor.replace((CharSequence) null));
732         assertFalse(strSubstitutor.isPreserveEscapes());
733         assertEquals('$', strSubstitutor.getEscapeChar());
734     }
735 
736     @Test
737     void testReplaceTakingThreeArgumentsThrowsNullPointerException() {
738         assertThrows(NullPointerException.class, () -> StringSubstitutor.replace(null, (Properties) null));
739     }
740 
741     @Test
742     void testReplaceThrowsStringIndexOutOfBoundsException() {
743         final StringSubstitutor sub = new StringSubstitutor();
744 
745         // replace(char[], int, int)
746         final char[] emptyCharArray = {};
747         // offset greater than array length
748         assertThrows(StringIndexOutOfBoundsException.class, () -> sub.replace(emptyCharArray, 0, 1));
749         // source != null && (offset > source.length || offset < 0)
750         assertThrows(StringIndexOutOfBoundsException.class, () -> sub.replace(emptyCharArray, 1, 0));
751 
752         // replace(String, int, int)
753         // offset greater than source length
754         assertThrows(StringIndexOutOfBoundsException.class, () -> sub.replace("", 1, 1));
755         // source != null && offset >= 0 && offset <= source.length() && (length > -offset + source.length() || length < 0)
756         assertThrows(StringIndexOutOfBoundsException.class, () -> sub.replace("", 0, 1));
757     }
758 
759     /**
760      * Tests replace creates output same as input.
761      */
762     @Test
763     void testReplaceToIdentical() throws IOException {
764         values.put("animal", "$${${thing}}");
765         values.put("thing", "animal");
766         doReplace("The ${animal} jumps.", "The ${animal} jumps.", true);
767     }
768 
769     /**
770      * Tests unknown key replace.
771      */
772     @Test
773     void testReplaceUnknownKey() throws IOException {
774         doReplace("The ${person} jumps over the lazy dog.", "The ${person} jumps over the ${target}.", true);
775     }
776 
777     /**
778      * Tests unknown key replace.
779      */
780     @Test
781     void testReplaceUnknownKeyDefaultValue() throws IOException {
782         doReplace("The ${person} jumps over the lazy dog. 1234567890.", "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true);
783     }
784 
785     /**
786      * Tests unknown key replace.
787      */
788     @Test
789     void testReplaceUnknownKeyOnly() throws IOException {
790         final String expected = "${person}";
791         assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected));
792     }
793 
794     /**
795      * Tests unknown key replace.
796      */
797     @Test
798     void testReplaceUnknownKeyOnlyExtraFirst() throws IOException {
799         final String expected = ".${person}";
800         assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected));
801     }
802 
803     /**
804      * Tests unknown key replace.
805      */
806     @Test
807     void testReplaceUnknownKeyOnlyExtraLast() throws IOException {
808         final String expected = "${person}.";
809         assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected));
810     }
811 
812     /**
813      * Tests unknown key replace.
814      */
815     @Test
816     void testReplaceUnknownShortestKeyOnly() throws IOException {
817         final String expected = "${U}";
818         assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected));
819     }
820 
821     /**
822      * Tests unknown key replace.
823      */
824     @Test
825     void testReplaceUnknownShortestKeyOnlyExtraFirst() throws IOException {
826         final String expected = ".${U}";
827         assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected));
828     }
829 
830     /**
831      * Tests unknown key replace.
832      */
833     @Test
834     void testReplaceUnknownShortestKeyOnlyExtraLast() throws IOException {
835         final String expected = "${U}.";
836         assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected));
837     }
838 
839     /**
840      * Tests simple key replace.
841      */
842     @Test
843     void testReplaceVariablesCount1() throws IOException {
844         doReplace(ACTUAL_ANIMAL, "${animal}", false);
845     }
846 
847     /**
848      * Tests escaping.
849      */
850     @Test
851     void testReplaceVariablesCount1Escaping2To1() throws IOException {
852         doReplace("${a}", "$${a}", false);
853         doReplace("${animal}", "$${animal}", false);
854     }
855 
856     /**
857      * Tests escaping.
858      */
859     @Test
860     void testReplaceVariablesCount1Escaping3To2() throws IOException {
861         doReplace("$${a}", "$$${a}", false);
862         doReplace("$${animal}", "$$${animal}", false);
863     }
864 
865     /**
866      * Tests escaping.
867      */
868     @Test
869     void testReplaceVariablesCount1Escaping4To3() throws IOException {
870         doReplace("$$${a}", "$$$${a}", false);
871         doReplace("$$${animal}", "$$$${animal}", false);
872     }
873 
874     /**
875      * Tests escaping.
876      */
877     @Test
878     void testReplaceVariablesCount1Escaping5To4() throws IOException {
879         doReplace("$$$${a}", "$$$$${a}", false);
880         doReplace("$$$${animal}", "$$$$${animal}", false);
881     }
882 
883     /**
884      * Tests escaping.
885      */
886     @Test
887     void testReplaceVariablesCount1Escaping6To4() throws IOException {
888         doReplace("$$$$${a}", "$$$$$${a}", false);
889         doReplace("$$$$${animal}", "$$$$$${animal}", false);
890     }
891 
892     /**
893      * Tests simple key replace.
894      */
895     @Test
896     void testReplaceVariablesCount2() throws IOException {
897         // doTestReplace("12", "${a}${b}", false);
898         doReplace("1122", "${aa}${bb}", false);
899         doReplace(ACTUAL_ANIMAL + ACTUAL_ANIMAL, "${animal}${animal}", false);
900         doReplace(ACTUAL_TARGET + ACTUAL_TARGET, "${target}${target}", false);
901         doReplace(ACTUAL_ANIMAL + ACTUAL_TARGET, "${animal}${target}", false);
902     }
903 
904     /**
905      * Tests simple key replace.
906      */
907     @Test
908     void testReplaceVariablesCount2NonAdjacent() throws IOException {
909         doReplace("1 2", "${a} ${b}", false);
910         doReplace("11 22", "${aa} ${bb}", false);
911         doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal}", false);
912         doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal}", false);
913         doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal}", false);
914     }
915 
916     /**
917      * Tests simple key replace.
918      */
919     @Test
920     void testReplaceVariablesCount3() throws IOException {
921         doReplace("121", "${a}${b}${a}", false);
922         doReplace("112211", "${aa}${bb}${aa}", false);
923         doReplace(ACTUAL_ANIMAL + ACTUAL_ANIMAL + ACTUAL_ANIMAL, "${animal}${animal}${animal}", false);
924         doReplace(ACTUAL_TARGET + ACTUAL_TARGET + ACTUAL_TARGET, "${target}${target}${target}", false);
925     }
926 
927     /**
928      * Tests simple key replace.
929      */
930     @Test
931     void testReplaceVariablesCount3NonAdjacent() throws IOException {
932         doReplace("1 2 1", "${a} ${b} ${a}", false);
933         doReplace("11 22 11", "${aa} ${bb} ${aa}", false);
934         doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal} ${animal}", false);
935         doReplace(ACTUAL_TARGET + " " + ACTUAL_TARGET + " " + ACTUAL_TARGET, "${target} ${target} ${target}", false);
936     }
937 
938     /**
939      * Tests interpolation with weird boundary patterns.
940      */
941     @Test
942     void testReplaceWeirdPattens() throws IOException {
943         doNotReplace(StringUtils.EMPTY);
944         doNotReplace(EMPTY_EXPR);
945         doNotReplace("${ }");
946         doNotReplace("${\t}");
947         doNotReplace("${\n}");
948         doNotReplace("${\b}");
949         doNotReplace("${");
950         doNotReplace("$}");
951         doNotReplace("$$}");
952         doNotReplace("}");
953         doNotReplace("${}$");
954         doNotReplace("${}$$");
955         doNotReplace("${${");
956         doNotReplace("${${}}");
957         doNotReplace("${$${}}");
958         doNotReplace("${$$${}}");
959         doNotReplace("${$$${$}}");
960         doNotReplace("${${}}");
961         doNotReplace("${${ }}");
962         //
963         doNotReplace("${$${a}}");
964         doNotReplace("${$$${a}}");
965         doNotReplace("${${a}}");
966         doNotReplace("${${${a}");
967         doNotReplace("${ ${a}");
968         doNotReplace("${ ${ ${a}");
969         //
970         doReplace("${1}", "$${${a}}", false);
971         doReplace("${ 1}", "$${ ${a}}", false);
972         doReplace("${12}", "$${${a}${b}}", false);
973         doReplace("${ 1 2 }", "$${ ${a} ${b} }", false);
974         doReplace("${${${a}2", "${${${a}${b}", false);
975     }
976 
977     /**
978      * Tests protected.
979      */
980     @Test
981     void testResolveVariable() {
982         final TextStringBuilder builder = new TextStringBuilder("Hi ${name}!");
983         final Map<String, String> map = new HashMap<>();
984         map.put("name", "commons");
985         final StringSubstitutor sub = new StringSubstitutor(map) {
986             @Override
987             protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos, final int endPos) {
988                 assertEquals("name", variableName);
989                 assertSame(builder, buf);
990                 assertEquals(3, startPos);
991                 assertEquals(10, endPos);
992                 return "jakarta";
993             }
994         };
995         sub.replaceIn(builder);
996         assertEqualsCharSeq("Hi jakarta!", builder.toString());
997     }
998 
999     @Test
1000     void testSamePrefixAndSuffix() {
1001         final Map<String, String> map = new HashMap<>();
1002         map.put("greeting", "Hello");
1003         map.put(" there ", "XXX");
1004         map.put("name", "commons");
1005         assertEqualsCharSeq("Hi commons!", StringSubstitutor.replace("Hi @name@!", map, "@", "@"));
1006         assertEqualsCharSeq("Hello there commons!", StringSubstitutor.replace("@greeting@ there @name@!", map, "@", "@"));
1007     }
1008 
1009     /**
1010      * Tests static.
1011      */
1012     @Test
1013     void testStaticReplace() {
1014         final Map<String, String> map = new HashMap<>();
1015         map.put("name", "commons");
1016         assertEqualsCharSeq("Hi commons!", StringSubstitutor.replace("Hi ${name}!", map));
1017     }
1018 
1019     /**
1020      * Tests static.
1021      */
1022     @Test
1023     void testStaticReplacePrefixSuffix() {
1024         final Map<String, String> map = new HashMap<>();
1025         map.put("name", "commons");
1026         assertEqualsCharSeq("Hi commons!", StringSubstitutor.replace("Hi <name>!", map, "<", ">"));
1027     }
1028 
1029     /**
1030      * Tests interpolation with system properties.
1031      */
1032     @Test
1033     void testStaticReplaceSystemProperties() {
1034         final TextStringBuilder buf = new TextStringBuilder();
1035         buf.append("Hi ").append(SystemProperties.getUserName());
1036         buf.append(", you are working with ");
1037         buf.append(SystemProperties.getOsName());
1038         buf.append(", your home directory is ");
1039         buf.append(SystemProperties.getUserHome()).append('.');
1040         assertEqualsCharSeq(buf.toString(),
1041                 StringSubstitutor.replaceSystemProperties("Hi ${user.name}, you are " + "working with ${os.name}, your home directory is ${user.home}."));
1042     }
1043 
1044     /**
1045      * Tests interpolation with system properties.
1046      */
1047     @Test
1048     void testStaticReplaceSystemPropertiesWithUpdate() {
1049         System.setProperty("foo", "bar1");
1050         try {
1051             assertEqualsCharSeq("bar1", StringSubstitutor.replaceSystemProperties("${foo}"));
1052             System.setProperty("foo", "bar2");
1053             assertEqualsCharSeq("bar2", StringSubstitutor.replaceSystemProperties("${foo}"));
1054         } finally {
1055             System.getProperties().remove("foo");
1056         }
1057     }
1058 
1059     /**
1060      * Test the replace of a properties object
1061      */
1062     @Test
1063     void testSubstituteDefaultProperties() {
1064         final String org = "${doesnotwork}";
1065         System.setProperty("doesnotwork", "It works!");
1066 
1067         // create a new Properties object with the System.getProperties as default
1068         final Properties props = new Properties(System.getProperties());
1069 
1070         assertEqualsCharSeq("It works!", StringSubstitutor.replace(org, props));
1071     }
1072 
1073     @Test
1074     void testSubstitutePreserveEscape() throws IOException {
1075         final String org = "${not-escaped} $${escaped}";
1076         final Map<String, String> map = new HashMap<>();
1077         map.put("not-escaped", "value");
1078 
1079         final StringSubstitutor sub = new StringSubstitutor(map, "${", "}", '$');
1080         assertFalse(sub.isPreserveEscapes());
1081         assertEqualsCharSeq("value ${escaped}", replace(sub, org));
1082 
1083         sub.setPreserveEscapes(true);
1084         assertTrue(sub.isPreserveEscapes());
1085         assertEqualsCharSeq("value $${escaped}", replace(sub, org));
1086     }
1087 
1088 }