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    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.interpol;
18  
19  import static org.hamcrest.MatcherAssert.assertThat;
20  import static org.hamcrest.Matchers.matchesPattern;
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.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNotSame;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertSame;
27  import static org.junit.jupiter.api.Assertions.assertThrows;
28  import static org.junit.jupiter.api.Assertions.assertTrue;
29  import static org.mockito.ArgumentMatchers.any;
30  import static org.mockito.Mockito.mock;
31  import static org.mockito.Mockito.verify;
32  import static org.mockito.Mockito.verifyNoInteractions;
33  import static org.mockito.Mockito.verifyNoMoreInteractions;
34  import static org.mockito.Mockito.when;
35  
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collections;
39  import java.util.EnumSet;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Objects;
46  import java.util.Properties;
47  import java.util.Set;
48  import java.util.function.Function;
49  
50  import org.apache.commons.text.lookup.StringLookupFactory;
51  import org.junit.jupiter.api.BeforeEach;
52  import org.junit.jupiter.api.Test;
53  
54  /**
55   * Test class for ConfigurationInterpolator.
56   */
57  public class TestConfigurationInterpolator {
58      /** Constant for a test variable name. */
59      private static final String TEST_NAME = "varname";
60  
61      /** Constant for a test variable prefix. */
62      private static final String TEST_PREFIX = "prefix";
63  
64      /** Constant for the value of the test variable. */
65      private static final String TEST_VALUE = "TestVariableValue";
66  
67      /**
68       * Creates a lookup object that can resolve the test variable (and nothing else).
69       *
70       * @return the test lookup object
71       */
72      private static Lookup setUpTestLookup() {
73          return setUpTestLookup(TEST_NAME, TEST_VALUE);
74      }
75  
76      /**
77       * Creates a lookup object that can resolve the specified variable (and nothing else).
78       *
79       * @param var the variable name
80       * @param value the value of this variable
81       * @return the test lookup object
82       */
83      private static Lookup setUpTestLookup(final String var, final Object value) {
84          final Lookup lookup = mock(Lookup.class);
85          when(lookup.lookup(any())).thenAnswer(invocation -> {
86              if (var.equals(invocation.getArgument(0))) {
87                  return value;
88              }
89              return null;
90          });
91          return lookup;
92      }
93  
94      /** Stores the object to be tested. */
95      private ConfigurationInterpolator interpolator;
96  
97      @BeforeEach
98      public void setUp() throws Exception {
99          interpolator = new ConfigurationInterpolator();
100     }
101 
102     /**
103      * Tests whether multiple default lookups can be added.
104      */
105     @Test
106     public void testAddDefaultLookups() {
107         final List<Lookup> lookups = new ArrayList<>();
108         lookups.add(setUpTestLookup());
109         lookups.add(setUpTestLookup("test", "value"));
110         interpolator.addDefaultLookups(lookups);
111         final List<Lookup> lookups2 = interpolator.getDefaultLookups();
112         assertEquals(lookups, lookups2);
113     }
114 
115     /**
116      * Tests whether a null collection of default lookups is handled correctly.
117      */
118     @Test
119     public void testAddDefaultLookupsNull() {
120         interpolator.addDefaultLookups(null);
121         assertTrue(interpolator.getDefaultLookups().isEmpty());
122     }
123 
124     /**
125      * Tests deregistering a lookup object.
126      */
127     @Test
128     public void testDeregisterLookup() {
129         final Lookup lookup = mock(Lookup.class);
130         interpolator.registerLookup(TEST_PREFIX, lookup);
131         assertTrue(interpolator.deregisterLookup(TEST_PREFIX));
132         assertFalse(interpolator.prefixSet().contains(TEST_PREFIX));
133         assertTrue(interpolator.getLookups().isEmpty());
134     }
135 
136     /**
137      * Tests deregistering an unknown lookup object.
138      */
139     @Test
140     public void testDeregisterLookupNonExisting() {
141         assertFalse(interpolator.deregisterLookup(TEST_PREFIX));
142     }
143 
144     /**
145      * Tests whether the flag for substitution in variable names can be modified.
146      */
147     @Test
148     public void testEnableSubstitutionInVariables() {
149         assertFalse(interpolator.isEnableSubstitutionInVariables());
150         interpolator.addDefaultLookup(setUpTestLookup("java.version", "1.4"));
151         interpolator.addDefaultLookup(setUpTestLookup("jre-1.4", "C:\\java\\1.4"));
152         final String var = "${jre-${java.version}}";
153         assertEquals(var, interpolator.interpolate(var));
154         interpolator.setEnableSubstitutionInVariables(true);
155         assertTrue(interpolator.isEnableSubstitutionInVariables());
156         assertEquals("C:\\java\\1.4", interpolator.interpolate(var));
157     }
158 
159     /**
160      * Tests fromSpecification() if the specification contains an instance.
161      */
162     @Test
163     public void testFromSpecificationInterpolator() {
164         final ConfigurationInterpolator ci = mock(ConfigurationInterpolator.class);
165         final InterpolatorSpecification spec = new InterpolatorSpecification.Builder().withDefaultLookup(mock(Lookup.class))
166             .withParentInterpolator(interpolator).withInterpolator(ci).create();
167         assertSame(ci, ConfigurationInterpolator.fromSpecification(spec));
168     }
169 
170     /**
171      * Tests fromSpecification() if a new instance has to be created.
172      */
173     @Test
174     public void testFromSpecificationNewInstance() {
175         final Lookup defLookup = mock(Lookup.class);
176         final Lookup preLookup = mock(Lookup.class);
177         final Function<Object, String> stringConverter = obj -> Objects.toString(obj, null);
178         final InterpolatorSpecification spec = new InterpolatorSpecification.Builder()
179             .withDefaultLookup(defLookup)
180             .withPrefixLookup("p", preLookup)
181             .withParentInterpolator(interpolator)
182             .withStringConverter(stringConverter)
183             .create();
184         final ConfigurationInterpolator ci = ConfigurationInterpolator.fromSpecification(spec);
185         assertEquals(Arrays.asList(defLookup), ci.getDefaultLookups());
186         assertEquals(1, ci.getLookups().size());
187         assertSame(preLookup, ci.getLookups().get("p"));
188         assertSame(interpolator, ci.getParentInterpolator());
189         assertSame(stringConverter, ci.getStringConverter());
190     }
191 
192     /**
193      * Tries to obtain an instance from a null specification.
194      */
195     @Test
196     public void testFromSpecificationNull() {
197         assertThrows(IllegalArgumentException.class, () -> ConfigurationInterpolator.fromSpecification(null));
198     }
199 
200     /**
201      * Tests whether modification of the list of default lookups does not affect the object.
202      */
203     @Test
204     public void testGetDefaultLookupsModify() {
205         final List<Lookup> lookups = interpolator.getDefaultLookups();
206         lookups.add(setUpTestLookup());
207         assertTrue(interpolator.getDefaultLookups().isEmpty());
208     }
209 
210     /**
211      * Tests whether default prefix lookups can be queried as a map.
212      */
213     @Test
214     public void testGetDefaultPrefixLookups() {
215         final EnumSet<DefaultLookups> excluded = EnumSet.of(
216                 DefaultLookups.DNS,
217                 DefaultLookups.URL,
218                 DefaultLookups.SCRIPT);
219 
220         final EnumSet<DefaultLookups> included = EnumSet.complementOf(excluded);
221 
222         final Map<String, Lookup> lookups = ConfigurationInterpolator.getDefaultPrefixLookups();
223 
224         assertEquals(included.size(), lookups.size());
225         for (final DefaultLookups l : included) {
226             assertSame(l.getLookup(), lookups.get(l.getPrefix()), "Wrong entry for " + l);
227         }
228 
229         for (final DefaultLookups l : excluded) {
230             assertNull(lookups.get(l.getPrefix()), "Unexpected entry for " + l);
231         }
232     }
233 
234     /**
235      * Tests that the map with default lookups cannot be modified.
236      */
237     @Test
238     public void testGetDefaultPrefixLookupsModify() {
239         final Map<String, Lookup> lookups = ConfigurationInterpolator.getDefaultPrefixLookups();
240         final Lookup lookup = mock(Lookup.class);
241         assertThrows(UnsupportedOperationException.class, () -> lookups.put("test", lookup));
242 
243         verifyNoInteractions(lookup);
244     }
245 
246     /**
247      * Tests that modification of the map with lookups does not affect the object.
248      */
249     @Test
250     public void testGetLookupsModify() {
251         final Map<String, Lookup> lookups = interpolator.getLookups();
252         lookups.put(TEST_PREFIX, setUpTestLookup());
253         assertTrue(interpolator.getLookups().isEmpty());
254     }
255 
256     /**
257      * Tests that a custom string converter can be used.
258      */
259     @Test
260     public void testSetStringConverter() {
261         final Function<Object, String> stringConverter = obj -> "'" + obj + "'";
262         interpolator.addDefaultLookup(setUpTestLookup("x", Arrays.asList(1, 2)));
263         interpolator.addDefaultLookup(setUpTestLookup("y", "abc"));
264         interpolator.setStringConverter(stringConverter);
265         assertSame(stringConverter, interpolator.getStringConverter());
266         assertEquals("'abc': '[1, 2]'", interpolator.interpolate("${y}: ${x}"));
267     }
268 
269     /**
270      * Tests that the default string converter can be reapplied by passing {@code null}.
271      */
272     @Test
273     public void testSetStringConverterNullArgumentUsesDefault() {
274         final Function<Object, String> stringConverter = obj -> "'" + obj + "'";
275         interpolator.addDefaultLookup(setUpTestLookup("x", Arrays.asList(1, 2)));
276         interpolator.addDefaultLookup(setUpTestLookup("y", "abc"));
277         interpolator.setStringConverter(stringConverter);
278         interpolator.setStringConverter(null);
279         assertNotSame(stringConverter, interpolator.getStringConverter());
280         assertEquals("abc: 1", interpolator.interpolate("${y}: ${x}"));
281     }
282 
283     /**
284      * Tests creating an instance. Does it contain some predefined lookups and a default string converter?
285      */
286     @Test
287     public void testInit() {
288         assertTrue(interpolator.getDefaultLookups().isEmpty());
289         assertTrue(interpolator.getLookups().isEmpty());
290         assertNull(interpolator.getParentInterpolator());
291         assertNotNull(interpolator.getStringConverter());
292         assertEquals("1", interpolator.getStringConverter().apply(Arrays.asList(1, 2)));
293     }
294 
295     /**
296      * Tests that an empty variable definition does not cause problems.
297      */
298     @Test
299     public void testInterpolateEmptyVariable() {
300         final String value = "${}";
301         assertEquals(value, interpolator.interpolate(value));
302     }
303 
304     /**
305      * Tests that a blank variable definition does not cause problems.
306      */
307     @Test
308     public void testInterpolateBlankVariable() {
309         final String value = "${ }";
310         assertEquals(value, interpolator.interpolate(value));
311     }
312 
313     /**
314      * Tests interpolation of a non string argument.
315      */
316     @Test
317     public void testInterpolateObject() {
318         final Object value = 42;
319         assertSame(value, interpolator.interpolate(value));
320     }
321 
322     /**
323      * Tests interpolation of a collection argument.
324      */
325     @Test
326     public void testInterpolateCollection() {
327         final List<Integer> value = Arrays.asList(1, 2);
328         assertSame(value, interpolator.interpolate(value));
329     }
330 
331     /**
332      * Tests interpolation of an array argument.
333      */
334     @Test
335     public void testInterpolateArray() {
336         final int[] value = {1, 2};
337         assertSame(value, interpolator.interpolate(value));
338     }
339 
340     /**
341      * Tests a successful interpolation of a string value.
342      */
343     @Test
344     public void testInterpolateString() {
345         final String value = "${" + TEST_PREFIX + ':' + TEST_NAME + "}";
346         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
347         assertEquals(TEST_VALUE, interpolator.interpolate(value));
348     }
349 
350     /**
351      * Tests interpolation with a variable which cannot be resolved.
352      */
353     @Test
354     public void testInterpolateStringUnknownVariable() {
355         final String value = "${unknownVariable}";
356         assertEquals(value, interpolator.interpolate(value));
357     }
358 
359     /**
360      * Tests a property value consisting of multiple variables.
361      */
362     @Test
363     public void testInterpolationMultipleVariables() {
364         final String value = "The ${subject} jumps over ${object}.";
365         interpolator.addDefaultLookup(setUpTestLookup("subject", "quick brown fox"));
366         interpolator.addDefaultLookup(setUpTestLookup("object", "the lazy dog"));
367         assertEquals("The quick brown fox jumps over the lazy dog.", interpolator.interpolate(value));
368     }
369 
370     /**
371      * Tests interpolation with variables containing multiple simple non-string variables.
372      */
373     @Test
374     public void testInterpolationMultipleSimpleNonStringVariables() {
375         final String value = "${x} = ${y} is ${result}";
376         interpolator.addDefaultLookup(setUpTestLookup("x", 1));
377         interpolator.addDefaultLookup(setUpTestLookup("y", 2));
378         interpolator.addDefaultLookup(setUpTestLookup("result", false));
379         assertEquals("1 = 2 is false", interpolator.interpolate(value));
380     }
381 
382     /**
383      * Tests interpolation with multiple variables containing collections and iterators.
384      */
385     @Test
386     public void testInterpolationMultipleCollectionVariables() {
387         final String value = "${single}bc${multi}23${empty}${null}${multiIt}${emptyIt}${nullIt}";
388         final List<Integer> multi = Arrays.asList(1, 0, 0);
389         final List<String> single = Arrays.asList("a");
390         final List<Object> empty = Collections.emptyList();
391         final List<Object> containsNull = Arrays.asList((Object) null);
392         interpolator.addDefaultLookup(setUpTestLookup("multi", multi));
393         interpolator.addDefaultLookup(setUpTestLookup("multiIt", multi.iterator()));
394         interpolator.addDefaultLookup(setUpTestLookup("single", single));
395         interpolator.addDefaultLookup(setUpTestLookup("empty", empty));
396         interpolator.addDefaultLookup(setUpTestLookup("emptyIt", empty.iterator()));
397         interpolator.addDefaultLookup(setUpTestLookup("null", containsNull));
398         interpolator.addDefaultLookup(setUpTestLookup("nullIt", containsNull.iterator()));
399         assertEquals("abc123${empty}${null}1${emptyIt}${nullIt}", interpolator.interpolate(value));
400     }
401 
402     /**
403      * Tests interpolation with multiple variables containing arrays.
404      */
405     @Test
406     public void testInterpolationMultipleArrayVariables() {
407         final String value = "${single}bc${multi}23${empty}${null}";
408         final int[] multi = {1, 0, 0};
409         final String[] single = {"a"};
410         final int[] empty = {};
411         final Object[] containsNull = {null};
412         interpolator.addDefaultLookup(setUpTestLookup("multi", multi));
413         interpolator.addDefaultLookup(setUpTestLookup("single", single));
414         interpolator.addDefaultLookup(setUpTestLookup("empty", empty));
415         interpolator.addDefaultLookup(setUpTestLookup("null", containsNull));
416         assertEquals("abc123${empty}${null}", interpolator.interpolate(value));
417     }
418 
419     /**
420      * Tests an interpolation that consists of a single variable only. The variable's value should be returned verbatim.
421      */
422     @Test
423     public void testInterpolationSingleVariable() {
424         final Object value = 42;
425         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
426         assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
427     }
428 
429     /**
430      * Tests an interpolation that consists of a single collection variable only. The variable's value
431      * should be returned verbatim.
432      */
433     @Test
434     public void testInterpolationSingleCollectionVariable() {
435         final List<Integer> value = Arrays.asList(42);
436         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
437         assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
438     }
439 
440     /**
441      * Tests an interpolation that consists of a single array variable only. The variable's value
442      * should be returned verbatim.
443      */
444     @Test
445     public void testInterpolationSingleArrayVariable() {
446         final int[] value = {42, -1};
447         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
448         assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
449     }
450 
451     /**
452      * Tests an interpolation that consists of a single undefined variable only with and without a default value.
453      */
454     @Test
455     public void testInterpolationSingleVariableDefaultValue() {
456         final Object value = 42;
457         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
458         assertEquals("${I_am_not_defined}", interpolator.interpolate("${I_am_not_defined}"));
459         assertEquals("42", interpolator.interpolate("${I_am_not_defined:-42}"));
460         assertEquals("", interpolator.interpolate("${I_am_not_defined:-}"));
461     }
462 
463     /**
464      * Tests a variable declaration which lacks the trailing closing bracket.
465      */
466     @Test
467     public void testInterpolationVariableIncomplete() {
468         final String value = "${" + TEST_NAME;
469         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, "someValue"));
470         assertEquals(value, interpolator.interpolate(value));
471     }
472 
473     /**
474      * Tests an interpolated string that begins and ends with variable lookups that have
475      * the potential to fail. Part of CONFIGURATION-764.
476      */
477     @Test
478     public void testInterpolationBeginningAndEndingRiskyVariableLookups() {
479         interpolator.registerLookups(ConfigurationInterpolator.getDefaultPrefixLookups());
480         final String result = (String) interpolator.interpolate("${date:yyyy-MM}-${date:dd}");
481         assertThat(result, matchesPattern("\\d{4}-\\d{2}-\\d{2}"));
482     }
483 
484     /**
485      * Tests nullSafeLookup() if a lookup object was provided.
486      */
487     @Test
488     public void testNullSafeLookupExisting() {
489         final Lookup look = mock(Lookup.class);
490         assertSame(look, ConfigurationInterpolator.nullSafeLookup(look));
491     }
492 
493     /**
494      * Tests whether nullSafeLookup() can handle null input.
495      */
496     @Test
497     public void testNullSafeLookupNull() {
498         final Lookup lookup = ConfigurationInterpolator.nullSafeLookup(null);
499         assertNull(lookup.lookup("someVar"));
500     }
501 
502     /**
503      * Tests that the prefix set cannot be modified.
504      */
505     @Test
506     public void testPrefixSetModify() {
507         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
508         final Iterator<String> it = interpolator.prefixSet().iterator();
509         it.next();
510         assertThrows(UnsupportedOperationException.class, it::remove);
511     }
512 
513     /**
514      * Tests registering a lookup object at an instance.
515      */
516     @Test
517     public void testRegisterLookup() {
518         final Lookup lookup = mock(Lookup.class);
519         interpolator.registerLookup(TEST_PREFIX, lookup);
520         assertSame(lookup, interpolator.getLookups().get(TEST_PREFIX));
521         assertTrue(interpolator.prefixSet().contains(TEST_PREFIX));
522         assertTrue(interpolator.getDefaultLookups().isEmpty());
523     }
524 
525     /**
526      * Tests registering a null lookup object. This should cause an exception.
527      */
528     @Test
529     public void testRegisterLookupNull() {
530         assertThrows(IllegalArgumentException.class, () -> interpolator.registerLookup(TEST_PREFIX, null));
531     }
532 
533     /**
534      * Tests registering a lookup object for an undefined prefix. This should cause an exception.
535      */
536     @Test
537     public void testRegisterLookupNullPrefix() {
538         final Lookup lookup = mock(Lookup.class);
539         assertThrows(IllegalArgumentException.class, () -> interpolator.registerLookup(null, lookup));
540 
541         verifyNoInteractions(lookup);
542     }
543 
544     /**
545      * Tests whether a map with lookup objects can be registered.
546      */
547     @Test
548     public void testRegisterLookups() {
549         final Lookup l1 = setUpTestLookup();
550         final Lookup l2 = setUpTestLookup("someVar", "someValue");
551         final Map<String, Lookup> lookups = new HashMap<>();
552         lookups.put(TEST_PREFIX, l1);
553         final String prefix2 = TEST_PREFIX + "_other";
554         lookups.put(prefix2, l2);
555         interpolator.registerLookups(lookups);
556         final Map<String, Lookup> lookups2 = interpolator.getLookups();
557 
558         final Map<String, Lookup> expected = new HashMap<>();
559         expected.put(TEST_PREFIX, l1);
560         expected.put(prefix2, l2);
561         assertEquals(expected, lookups2);
562     }
563 
564     /**
565      * Tests whether a null map with lookup objects is handled correctly.
566      */
567     @Test
568     public void testRegisterLookupsNull() {
569         interpolator.registerLookups(null);
570         assertTrue(interpolator.getLookups().isEmpty());
571     }
572 
573     /**
574      * Tests whether a default lookup object can be removed.
575      */
576     @Test
577     public void testRemoveDefaultLookup() {
578         final List<Lookup> lookups = new ArrayList<>();
579         lookups.add(setUpTestLookup());
580         lookups.add(setUpTestLookup("test", "value"));
581         interpolator.addDefaultLookups(lookups);
582         assertTrue(interpolator.removeDefaultLookup(lookups.get(0)));
583         assertFalse(interpolator.getDefaultLookups().contains(lookups.get(0)));
584         assertEquals(1, interpolator.getDefaultLookups().size());
585     }
586 
587     /**
588      * Tests whether a non existing default lookup object can be removed.
589      */
590     @Test
591     public void testRemoveDefaultLookupNonExisting() {
592         assertFalse(interpolator.removeDefaultLookup(setUpTestLookup()));
593     }
594 
595     /**
596      * Tests looking up a variable without a prefix. This should trigger the default lookup object.
597      */
598     @Test
599     public void testResolveDefault() {
600         final Lookup l1 = mock(Lookup.class);
601         final Lookup l2 = mock(Lookup.class);
602         final Lookup l3 = mock(Lookup.class);
603 
604         when(l1.lookup(TEST_NAME)).thenReturn(null);
605         when(l2.lookup(TEST_NAME)).thenReturn(TEST_VALUE);
606 
607         interpolator.addDefaultLookups(Arrays.asList(l1, l2, l3));
608         assertEquals(TEST_VALUE, interpolator.resolve(TEST_NAME));
609 
610         verify(l1).lookup(TEST_NAME);
611         verify(l2).lookup(TEST_NAME);
612         verifyNoMoreInteractions(l1, l2, l3);
613     }
614 
615     /**
616      * Tests whether the default lookup is called for variables with a prefix when the lookup that was registered for this
617      * prefix is not able to resolve the variable.
618      */
619     @Test
620     public void testResolveDefaultAfterPrefixFails() {
621         final String varName = TEST_PREFIX + ':' + TEST_NAME + "2";
622         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
623         interpolator.addDefaultLookup(setUpTestLookup(varName, TEST_VALUE));
624         assertEquals(TEST_VALUE, interpolator.resolve(varName));
625     }
626 
627     /**
628      * Tests an empty variable name without a prefix.
629      */
630     @Test
631     public void testResolveDefaultEmptyVarName() {
632         interpolator.addDefaultLookup(setUpTestLookup("", TEST_VALUE));
633         assertEquals(TEST_VALUE, interpolator.resolve(""));
634     }
635 
636     /**
637      * Tests the empty variable prefix. This is a special case, but legal.
638      */
639     @Test
640     public void testResolveEmptyPrefix() {
641         interpolator.registerLookup("", setUpTestLookup());
642         assertEquals(TEST_VALUE, interpolator.resolve(":" + TEST_NAME));
643     }
644 
645     /**
646      * Tests an empty variable name.
647      */
648     @Test
649     public void testResolveEmptyVarName() {
650         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup("", TEST_VALUE));
651         assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ":"));
652     }
653 
654     /**
655      * Tests looking up a variable without a prefix when no default lookup is specified. Result should be null in this case.
656      */
657     @Test
658     public void testResolveNoDefault() {
659         assertNull(interpolator.resolve(TEST_NAME));
660     }
661 
662     /**
663      * Tests looking up a null variable. Result should be null, too.
664      */
665     @Test
666     public void testResolveNull() {
667         assertNull(interpolator.resolve(null));
668     }
669 
670     /**
671      * Tests handling of a parent {@code ConfigurationInterpolator} if the variable can already be resolved by the current
672      * instance.
673      */
674     @Test
675     public void testResolveParentVariableFound() {
676         final ConfigurationInterpolator parent = mock(ConfigurationInterpolator.class);
677         interpolator.setParentInterpolator(parent);
678         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
679         assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ':' + TEST_NAME));
680     }
681 
682     /**
683      * Tests whether the parent {@code ConfigurationInterpolator} is invoked if the test instance cannot resolve a variable.
684      */
685     @Test
686     public void testResolveParentVariableNotFound() {
687         final ConfigurationInterpolator parent = mock(ConfigurationInterpolator.class);
688 
689         when(parent.resolve(TEST_NAME)).thenReturn(TEST_VALUE);
690 
691         interpolator.setParentInterpolator(parent);
692         assertEquals(TEST_VALUE, interpolator.resolve(TEST_NAME));
693 
694         verify(parent).resolve(TEST_NAME);
695         verifyNoMoreInteractions(parent);
696     }
697 
698     /**
699      * Tests whether a variable can be resolved using the associated lookup object. The lookup is identified by the
700      * variable's prefix.
701      */
702     @Test
703     public void testResolveWithPrefix() {
704         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
705         assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ':' + TEST_NAME));
706     }
707 
708     /**
709      * Tests the behavior of the lookup method for variables with an unknown prefix. These variables should not be resolved.
710      */
711     @Test
712     public void testResolveWithUnknownPrefix() {
713         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
714         assertNull(interpolator.resolve("UnknownPrefix:" + TEST_NAME));
715         assertNull(interpolator.resolve(":" + TEST_NAME));
716     }
717 
718     @Test
719     public void testDefaultStringLookupsHolder_lookupsPropertyNotPresent() {
720         checkDefaultPrefixLookupsHolder(new Properties(),
721                 "base64",
722                 StringLookupFactory.KEY_BASE64_DECODER,
723                 StringLookupFactory.KEY_BASE64_ENCODER,
724                 StringLookupFactory.KEY_CONST,
725                 StringLookupFactory.KEY_DATE,
726                 StringLookupFactory.KEY_ENV,
727                 StringLookupFactory.KEY_FILE,
728                 StringLookupFactory.KEY_JAVA,
729                 StringLookupFactory.KEY_LOCALHOST,
730                 StringLookupFactory.KEY_PROPERTIES,
731                 StringLookupFactory.KEY_RESOURCE_BUNDLE,
732                 StringLookupFactory.KEY_SYS,
733                 StringLookupFactory.KEY_URL_DECODER,
734                 StringLookupFactory.KEY_URL_ENCODER,
735                 StringLookupFactory.KEY_XML);
736     }
737 
738     @Test
739     public void testDefaultStringLookupsHolder_lookupsPropertyEmptyAndBlank() {
740         final Properties propsWithNull = new Properties();
741         propsWithNull.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "");
742 
743         checkDefaultPrefixLookupsHolder(propsWithNull);
744 
745         final Properties propsWithBlank = new Properties();
746         propsWithBlank.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, " ");
747 
748         checkDefaultPrefixLookupsHolder(propsWithBlank);
749     }
750 
751     @Test
752     public void testDefaultStringLookupsHolder_givenSingleLookup() {
753         final Properties props = new Properties();
754         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "base64_encoder");
755 
756         checkDefaultPrefixLookupsHolder(props,
757                 "base64",
758                 StringLookupFactory.KEY_BASE64_ENCODER);
759     }
760 
761     @Test
762     public void testDefaultStringLookupsHolder_givenSingleLookup_weirdString() {
763         final Properties props = new Properties();
764         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, " \n \t  ,, DnS , , ");
765 
766         checkDefaultPrefixLookupsHolder(props, StringLookupFactory.KEY_DNS);
767     }
768 
769     @Test
770     public void testDefaultStringLookupsHolder_multipleLookups() {
771         final Properties props = new Properties();
772         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "dns, url script ");
773 
774         checkDefaultPrefixLookupsHolder(props,
775                 StringLookupFactory.KEY_DNS,
776                 StringLookupFactory.KEY_URL,
777                 StringLookupFactory.KEY_SCRIPT);
778     }
779 
780     @Test
781     public void testDefaultStringLookupsHolder_allLookups() {
782         final Properties props = new Properties();
783         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY,
784                 "BASE64_DECODER BASE64_ENCODER const, date, dns, environment "
785                 + "file ,java, local_host properties, resource_bundle,script,system_properties "
786                 + "url url_decoder  , url_encoder, xml");
787 
788         checkDefaultPrefixLookupsHolder(props,
789                 "base64",
790                 StringLookupFactory.KEY_BASE64_DECODER,
791                 StringLookupFactory.KEY_BASE64_ENCODER,
792                 StringLookupFactory.KEY_CONST,
793                 StringLookupFactory.KEY_DATE,
794                 StringLookupFactory.KEY_ENV,
795                 StringLookupFactory.KEY_FILE,
796                 StringLookupFactory.KEY_JAVA,
797                 StringLookupFactory.KEY_LOCALHOST,
798                 StringLookupFactory.KEY_PROPERTIES,
799                 StringLookupFactory.KEY_RESOURCE_BUNDLE,
800                 StringLookupFactory.KEY_SYS,
801                 StringLookupFactory.KEY_URL_DECODER,
802                 StringLookupFactory.KEY_URL_ENCODER,
803                 StringLookupFactory.KEY_XML,
804 
805                 StringLookupFactory.KEY_DNS,
806                 StringLookupFactory.KEY_URL,
807                 StringLookupFactory.KEY_SCRIPT);
808     }
809 
810     @Test
811     public void testDefaultStringLookupsHolder_invalidLookupsDefinition() {
812         final Properties props = new Properties();
813         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "base64_encoder nope");
814 
815         final Exception exc = assertThrows(Exception.class, () -> new ConfigurationInterpolator.DefaultPrefixLookupsHolder(props));
816         assertEquals("Invalid default lookups definition: base64_encoder nope", exc.getMessage());
817     }
818 
819     private static void checkDefaultPrefixLookupsHolder(final Properties props, final String... keys) {
820         final ConfigurationInterpolator.DefaultPrefixLookupsHolder holder =
821                 new ConfigurationInterpolator.DefaultPrefixLookupsHolder(props);
822 
823         final Map<String, Lookup> lookupMap = holder.getDefaultPrefixLookups();
824 
825         assertMappedLookups(lookupMap, keys);
826     }
827 
828     private static void assertMappedLookups(final Map<String, Lookup> lookupMap, final String... keys) {
829         final Set<String> remainingKeys = new HashSet<>(lookupMap.keySet());
830 
831         for (final String key : keys) {
832             assertNotNull(key, "Expected map to contain string lookup for key " + key);
833 
834             remainingKeys.remove(key);
835         }
836 
837         assertEquals(Collections.emptySet(), remainingKeys);
838     }
839 
840     /**
841      * Main method used to verify the default lookups resolved during JVM execution.
842      * @param args
843      */
844     public static void main(final String[] args) {
845         System.out.println("Default lookups");
846         for (final String key : ConfigurationInterpolator.getDefaultPrefixLookups().keySet()) {
847             System.out.println("- " + key);
848         }
849     }
850 }