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.lang3.text;
19  
20  import static org.apache.commons.lang3.LangAssertions.assertNullPointerException;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertSame;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  
29  import java.util.HashMap;
30  import java.util.Map;
31  import java.util.Properties;
32  
33  import org.apache.commons.lang3.AbstractLangTest;
34  import org.apache.commons.lang3.SystemProperties;
35  import org.apache.commons.lang3.mutable.MutableObject;
36  import org.junit.jupiter.api.AfterEach;
37  import org.junit.jupiter.api.BeforeEach;
38  import org.junit.jupiter.api.Test;
39  
40  /**
41   * Test class for StrSubstitutor.
42   */
43  @Deprecated
44  class StrSubstitutorTest extends AbstractLangTest {
45  
46      private Map<String, String> values;
47  
48      private void doTestNoReplace(final String replaceTemplate) {
49          final StrSubstitutor sub = new StrSubstitutor(values);
50  
51          if (replaceTemplate == null) {
52              assertNull(sub.replace((String) null));
53              assertNull(sub.replace((String) null, 0, 100));
54              assertNull(sub.replace((char[]) null));
55              assertNull(sub.replace((char[]) null, 0, 100));
56              assertNull(sub.replace((StringBuffer) null));
57              assertNull(sub.replace((StringBuffer) null, 0, 100));
58              assertNull(sub.replace((StrBuilder) null));
59              assertNull(sub.replace((StrBuilder) null, 0, 100));
60              assertNull(sub.replace((Object) null));
61              assertFalse(sub.replaceIn((StringBuffer) null));
62              assertFalse(sub.replaceIn((StringBuffer) null, 0, 100));
63              assertFalse(sub.replaceIn((StrBuilder) null));
64              assertFalse(sub.replaceIn((StrBuilder) null, 0, 100));
65          } else {
66              assertEquals(replaceTemplate, sub.replace(replaceTemplate));
67              final StrBuilder bld = new StrBuilder(replaceTemplate);
68              assertFalse(sub.replaceIn(bld));
69              assertEquals(replaceTemplate, bld.toString());
70          }
71      }
72  
73      private void doTestReplace(final String expectedResult, final String replaceTemplate, final boolean substring) {
74          final String expectedShortResult = expectedResult.substring(1, expectedResult.length() - 1);
75          final StrSubstitutor sub = new StrSubstitutor(values);
76  
77          // replace using String
78          assertEquals(expectedResult, sub.replace(replaceTemplate));
79          if (substring) {
80              assertEquals(expectedShortResult, sub.replace(replaceTemplate, 1, replaceTemplate.length() - 2));
81          }
82  
83          // replace using char[]
84          final char[] chars = replaceTemplate.toCharArray();
85          assertEquals(expectedResult, sub.replace(chars));
86          if (substring) {
87              assertEquals(expectedShortResult, sub.replace(chars, 1, chars.length - 2));
88          }
89  
90          // replace using StringBuffer
91          StringBuffer buf = new StringBuffer(replaceTemplate);
92          assertEquals(expectedResult, sub.replace(buf));
93          if (substring) {
94              assertEquals(expectedShortResult, sub.replace(buf, 1, buf.length() - 2));
95          }
96  
97          // replace using StringBuilder
98          StringBuilder builder = new StringBuilder(replaceTemplate);
99          assertEquals(expectedResult, sub.replace(builder));
100         if (substring) {
101             assertEquals(expectedShortResult, sub.replace(builder, 1, builder.length() - 2));
102         }
103 
104         // replace using StrBuilder
105         StrBuilder bld = new StrBuilder(replaceTemplate);
106         assertEquals(expectedResult, sub.replace(bld));
107         if (substring) {
108             assertEquals(expectedShortResult, sub.replace(bld, 1, bld.length() - 2));
109         }
110 
111         // replace using object
112         final MutableObject<String> obj = new MutableObject<>(replaceTemplate);  // toString returns template
113         assertEquals(expectedResult, sub.replace(obj));
114 
115         // replace in StringBuffer
116         buf = new StringBuffer(replaceTemplate);
117         assertTrue(sub.replaceIn(buf));
118         assertEquals(expectedResult, buf.toString());
119         if (substring) {
120             buf = new StringBuffer(replaceTemplate);
121             assertTrue(sub.replaceIn(buf, 1, buf.length() - 2));
122             assertEquals(expectedResult, buf.toString());  // expect full result as remainder is untouched
123         }
124 
125         // replace in StringBuilder
126         builder = new StringBuilder(replaceTemplate);
127         assertTrue(sub.replaceIn(builder));
128         assertEquals(expectedResult, builder.toString());
129         if (substring) {
130             builder = new StringBuilder(replaceTemplate);
131             assertTrue(sub.replaceIn(builder, 1, builder.length() - 2));
132             assertEquals(expectedResult, builder.toString());  // expect full result as remainder is untouched
133         }
134 
135         // replace in StrBuilder
136         bld = new StrBuilder(replaceTemplate);
137         assertTrue(sub.replaceIn(bld));
138         assertEquals(expectedResult, bld.toString());
139         if (substring) {
140             bld = new StrBuilder(replaceTemplate);
141             assertTrue(sub.replaceIn(bld, 1, bld.length() - 2));
142             assertEquals(expectedResult, bld.toString());  // expect full result as remainder is untouched
143         }
144     }
145 
146     @BeforeEach
147     public void setUp() {
148         values = new HashMap<>();
149         values.put("animal", "quick brown fox");
150         values.put("target", "lazy dog");
151     }
152 
153     @AfterEach
154     public void tearDown() {
155         values = null;
156     }
157 
158     /**
159      * Tests constructor.
160      */
161     @Test
162     void testConstructorMapFull() {
163         final Map<String, String> map = new HashMap<>();
164         map.put("name", "commons");
165         StrSubstitutor sub = new StrSubstitutor(map, "<", ">", '!');
166         assertEquals("Hi < commons", sub.replace("Hi !< <name>"));
167         sub = new StrSubstitutor(map, "<", ">", '!', "||");
168         assertEquals("Hi < commons", sub.replace("Hi !< <name2||commons>"));
169     }
170 
171     /**
172      * Tests constructor.
173      */
174     @Test
175     void testConstructorMapPrefixSuffix() {
176         final Map<String, String> map = new HashMap<>();
177         map.put("name", "commons");
178         final StrSubstitutor sub = new StrSubstitutor(map, "<", ">");
179         assertEquals("Hi < commons", sub.replace("Hi $< <name>"));
180     }
181 
182     /**
183      * Tests constructor.
184      */
185     @Test
186     void testConstructorNoArgs() {
187         final StrSubstitutor sub = new StrSubstitutor();
188         assertEquals("Hi ${name}", sub.replace("Hi ${name}"));
189     }
190 
191     /**
192      * Tests a cyclic replace operation.
193      * The cycle should be detected and cause an exception to be thrown.
194      */
195     @Test
196     void testCyclicReplacement() {
197         final Map<String, String> map = new HashMap<>();
198         map.put("animal", "${critter}");
199         map.put("target", "${pet}");
200         map.put("pet", "${petCharacteristic} dog");
201         map.put("petCharacteristic", "lazy");
202         map.put("critter", "${critterSpeed} ${critterColor} ${critterType}");
203         map.put("critterSpeed", "quick");
204         map.put("critterColor", "brown");
205         map.put("critterType", "${animal}");
206         final StrSubstitutor sub = new StrSubstitutor(map);
207         assertThrows(IllegalStateException.class, () -> sub.replace("The ${animal} jumps over the ${target}."), "Cyclic replacement was not detected.");
208         // also check even when default value is set.
209         map.put("critterType", "${animal:-fox}");
210         final StrSubstitutor sub2 = new StrSubstitutor(map);
211         assertThrows(IllegalStateException.class, () -> sub2.replace("The ${animal} jumps over the ${target}."), "Cyclic replacement was not detected.");
212     }
213 
214     /**
215      * Tests that deeply nested (acyclic) variable references hit the depth budget with an
216      * {@link IllegalStateException} instead of recursing towards {@link StackOverflowError}.
217      */
218     @Test
219     void testDeeplyNestedReplacementThrowsIllegalStateException() {
220         final Map<String, String> map = new HashMap<>();
221         for (int i = 0; i < 400; i++) {
222             map.put("v" + i, "${v" + (i + 1) + "}");
223         }
224         map.put("v400", "x");
225         final StrSubstitutor sub = new StrSubstitutor(map);
226         assertThrows(IllegalStateException.class, () -> sub.replace("${v0}"), "Depth budget was not enforced.");
227         // shallow nesting still works, including after a budget-exceeded failure (counters reset per top-level call)
228         assertEquals("x", sub.replace("${v398}"));
229     }
230 
231     @Test
232     void testDefaultValueDelimiters() {
233         final Map<String, String> map = new HashMap<>();
234         map.put("animal", "fox");
235         map.put("target", "dog");
236 
237         StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$');
238         assertEquals("The fox jumps over the lazy dog. 1234567890.",
239                 sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number:-1234567890}."));
240 
241         sub = new StrSubstitutor(map, "${", "}", '$', "?:");
242         assertEquals("The fox jumps over the lazy dog. 1234567890.",
243                 sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number?:1234567890}."));
244 
245         sub = new StrSubstitutor(map, "${", "}", '$', "||");
246         assertEquals("The fox jumps over the lazy dog. 1234567890.",
247                 sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number||1234567890}."));
248 
249         sub = new StrSubstitutor(map, "${", "}", '$', "!");
250         assertEquals("The fox jumps over the lazy dog. 1234567890.",
251                 sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
252 
253         sub = new StrSubstitutor(map, "${", "}", '$', "");
254         sub.setValueDelimiterMatcher(null);
255         assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.",
256                 sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
257 
258         sub = new StrSubstitutor(map, "${", "}", '$');
259         sub.setValueDelimiterMatcher(null);
260         assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.",
261                 sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
262     }
263 
264     /**
265      * Tests that exponential acyclic fan-out (each variable expanding to many more) hits the
266      * total-output-size budget with an {@link IllegalStateException} instead of consuming
267      * unbounded CPU and memory. The cycle check cannot detect this shape (no variable repeats
268      * on the substitution stack).
269      */
270     @Test
271     void testExponentialFanOutReplacementThrowsIllegalStateException() {
272         final Map<String, String> map = new HashMap<>();
273         final char[] leafChars = new char[8192];
274         java.util.Arrays.fill(leafChars, 'x');
275         map.put("a6", new String(leafChars));
276         for (int level = 5; level >= 0; level--) {
277             final StringBuilder value = new StringBuilder();
278             for (int i = 0; i < 10; i++) {
279                 value.append("${a").append(level + 1).append("}");
280             }
281             map.put("a" + level, value.toString());
282         }
283         // full expansion would be 10^6 leaves * 8 KiB = ~8 GiB
284         final StrSubstitutor sub = new StrSubstitutor(map);
285         assertThrows(IllegalStateException.class, () -> sub.replace("${a0}"), "Size budget was not enforced.");
286     }
287 
288     /**
289      * Tests get set.
290      */
291     @Test
292     void testGetSetEscape() {
293         final StrSubstitutor sub = new StrSubstitutor();
294         assertEquals('$', sub.getEscapeChar());
295         sub.setEscapeChar('<');
296         assertEquals('<', sub.getEscapeChar());
297     }
298 
299     /**
300      * Tests get set.
301      */
302     @Test
303     void testGetSetPrefix() {
304         final StrSubstitutor sub = new StrSubstitutor();
305         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getVariablePrefixMatcher());
306         sub.setVariablePrefix('<');
307         assertInstanceOf(StrMatcher.CharMatcher.class, sub.getVariablePrefixMatcher());
308 
309         sub.setVariablePrefix("<<");
310         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getVariablePrefixMatcher());
311         assertNullPointerException(() -> sub.setVariablePrefix(null));
312         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getVariablePrefixMatcher());
313 
314         final StrMatcher matcher = StrMatcher.commaMatcher();
315         sub.setVariablePrefixMatcher(matcher);
316         assertSame(matcher, sub.getVariablePrefixMatcher());
317         assertNullPointerException(() -> sub.setVariablePrefixMatcher(null));
318         assertSame(matcher, sub.getVariablePrefixMatcher());
319     }
320 
321     /**
322      * Tests get set.
323      */
324     @Test
325     void testGetSetSuffix() {
326         final StrSubstitutor sub = new StrSubstitutor();
327         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getVariableSuffixMatcher());
328         sub.setVariableSuffix('<');
329         assertInstanceOf(StrMatcher.CharMatcher.class, sub.getVariableSuffixMatcher());
330 
331         sub.setVariableSuffix("<<");
332         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getVariableSuffixMatcher());
333         assertNullPointerException(() -> sub.setVariableSuffix(null));
334         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getVariableSuffixMatcher());
335 
336         final StrMatcher matcher = StrMatcher.commaMatcher();
337         sub.setVariableSuffixMatcher(matcher);
338         assertSame(matcher, sub.getVariableSuffixMatcher());
339         assertNullPointerException(() -> sub.setVariableSuffixMatcher(null));
340         assertSame(matcher, sub.getVariableSuffixMatcher());
341     }
342 
343     /**
344      * Tests get set.
345      */
346     @Test
347     void testGetSetValueDelimiter() {
348         final StrSubstitutor sub = new StrSubstitutor();
349         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getValueDelimiterMatcher());
350         sub.setValueDelimiter(':');
351         assertInstanceOf(StrMatcher.CharMatcher.class, sub.getValueDelimiterMatcher());
352 
353         sub.setValueDelimiter("||");
354         assertInstanceOf(StrMatcher.StringMatcher.class, sub.getValueDelimiterMatcher());
355         sub.setValueDelimiter(null);
356         assertNull(sub.getValueDelimiterMatcher());
357 
358         final StrMatcher matcher = StrMatcher.commaMatcher();
359         sub.setValueDelimiterMatcher(matcher);
360         assertSame(matcher, sub.getValueDelimiterMatcher());
361         sub.setValueDelimiterMatcher(null);
362         assertNull(sub.getValueDelimiterMatcher());
363     }
364 
365     /**
366      * Test for LANG-1055: StrSubstitutor.replaceSystemProperties does not work consistently
367      */
368     @Test
369     void testLANG1055() {
370         System.setProperty("test_key",  "test_value");
371 
372         final String expected = StrSubstitutor.replace("test_key=${test_key}", System.getProperties());
373         final String actual = StrSubstitutor.replaceSystemProperties("test_key=${test_key}");
374         assertEquals(expected, actual);
375     }
376 
377     /**
378      * Tests adjacent keys.
379      */
380     @Test
381     void testReplaceAdjacentAtEnd() {
382         values.put("code", "GBP");
383         values.put("amount", "12.50");
384         final StrSubstitutor sub = new StrSubstitutor(values);
385         assertEquals("Amount is GBP12.50", sub.replace("Amount is ${code}${amount}"));
386     }
387 
388     /**
389      * Tests adjacent keys.
390      */
391     @Test
392     void testReplaceAdjacentAtStart() {
393         values.put("code", "GBP");
394         values.put("amount", "12.50");
395         final StrSubstitutor sub = new StrSubstitutor(values);
396         assertEquals("GBP12.50 charged", sub.replace("${code}${amount} charged"));
397     }
398 
399     /**
400      * Tests key replace changing map after initialization (not recommended).
401      */
402     @Test
403     void testReplaceChangedMap() {
404         final StrSubstitutor sub = new StrSubstitutor(values);
405         values.put("target", "moon");
406         assertEquals("The quick brown fox jumps over the moon.", sub.replace("The ${animal} jumps over the ${target}."));
407     }
408 
409     /**
410      * Tests complex escaping.
411      */
412     @Test
413     void testReplaceComplexEscaping() {
414         doTestReplace("The ${quick brown fox} jumps over the lazy dog.", "The $${${animal}} jumps over the ${target}.", true);
415         doTestReplace("The ${quick brown fox} jumps over the lazy dog. ${1234567890}.", "The $${${animal}} jumps over the ${target}. $${${undefined.number:-1234567890}}.", true);
416     }
417 
418     /**
419      * Tests replace with null.
420      */
421     @Test
422     void testReplaceEmpty() {
423         doTestNoReplace("");
424     }
425 
426     /**
427      * Tests when no variable name.
428      */
429     @Test
430     void testReplaceEmptyKeys() {
431         doTestReplace("The ${} jumps over the lazy dog.", "The ${} jumps over the ${target}.", true);
432         doTestReplace("The animal jumps over the lazy dog.", "The ${:-animal} jumps over the ${target}.", true);
433     }
434 
435     /**
436      * Tests escaping.
437      */
438     @Test
439     void testReplaceEscaping() {
440         doTestReplace("The ${animal} jumps over the lazy dog.", "The $${animal} jumps over the ${target}.", true);
441     }
442 
443     /**
444      * Tests when no incomplete prefix.
445      */
446     @Test
447     void testReplaceIncompletePrefix() {
448         doTestReplace("The {animal} jumps over the lazy dog.", "The {animal} jumps over the ${target}.", true);
449     }
450 
451     /**
452      * Tests whether a variable can be replaced in a variable name.
453      */
454     @Test
455     void testReplaceInVariable() {
456         values.put("animal.1", "fox");
457         values.put("animal.2", "mouse");
458         values.put("species", "2");
459         final StrSubstitutor sub = new StrSubstitutor(values);
460         sub.setEnableSubstitutionInVariables(true);
461         assertEquals(
462                 "The mouse jumps over the lazy dog.",
463                 sub.replace("The ${animal.${species}} jumps over the ${target}."),
464                 "Wrong result (1)");
465         values.put("species", "1");
466         assertEquals(
467                 "The fox jumps over the lazy dog.",
468                 sub.replace("The ${animal.${species}} jumps over the ${target}."),
469                 "Wrong result (2)");
470         assertEquals(
471                 "The fox jumps over the lazy dog.",
472                 sub.replace("The ${unknown.animal.${unknown.species:-1}:-fox} jumps over the ${unknown.target:-lazy dog}."),
473                 "Wrong result (3)");
474     }
475 
476     /**
477      * Tests whether substitution in variable names is disabled per default.
478      */
479     @Test
480     void testReplaceInVariableDisabled() {
481         values.put("animal.1", "fox");
482         values.put("animal.2", "mouse");
483         values.put("species", "2");
484         final StrSubstitutor sub = new StrSubstitutor(values);
485         assertEquals(
486                 "The ${animal.${species}} jumps over the lazy dog.",
487                 sub.replace("The ${animal.${species}} jumps over the ${target}."),
488                 "Wrong result (1)");
489         assertEquals(
490                 "The ${animal.${species:-1}} jumps over the lazy dog.",
491                 sub.replace("The ${animal.${species:-1}} jumps over the ${target}."),
492                 "Wrong result (2)");
493     }
494 
495     /**
496      * Tests complex and recursive substitution in variable names.
497      */
498     @Test
499     void testReplaceInVariableRecursive() {
500         values.put("animal.2", "brown fox");
501         values.put("animal.1", "white mouse");
502         values.put("color", "white");
503         values.put("species.white", "1");
504         values.put("species.brown", "2");
505         final StrSubstitutor sub = new StrSubstitutor(values);
506         sub.setEnableSubstitutionInVariables(true);
507         assertEquals(
508                 "The white mouse jumps over the lazy dog.",
509                 sub.replace("The ${animal.${species.${color}}} jumps over the ${target}."),
510                 "Wrong result (1)");
511         assertEquals(
512                 "The brown fox jumps over the lazy dog.",
513                 sub.replace("The ${animal.${species.${unknownColor:-brown}}} jumps over the ${target}."),
514                 "Wrong result (2)");
515     }
516 
517     /**
518      * Tests when no prefix or suffix.
519      */
520     @Test
521     void testReplaceNoPrefixNoSuffix() {
522         doTestReplace("The animal jumps over the lazy dog.", "The animal jumps over the ${target}.", true);
523     }
524 
525     /**
526      * Tests when suffix but no prefix.
527      */
528     @Test
529     void testReplaceNoPrefixSuffix() {
530         doTestReplace("The animal} jumps over the lazy dog.", "The animal} jumps over the ${target}.", true);
531     }
532 
533     /**
534      * Tests replace with no variables.
535      */
536     @Test
537     void testReplaceNoVariables() {
538         doTestNoReplace("The balloon arrived.");
539     }
540 
541     /**
542      * Tests replace with null.
543      */
544     @Test
545     void testReplaceNull() {
546         doTestNoReplace(null);
547     }
548 
549     /**
550      * Tests simple key replace.
551      */
552     @Test
553     void testReplacePartialString_noReplace() {
554         final StrSubstitutor sub = new StrSubstitutor();
555         assertEquals("${animal} jumps", sub.replace("The ${animal} jumps over the ${target}.", 4, 15));
556     }
557 
558     /**
559      * Tests when prefix but no suffix.
560      */
561     @Test
562     void testReplacePrefixNoSuffix() {
563         doTestReplace("The ${animal jumps over the ${target} lazy dog.", "The ${animal jumps over the ${target} ${target}.", true);
564     }
565 
566     /**
567      * Tests simple recursive replace.
568      */
569     @Test
570     void testReplaceRecursive() {
571         values.put("animal", "${critter}");
572         values.put("target", "${pet}");
573         values.put("pet", "${petCharacteristic} dog");
574         values.put("petCharacteristic", "lazy");
575         values.put("critter", "${critterSpeed} ${critterColor} ${critterType}");
576         values.put("critterSpeed", "quick");
577         values.put("critterColor", "brown");
578         values.put("critterType", "fox");
579         doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
580 
581         values.put("pet", "${petCharacteristicUnknown:-lazy} dog");
582         doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
583     }
584 
585     /**
586      * Tests simple key replace.
587      */
588     @Test
589     void testReplaceSimple() {
590         doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
591     }
592 
593     /**
594      * Tests simple key replace.
595      */
596     @Test
597     void testReplaceSolo() {
598         doTestReplace("quick brown fox", "${animal}", false);
599     }
600 
601     /**
602      * Tests escaping.
603      */
604     @Test
605     void testReplaceSoloEscaping() {
606         doTestReplace("${animal}", "$${animal}", false);
607     }
608 
609     /**
610      * Tests replace creates output same as input.
611      */
612     @Test
613     void testReplaceToIdentical() {
614         values.put("animal", "$${${thing}}");
615         values.put("thing", "animal");
616         doTestReplace("The ${animal} jumps.", "The ${animal} jumps.", true);
617     }
618 
619     /**
620      * Tests unknown key replace.
621      */
622     @Test
623     void testReplaceUnknownKey() {
624         doTestReplace("The ${person} jumps over the lazy dog.", "The ${person} jumps over the ${target}.", true);
625         doTestReplace("The ${person} jumps over the lazy dog. 1234567890.", "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true);
626     }
627 
628     /**
629      * Tests interpolation with weird boundary patterns.
630      */
631     @Test
632     void testReplaceWeirdPattens() {
633         doTestNoReplace("");
634         doTestNoReplace("${}");
635         doTestNoReplace("${ }");
636         doTestNoReplace("${\t}");
637         doTestNoReplace("${\n}");
638         doTestNoReplace("${\b}");
639         doTestNoReplace("${");
640         doTestNoReplace("$}");
641         doTestNoReplace("}");
642         doTestNoReplace("${}$");
643         doTestNoReplace("${${");
644         doTestNoReplace("${${}}");
645         doTestNoReplace("${$${}}");
646         doTestNoReplace("${$$${}}");
647         doTestNoReplace("${$$${$}}");
648         doTestNoReplace("${${}}");
649         doTestNoReplace("${${ }}");
650     }
651 
652     /**
653      * Tests protected.
654      */
655     @Test
656     void testResolveVariable() {
657         final StrBuilder builder = new StrBuilder("Hi ${name}!");
658         final Map<String, String> map = new HashMap<>();
659         map.put("name", "commons");
660         final StrSubstitutor sub = new StrSubstitutor(map) {
661             @Override
662             protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
663                 assertEquals("name", variableName);
664                 assertSame(builder, buf);
665                 assertEquals(3, startPos);
666                 assertEquals(10, endPos);
667                 return "jakarta";
668             }
669         };
670         sub.replaceIn(builder);
671         assertEquals("Hi jakarta!", builder.toString());
672     }
673 
674     @Test
675     void testSamePrefixAndSuffix() {
676         final Map<String, String> map = new HashMap<>();
677         map.put("greeting", "Hello");
678         map.put(" there ", "XXX");
679         map.put("name", "commons");
680         assertEquals("Hi commons!", StrSubstitutor.replace("Hi @name@!", map, "@", "@"));
681         assertEquals("Hello there commons!", StrSubstitutor.replace("@greeting@ there @name@!", map, "@", "@"));
682     }
683 
684     /**
685      * Tests static.
686      */
687     @Test
688     void testStaticReplace() {
689         final Map<String, String> map = new HashMap<>();
690         map.put("name", "commons");
691         assertEquals("Hi commons!", StrSubstitutor.replace("Hi ${name}!", map));
692     }
693 
694     /**
695      * Tests static.
696      */
697     @Test
698     void testStaticReplacePrefixSuffix() {
699         final Map<String, String> map = new HashMap<>();
700         map.put("name", "commons");
701         assertEquals("Hi commons!", StrSubstitutor.replace("Hi <name>!", map, "<", ">"));
702     }
703 
704     /**
705      * Tests interpolation with system properties.
706      */
707     @Test
708     void testStaticReplaceSystemProperties() {
709         final StrBuilder buf = new StrBuilder();
710         buf.append("Hi ").append(SystemProperties.getUserName());
711         buf.append(", you are working with ");
712         buf.append(SystemProperties.getOsName());
713         buf.append(", your home directory is ");
714         buf.append(SystemProperties.getUserHome()).append('.');
715         assertEquals(buf.toString(), StrSubstitutor.replaceSystemProperties("Hi ${user.name}, you are "
716             + "working with ${os.name}, your home "
717             + "directory is ${user.home}."));
718     }
719 
720     /**
721      * Test the replace of a properties object
722      */
723     @Test
724     void testSubstituteDefaultProperties() {
725         final String org = "${doesnotwork}";
726         System.setProperty("doesnotwork", "It works!");
727 
728         // create a new Properties object with the System.getProperties as default
729         final Properties props = new Properties(System.getProperties());
730 
731         assertEquals("It works!", StrSubstitutor.replace(org, props));
732     }
733 
734     @Test
735     void testSubstitutePreserveEscape() {
736         final String org = "${not-escaped} $${escaped}";
737         final Map<String, String> map = new HashMap<>();
738         map.put("not-escaped", "value");
739 
740         final StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$');
741         assertFalse(sub.isPreserveEscapes());
742         assertEquals("value ${escaped}", sub.replace(org));
743 
744         sub.setPreserveEscapes(true);
745         assertTrue(sub.isPreserveEscapes());
746         assertEquals("value $${escaped}", sub.replace(org));
747     }
748 
749 }