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 package org.apache.commons.configuration2.interpol;
18
19 import java.lang.reflect.Array;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Properties;
29 import java.util.Set;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.CopyOnWriteArrayList;
32 import java.util.function.Function;
33
34 import org.apache.commons.text.StringSubstitutor;
35
36 /**
37 * <p>
38 * A class that handles interpolation (variable substitution) for configuration objects.
39 * </p>
40 * <p>
41 * Each instance of {@code AbstractConfiguration} is associated with an object of this class. All interpolation tasks
42 * are delegated to this object.
43 * </p>
44 * <p>
45 * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor} class from
46 * <a href="https://commons.apache.org/text">Commons Text</a>. Thus it supports the same syntax of variable expressions.
47 * </p>
48 * <p>
49 * The basic idea of this class is that it can maintain a set of primitive {@link Lookup} objects, each of which is
50 * identified by a special prefix. The variables to be processed have the form {@code ${prefix:name}}.
51 * {@code ConfigurationInterpolator} will extract the prefix and determine, which primitive lookup object is registered
52 * for it. Then the name of the variable is passed to this object to obtain the actual value. It is also possible to
53 * define an arbitrary number of default lookup objects, which are used for variables that do not have a prefix or that
54 * cannot be resolved by their associated lookup object. When adding default lookup objects their order matters; they
55 * are queried in this order, and the first non-<strong>null</strong> variable value is used.
56 * </p>
57 * <p>
58 * After an instance has been created it does not contain any {@code Lookup} objects. The current set of lookup objects
59 * can be modified using the {@code registerLookup()} and {@code deregisterLookup()} methods. Default lookup objects
60 * (that are invoked for variables without a prefix) can be added or removed with the {@code addDefaultLookup()} and
61 * {@code removeDefaultLookup()} methods respectively. (When a {@code ConfigurationInterpolator} instance is created by
62 * a configuration object, a default lookup object is added pointing to the configuration itself, so that variables are
63 * resolved using the configuration's properties.)
64 * </p>
65 * <p>
66 * The default usage scenario is that on a fully initialized instance the {@code interpolate()} method is called. It is
67 * passed an object value which may contain variables. All these variables are substituted if they can be resolved. The
68 * result is the passed in value with variables replaced. Alternatively, the {@code resolve()} method can be called to
69 * obtain the values of specific variables without performing interpolation.
70 * </p>
71 * <p><strong>String Conversion</strong></p>
72 * <p>
73 * When variables are part of larger interpolated strings, the variable values, which can be of any type, must be
74 * converted to strings to produce the full result. Each interpolator instance has a configurable
75 * {@link #setStringConverter(Function) string converter} to perform this conversion. The default implementation of this
76 * function simply uses the value's {@code toString} method in the majority of cases. However, for maximum
77 * consistency with
78 * {@link org.apache.commons.configuration2.convert.DefaultConversionHandler DefaultConversionHandler}, when a variable
79 * value is a container type (such as a collection or array), then only the first element of the container is converted
80 * to a string instead of the container itself. For example, if the variable {@code x} resolves to the integer array
81 * {@code [1, 2, 3]}, then the string {@code "my value = ${x}"} will by default be interpolated to
82 * {@code "my value = 1"}.
83 * </p>
84 * <p>
85 * <strong>Implementation note:</strong> This class is thread-safe. Lookup objects can be added or removed at any time
86 * concurrent to interpolation operations.
87 * </p>
88 *
89 * @since 1.4
90 */
91 public class ConfigurationInterpolator {
92
93 /**
94 * Internal class used to construct the default {@link Lookup} map used by
95 * {@link ConfigurationInterpolator#getDefaultPrefixLookups()}.
96 */
97 static final class DefaultPrefixLookupsHolder {
98
99 /** Singleton instance, initialized with the system properties. */
100 static final DefaultPrefixLookupsHolder INSTANCE = new DefaultPrefixLookupsHolder(System.getProperties());
101
102 /**
103 * Add the prefix and lookup from {@code lookup} to {@code map}.
104 *
105 * @param lookup lookup to add
106 * @param map map to add to
107 */
108 private static void addLookup(final DefaultLookups lookup, final Map<String, Lookup> map) {
109 map.put(lookup.getPrefix(), lookup.getLookup());
110 }
111
112 /**
113 * Create the lookup map used when the user has requested no customization.
114 *
115 * @return default lookup map
116 */
117 private static Map<String, Lookup> createDefaultLookups() {
118 final Map<String, Lookup> lookupMap = new HashMap<>();
119
120 addLookup(DefaultLookups.BASE64_DECODER, lookupMap);
121 addLookup(DefaultLookups.BASE64_ENCODER, lookupMap);
122 addLookup(DefaultLookups.CONST, lookupMap);
123 addLookup(DefaultLookups.DATE, lookupMap);
124 addLookup(DefaultLookups.ENVIRONMENT, lookupMap);
125 addLookup(DefaultLookups.FILE, lookupMap);
126 addLookup(DefaultLookups.JAVA, lookupMap);
127 addLookup(DefaultLookups.LOCAL_HOST, lookupMap);
128 addLookup(DefaultLookups.PROPERTIES, lookupMap);
129 addLookup(DefaultLookups.RESOURCE_BUNDLE, lookupMap);
130 addLookup(DefaultLookups.SYSTEM_PROPERTIES, lookupMap);
131 addLookup(DefaultLookups.URL_DECODER, lookupMap);
132 addLookup(DefaultLookups.URL_ENCODER, lookupMap);
133 addLookup(DefaultLookups.XML, lookupMap);
134
135 return lookupMap;
136 }
137
138 /**
139 * Constructs a lookup map by parsing the given string. The string is expected to contain
140 * comma or space-separated names of values from the {@link DefaultLookups} enum.
141 *
142 * @param str string to parse; not null
143 * @return lookup map parsed from the given string
144 * @throws IllegalArgumentException if the string does not contain a valid default lookup
145 * definition
146 */
147 private static Map<String, Lookup> parseLookups(final String str) {
148 final Map<String, Lookup> lookupMap = new HashMap<>();
149
150 try {
151 for (final String lookupName : str.split("[\\s,]+")) {
152 if (!lookupName.isEmpty()) {
153 addLookup(DefaultLookups.valueOf(lookupName.toUpperCase()), lookupMap);
154 }
155 }
156 } catch (final IllegalArgumentException exc) {
157 throw new IllegalArgumentException("Invalid default lookups definition: " + str, exc);
158 }
159
160 return lookupMap;
161 }
162
163 /** Default lookup map. */
164 private final Map<String, Lookup> defaultLookups;
165
166 /**
167 * Constructs a new instance initialized with the given properties.
168 *
169 * @param props initialization properties
170 */
171 DefaultPrefixLookupsHolder(final Properties props) {
172 final Map<String, Lookup> lookups = props.containsKey(DEFAULT_PREFIX_LOOKUPS_PROPERTY)
173 ? parseLookups(props.getProperty(DEFAULT_PREFIX_LOOKUPS_PROPERTY))
174 : createDefaultLookups();
175
176 defaultLookups = Collections.unmodifiableMap(lookups);
177 }
178
179 /**
180 * Gets the default prefix lookups map.
181 *
182 * @return default prefix lookups map
183 */
184 Map<String, Lookup> getDefaultPrefixLookups() {
185 return defaultLookups;
186 }
187 }
188
189 /** Class encapsulating the default logic to convert resolved variable values into strings.
190 * This class is thread-safe.
191 */
192 private static final class DefaultStringConverter implements Function<Object, String> {
193
194 /** Shared instance. */
195 static final DefaultStringConverter INSTANCE = new DefaultStringConverter();
196
197 /** {@inheritDoc} */
198 @Override
199 public String apply(final Object obj) {
200 return Objects.toString(extractSimpleValue(obj), null);
201 }
202
203 /** Attempt to extract a simple value from {@code obj} for use in string conversion.
204 * If the input represents a collection of some sort (for example, an iterable or array),
205 * the first item from the collection is returned.
206 *
207 * @param obj input object
208 * @return extracted simple object
209 */
210 private Object extractSimpleValue(final Object obj) {
211 if (!(obj instanceof String)) {
212 if (obj instanceof Iterable) {
213 return nextOrNull(((Iterable<?>) obj).iterator());
214 }
215 if (obj instanceof Iterator) {
216 return nextOrNull((Iterator<?>) obj);
217 }
218 if (obj.getClass().isArray()) {
219 return Array.getLength(obj) > 0
220 ? Array.get(obj, 0)
221 : null;
222 }
223 }
224 return obj;
225 }
226
227 /** Return the next value from {@code it} or {@code null} if no values remain.
228 * @param <T> iterated type
229 * @param it iterator
230 * @return next value from {@code it} or {@code null} if no values remain
231 */
232 private <T> T nextOrNull(final Iterator<T> it) {
233 return it.hasNext()
234 ? it.next()
235 : null;
236 }
237 }
238
239 /**
240 * Name of the system property used to determine the lookups added by the
241 * {@link #getDefaultPrefixLookups()} method. Use of this property is only required
242 * in cases where the set of default lookups must be modified.
243 *
244 * @since 2.8.0
245 */
246 public static final String DEFAULT_PREFIX_LOOKUPS_PROPERTY =
247 "org.apache.commons.configuration2.interpol.ConfigurationInterpolator.defaultPrefixLookups";
248
249 /** Constant for the prefix separator. */
250 private static final char PREFIX_SEPARATOR = ':';
251
252 /** The variable prefix. */
253 private static final String VAR_START = "${";
254
255 /** The length of {@link #VAR_START}. */
256 private static final int VAR_START_LENGTH = VAR_START.length();
257
258 /** The variable suffix. */
259 private static final String VAR_END = "}";
260
261 /** The length of {@link #VAR_END}. */
262 private static final int VAR_END_LENGTH = VAR_END.length();
263
264 /**
265 * Creates a new instance based on the properties in the given specification object.
266 *
267 * @param spec the {@code InterpolatorSpecification}
268 * @return the newly created instance
269 */
270 private static ConfigurationInterpolator createInterpolator(final InterpolatorSpecification spec) {
271 final ConfigurationInterpolator ci = new ConfigurationInterpolator();
272 ci.addDefaultLookups(spec.getDefaultLookups());
273 ci.registerLookups(spec.getPrefixLookups());
274 ci.setParentInterpolator(spec.getParentInterpolator());
275 ci.setStringConverter(spec.getStringConverter());
276 return ci;
277 }
278
279 /**
280 * Extracts the variable name from a value that consists of a single variable.
281 *
282 * @param strValue the value
283 * @return the extracted variable name
284 */
285 private static String extractVariableName(final String strValue) {
286 return strValue.substring(VAR_START_LENGTH, strValue.length() - VAR_END_LENGTH);
287 }
288
289 /**
290 * Creates a new {@code ConfigurationInterpolator} instance based on the passed in specification object. If the
291 * {@code InterpolatorSpecification} already contains a {@code ConfigurationInterpolator} object, it is used directly.
292 * Otherwise, a new instance is created and initialized with the properties stored in the specification.
293 *
294 * @param spec the {@code InterpolatorSpecification} (must not be <strong>null</strong>)
295 * @return the {@code ConfigurationInterpolator} obtained or created based on the given specification
296 * @throws IllegalArgumentException if the specification is <strong>null</strong>
297 * @since 2.0
298 */
299 public static ConfigurationInterpolator fromSpecification(final InterpolatorSpecification spec) {
300 if (spec == null) {
301 throw new IllegalArgumentException("InterpolatorSpecification must not be null!");
302 }
303 return spec.getInterpolator() != null ? spec.getInterpolator() : createInterpolator(spec);
304 }
305
306 /**
307 * Gets a map containing the default prefix lookups. Every configuration object derived from
308 * {@code AbstractConfiguration} is by default initialized with a {@code ConfigurationInterpolator} containing
309 * these {@code Lookup} objects and their prefixes. The map cannot be modified.
310 *
311 * <p>
312 * All of the lookups present in the returned map are from {@link DefaultLookups}. However, not all of the
313 * available lookups are included by default. Specifically, lookups that can execute code (for example,
314 * {@link DefaultLookups#SCRIPT SCRIPT}) and those that can result in contact with remote servers (for example,
315 * {@link DefaultLookups#URL URL} and {@link DefaultLookups#DNS DNS}) are not included. If this behavior
316 * must be modified, users can define the {@value #DEFAULT_PREFIX_LOOKUPS_PROPERTY} system property
317 * with a comma-separated list of {@link DefaultLookups} enum names to be included in the set of defaults.
318 * For example, setting this system property to {@code "BASE64_ENCODER,ENVIRONMENT"} will only include the
319 * {@link DefaultLookups#BASE64_ENCODER BASE64_ENCODER} and
320 * {@link DefaultLookups#ENVIRONMENT ENVIRONMENT} lookups. Setting the property to the empty string will
321 * cause no defaults to be configured.
322 * </p>
323 *
324 * <table>
325 * <caption>Default Lookups</caption>
326 * <tr>
327 * <th>Prefix</th>
328 * <th>Lookup</th>
329 * </tr>
330 * <tr>
331 * <td>"base64Decoder"</td>
332 * <td>{@link DefaultLookups#BASE64_DECODER BASE64_DECODER}</td>
333 * </tr>
334 * <tr>
335 * <td>"base64Encoder"</td>
336 * <td>{@link DefaultLookups#BASE64_ENCODER BASE64_ENCODER}</td>
337 * </tr>
338 * <tr>
339 * <td>"const"</td>
340 * <td>{@link DefaultLookups#CONST CONST}</td>
341 * </tr>
342 * <tr>
343 * <td>"date"</td>
344 * <td>{@link DefaultLookups#DATE DATE}</td>
345 * </tr>
346 * <tr>
347 * <td>"env"</td>
348 * <td>{@link DefaultLookups#ENVIRONMENT ENVIRONMENT}</td>
349 * </tr>
350 * <tr>
351 * <td>"file"</td>
352 * <td>{@link DefaultLookups#FILE FILE}</td>
353 * </tr>
354 * <tr>
355 * <td>"java"</td>
356 * <td>{@link DefaultLookups#JAVA JAVA}</td>
357 * </tr>
358 * <tr>
359 * <td>"localhost"</td>
360 * <td>{@link DefaultLookups#LOCAL_HOST LOCAL_HOST}</td>
361 * </tr>
362 * <tr>
363 * <td>"properties"</td>
364 * <td>{@link DefaultLookups#PROPERTIES PROPERTIES}</td>
365 * </tr>
366 * <tr>
367 * <td>"resourceBundle"</td>
368 * <td>{@link DefaultLookups#RESOURCE_BUNDLE RESOURCE_BUNDLE}</td>
369 * </tr>
370 * <tr>
371 * <td>"sys"</td>
372 * <td>{@link DefaultLookups#SYSTEM_PROPERTIES SYSTEM_PROPERTIES}</td>
373 * </tr>
374 * <tr>
375 * <td>"urlDecoder"</td>
376 * <td>{@link DefaultLookups#URL_DECODER URL_DECODER}</td>
377 * </tr>
378 * <tr>
379 * <td>"urlEncoder"</td>
380 * <td>{@link DefaultLookups#URL_ENCODER URL_ENCODER}</td>
381 * </tr>
382 * <tr>
383 * <td>"xml"</td>
384 * <td>{@link DefaultLookups#XML XML}</td>
385 * </tr>
386 * </table>
387 *
388 * <table>
389 * <caption>Additional Lookups (not included by default)</caption>
390 * <tr>
391 * <th>Prefix</th>
392 * <th>Lookup</th>
393 * </tr>
394 * <tr>
395 * <td>"dns"</td>
396 * <td>{@link DefaultLookups#DNS DNS}</td>
397 * </tr>
398 * <tr>
399 * <td>"url"</td>
400 * <td>{@link DefaultLookups#URL URL}</td>
401 * </tr>
402 * <tr>
403 * <td>"script"</td>
404 * <td>{@link DefaultLookups#SCRIPT SCRIPT}</td>
405 * </tr>
406 * </table>
407 *
408 * @return a map with the default prefix {@code Lookup} objects and their prefixes
409 * @since 2.0
410 */
411 public static Map<String, Lookup> getDefaultPrefixLookups() {
412 return DefaultPrefixLookupsHolder.INSTANCE.getDefaultPrefixLookups();
413 }
414
415 /**
416 * Utility method for obtaining a {@code Lookup} object in a safe way. This method always returns a non-<strong>null</strong>
417 * {@code Lookup} object. If the passed in {@code Lookup} is not <strong>null</strong>, it is directly returned. Otherwise, result
418 * is a dummy {@code Lookup} which does not provide any values.
419 *
420 * @param lookup the {@code Lookup} to check
421 * @return a non-<strong>null</strong> {@code Lookup} object
422 * @since 2.0
423 */
424 public static Lookup nullSafeLookup(Lookup lookup) {
425 if (lookup == null) {
426 lookup = DummyLookup.INSTANCE;
427 }
428 return lookup;
429 }
430
431 /** A map with the currently registered lookup objects. */
432 private final Map<String, Lookup> prefixLookups;
433
434 /** Stores the default lookup objects. */
435 private final List<Lookup> defaultLookups;
436
437 /** The helper object performing variable substitution. */
438 private final StringSubstitutor substitutor;
439
440 /** Stores a parent interpolator objects if the interpolator is nested hierarchically. */
441 private volatile ConfigurationInterpolator parentInterpolator;
442
443 /** Function used to convert interpolated values to strings. */
444 private volatile Function<Object, String> stringConverter = DefaultStringConverter.INSTANCE;
445
446 /**
447 * Creates a new instance of {@code ConfigurationInterpolator}.
448 */
449 public ConfigurationInterpolator() {
450 prefixLookups = new ConcurrentHashMap<>();
451 defaultLookups = new CopyOnWriteArrayList<>();
452 substitutor = initSubstitutor();
453 }
454
455 /**
456 * Adds a default {@code Lookup} object. Default {@code Lookup} objects are queried (in the order they were added) for
457 * all variables without a special prefix. If no default {@code Lookup} objects are present, such variables won't be
458 * processed.
459 *
460 * @param defaultLookup the default {@code Lookup} object to be added (must not be <strong>null</strong>)
461 * @throws IllegalArgumentException if the {@code Lookup} object is <strong>null</strong>
462 */
463 public void addDefaultLookup(final Lookup defaultLookup) {
464 defaultLookups.add(defaultLookup);
465 }
466
467 /**
468 * Adds all {@code Lookup} objects in the given collection as default lookups. The collection can be <strong>null</strong>, then
469 * this method has no effect. It must not contain <strong>null</strong> entries.
470 *
471 * @param lookups the {@code Lookup} objects to be added as default lookups
472 * @throws IllegalArgumentException if the collection contains a <strong>null</strong> entry
473 */
474 public void addDefaultLookups(final Collection<? extends Lookup> lookups) {
475 if (lookups != null) {
476 defaultLookups.addAll(lookups);
477 }
478 }
479
480 /**
481 * Deregisters the {@code Lookup} object for the specified prefix at this instance. It will be removed from this
482 * instance.
483 *
484 * @param prefix the variable prefix
485 * @return a flag whether for this prefix a lookup object had been registered
486 */
487 public boolean deregisterLookup(final String prefix) {
488 return prefixLookups.remove(prefix) != null;
489 }
490
491 /**
492 * Obtains the lookup object for the specified prefix. This method is called by the {@code lookup()} method. This
493 * implementation will check whether a lookup object is registered for the given prefix. If not, a <strong>null</strong> lookup
494 * object will be returned (never <strong>null</strong>).
495 *
496 * @param prefix the prefix
497 * @return the lookup object to be used for this prefix
498 */
499 protected Lookup fetchLookupForPrefix(final String prefix) {
500 return nullSafeLookup(prefixLookups.get(prefix));
501 }
502
503 /**
504 * Gets a collection with the default {@code Lookup} objects added to this {@code ConfigurationInterpolator}. These
505 * objects are not associated with a variable prefix. The returned list is a snapshot copy of the internal collection of
506 * default lookups; so manipulating it does not affect this instance.
507 *
508 * @return the default lookup objects
509 */
510 public List<Lookup> getDefaultLookups() {
511 return new ArrayList<>(defaultLookups);
512 }
513
514 /**
515 * Gets a map with the currently registered {@code Lookup} objects and their prefixes. This is a snapshot copy of the
516 * internally used map. So modifications of this map do not effect this instance.
517 *
518 * @return a copy of the map with the currently registered {@code Lookup} objects
519 */
520 public Map<String, Lookup> getLookups() {
521 return new HashMap<>(prefixLookups);
522 }
523
524 /**
525 * Gets the parent {@code ConfigurationInterpolator}.
526 *
527 * @return the parent {@code ConfigurationInterpolator} (can be <strong>null</strong>)
528 */
529 public ConfigurationInterpolator getParentInterpolator() {
530 return this.parentInterpolator;
531 }
532
533 /** Gets the function used to convert interpolated values to strings.
534 * @return function used to convert interpolated values to strings
535 */
536 public Function<Object, String> getStringConverter() {
537 return stringConverter;
538 }
539
540 /**
541 * Creates and initializes a {@code StringSubstitutor} object which is used for variable substitution. This
542 * {@code StringSubstitutor} is assigned a specialized lookup object implementing the correct variable resolving
543 * algorithm.
544 *
545 * @return the {@code StringSubstitutor} used by this object
546 */
547 private StringSubstitutor initSubstitutor() {
548 return new StringSubstitutor(key -> {
549 final Object value = resolve(key);
550 return value != null
551 ? stringConverter.apply(value)
552 : null;
553 });
554 }
555
556 /**
557 * Performs interpolation of the passed in value. If the value is of type {@code String}, this method checks
558 * whether it contains variables. If so, all variables are replaced by their current values (if possible). For
559 * non string arguments, the value is returned without changes. In the special case where the value is a string
560 * consisting of a single variable reference, the interpolated variable value is <em>not</em> converted to a
561 * string before returning, so that callers can access the raw value. However, if the variable is part of a larger
562 * interpolated string, then the variable value is converted to a string using the configured
563 * {@link #getStringConverter() string converter}. (See the discussion on string conversion in the class
564 * documentation for more details.)
565 *
566 * <p><strong>Examples</strong></p>
567 * <p>
568 * For the following examples, assume that the default string conversion function is in place and that the
569 * variable {@code i} maps to the integer value {@code 42}.
570 * </p>
571 * <pre>
572 * interpolator.interpolate(1) → 1 // non-string argument returned unchanged
573 * interpolator.interpolate("${i}") → 42 // single variable value returned with raw type
574 * interpolator.interpolate("answer = ${i}") → "answer = 42" // variable value converted to string
575 * </pre>
576 *
577 * @param value the value to be interpolated
578 * @return the interpolated value
579 */
580 public Object interpolate(final Object value) {
581 if (value instanceof String) {
582 final String strValue = (String) value;
583 if (isSingleVariable(strValue)) {
584 final Object resolvedValue = resolveSingleVariable(strValue);
585 if (resolvedValue != null && !(resolvedValue instanceof String)) {
586 // If the value is again a string, it needs no special
587 // treatment; it may also contain further variables which
588 // must be resolved; therefore, the default mechanism is
589 // applied.
590 return resolvedValue;
591 }
592 }
593 return substitutor.replace(strValue);
594 }
595 return value;
596 }
597
598 /**
599 * Sets a flag that variable names can contain other variables. If enabled, variable substitution is also done in
600 * variable names.
601 *
602 * @return the substitution in variables flag
603 */
604 public boolean isEnableSubstitutionInVariables() {
605 return substitutor.isEnableSubstitutionInVariables();
606 }
607
608 /**
609 * Checks whether a value to be interpolated consists of single, simple variable reference, for example,
610 * {@code ${myvar}}. In this case, the variable is resolved directly without using the
611 * {@code StringSubstitutor}.
612 *
613 * @param strValue the value to be interpolated
614 * @return {@code true} if the value contains a single, simple variable reference
615 */
616 private boolean isSingleVariable(final String strValue) {
617 return strValue.startsWith(VAR_START)
618 && strValue.indexOf(VAR_END, VAR_START_LENGTH) == strValue.length() - VAR_END_LENGTH;
619 }
620
621 /**
622 * Returns an unmodifiable set with the prefixes, for which {@code Lookup} objects are registered at this instance. This
623 * means that variables with these prefixes can be processed.
624 *
625 * @return a set with the registered variable prefixes
626 */
627 public Set<String> prefixSet() {
628 return Collections.unmodifiableSet(prefixLookups.keySet());
629 }
630
631 /**
632 * Registers the given {@code Lookup} object for the specified prefix at this instance. From now on this lookup object
633 * will be used for variables that have the specified prefix.
634 *
635 * @param prefix the variable prefix (must not be <strong>null</strong>)
636 * @param lookup the {@code Lookup} object to be used for this prefix (must not be <strong>null</strong>)
637 * @throws IllegalArgumentException if either the prefix or the {@code Lookup} object is <strong>null</strong>
638 */
639 public void registerLookup(final String prefix, final Lookup lookup) {
640 if (prefix == null) {
641 throw new IllegalArgumentException("Prefix for lookup object must not be null!");
642 }
643 if (lookup == null) {
644 throw new IllegalArgumentException("Lookup object must not be null!");
645 }
646 prefixLookups.put(prefix, lookup);
647 }
648
649 /**
650 * Registers all {@code Lookup} objects in the given map with their prefixes at this {@code ConfigurationInterpolator}.
651 * Using this method multiple {@code Lookup} objects can be registered at once. If the passed in map is <strong>null</strong>,
652 * this method does not have any effect.
653 *
654 * @param lookups the map with lookups to register (may be <strong>null</strong>)
655 * @throws IllegalArgumentException if the map contains <strong>entries</strong>
656 */
657 public void registerLookups(final Map<String, ? extends Lookup> lookups) {
658 if (lookups != null) {
659 prefixLookups.putAll(lookups);
660 }
661 }
662
663 /**
664 * Removes the specified {@code Lookup} object from the list of default {@code Lookup}s.
665 *
666 * @param lookup the {@code Lookup} object to be removed
667 * @return a flag whether this {@code Lookup} object actually existed and was removed
668 */
669 public boolean removeDefaultLookup(final Lookup lookup) {
670 return defaultLookups.remove(lookup);
671 }
672
673 /**
674 * Resolves the specified variable. This implementation tries to extract a variable prefix from the given variable name
675 * (the first colon (':') is used as prefix separator). It then passes the name of the variable with the prefix stripped
676 * to the lookup object registered for this prefix. If no prefix can be found or if the associated lookup object cannot
677 * resolve this variable, the default lookup objects are used. If this is not successful either and a parent
678 * {@code ConfigurationInterpolator} is available, this object is asked to resolve the variable.
679 *
680 * @param var the name of the variable whose value is to be looked up which may contain a prefix.
681 * @return the value of this variable or <strong>null</strong> if it cannot be resolved
682 */
683 public Object resolve(final String var) {
684 if (var == null) {
685 return null;
686 }
687
688 final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
689 if (prefixPos >= 0) {
690 final String prefix = var.substring(0, prefixPos);
691 final String name = var.substring(prefixPos + 1);
692 final Object value = fetchLookupForPrefix(prefix).lookup(name);
693 if (value != null) {
694 return value;
695 }
696 }
697
698 for (final Lookup lookup : defaultLookups) {
699 final Object value = lookup.lookup(var);
700 if (value != null) {
701 return value;
702 }
703 }
704
705 final ConfigurationInterpolator parent = getParentInterpolator();
706 if (parent != null) {
707 return getParentInterpolator().resolve(var);
708 }
709 return null;
710 }
711
712 /**
713 * Interpolates a string value that consists of a single variable.
714 *
715 * @param strValue the string to be interpolated
716 * @return the resolved value or <strong>null</strong> if resolving failed
717 */
718 private Object resolveSingleVariable(final String strValue) {
719 return resolve(extractVariableName(strValue));
720 }
721
722 /**
723 * Sets the flag whether variable names can contain other variables. This flag corresponds to the
724 * {@code enableSubstitutionInVariables} property of the underlying {@code StringSubstitutor} object.
725 *
726 * @param f the new value of the flag
727 */
728 public void setEnableSubstitutionInVariables(final boolean f) {
729 substitutor.setEnableSubstitutionInVariables(f);
730 }
731
732 /**
733 * Sets the parent {@code ConfigurationInterpolator}. This object is used if the {@code Lookup} objects registered at
734 * this object cannot resolve a variable.
735 *
736 * @param parentInterpolator the parent {@code ConfigurationInterpolator} object (can be <strong>null</strong>)
737 */
738 public void setParentInterpolator(final ConfigurationInterpolator parentInterpolator) {
739 this.parentInterpolator = parentInterpolator;
740 }
741
742 /** Sets the function used to convert interpolated values to strings. Pass
743 * {@code null} to use the default conversion function.
744 *
745 * @param stringConverter function used to convert interpolated values to strings
746 * or {@code null} to use the default conversion function
747 */
748 public void setStringConverter(final Function<Object, String> stringConverter) {
749 this.stringConverter = stringConverter != null
750 ? stringConverter
751 : DefaultStringConverter.INSTANCE;
752 }
753 }