001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.configuration2.interpol; 018 019import java.lang.reflect.Array; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Properties; 029import java.util.Set; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.CopyOnWriteArrayList; 032import java.util.function.Function; 033 034import org.apache.commons.text.StringSubstitutor; 035 036/** 037 * <p> 038 * A class that handles interpolation (variable substitution) for configuration objects. 039 * </p> 040 * <p> 041 * Each instance of {@code AbstractConfiguration} is associated with an object of this class. All interpolation tasks 042 * are delegated to this object. 043 * </p> 044 * <p> 045 * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor} class from 046 * <a href="https://commons.apache.org/text">Commons Text</a>. Thus it supports the same syntax of variable expressions. 047 * </p> 048 * <p> 049 * The basic idea of this class is that it can maintain a set of primitive {@link Lookup} objects, each of which is 050 * identified by a special prefix. The variables to be processed have the form {@code ${prefix:name}}. 051 * {@code ConfigurationInterpolator} will extract the prefix and determine, which primitive lookup object is registered 052 * for it. Then the name of the variable is passed to this object to obtain the actual value. It is also possible to 053 * define an arbitrary number of default lookup objects, which are used for variables that do not have a prefix or that 054 * cannot be resolved by their associated lookup object. When adding default lookup objects their order matters; they 055 * are queried in this order, and the first non-<strong>null</strong> variable value is used. 056 * </p> 057 * <p> 058 * After an instance has been created it does not contain any {@code Lookup} objects. The current set of lookup objects 059 * can be modified using the {@code registerLookup()} and {@code deregisterLookup()} methods. Default lookup objects 060 * (that are invoked for variables without a prefix) can be added or removed with the {@code addDefaultLookup()} and 061 * {@code removeDefaultLookup()} methods respectively. (When a {@code ConfigurationInterpolator} instance is created by 062 * a configuration object, a default lookup object is added pointing to the configuration itself, so that variables are 063 * resolved using the configuration's properties.) 064 * </p> 065 * <p> 066 * The default usage scenario is that on a fully initialized instance the {@code interpolate()} method is called. It is 067 * passed an object value which may contain variables. All these variables are substituted if they can be resolved. The 068 * result is the passed in value with variables replaced. Alternatively, the {@code resolve()} method can be called to 069 * obtain the values of specific variables without performing interpolation. 070 * </p> 071 * <p><strong>String Conversion</strong></p> 072 * <p> 073 * When variables are part of larger interpolated strings, the variable values, which can be of any type, must be 074 * converted to strings to produce the full result. Each interpolator instance has a configurable 075 * {@link #setStringConverter(Function) string converter} to perform this conversion. The default implementation of this 076 * function simply uses the value's {@code toString} method in the majority of cases. However, for maximum 077 * consistency with 078 * {@link org.apache.commons.configuration2.convert.DefaultConversionHandler DefaultConversionHandler}, when a variable 079 * value is a container type (such as a collection or array), then only the first element of the container is converted 080 * to a string instead of the container itself. For example, if the variable {@code x} resolves to the integer array 081 * {@code [1, 2, 3]}, then the string {@code "my value = ${x}"} will by default be interpolated to 082 * {@code "my value = 1"}. 083 * </p> 084 * <p> 085 * <strong>Implementation note:</strong> This class is thread-safe. Lookup objects can be added or removed at any time 086 * concurrent to interpolation operations. 087 * </p> 088 * 089 * @since 1.4 090 */ 091public class ConfigurationInterpolator { 092 093 /** 094 * Internal class used to construct the default {@link Lookup} map used by 095 * {@link ConfigurationInterpolator#getDefaultPrefixLookups()}. 096 */ 097 static final class DefaultPrefixLookupsHolder { 098 099 /** 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}