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