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