Coverage Report - org.kuali.spring.util.PlaceholderStringResolver
 
Classes in this File Line Coverage Branch Coverage Complexity
PlaceholderStringResolver
100%
159/159
95%
40/42
1.879
 
 1  
 package org.kuali.spring.util;
 2  
 
 3  
 import java.util.ArrayList;
 4  
 import java.util.Collection;
 5  
 import java.util.HashMap;
 6  
 import java.util.HashSet;
 7  
 import java.util.List;
 8  
 import java.util.Map;
 9  
 import java.util.Set;
 10  
 
 11  
 import org.slf4j.Logger;
 12  
 import org.slf4j.LoggerFactory;
 13  
 import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
 14  
 import org.springframework.util.Assert;
 15  
 import org.springframework.util.StringUtils;
 16  
 
 17  
 /**
 18  
  * This class contains logic for recursively parsing/expanding PlaceholderString's. A PlaceholderString is a String that
 19  
  * contains Placeholder object(s).<br>
 20  
  * 
 21  
  * Recursion is used since the text of a Placeholder may itself be a PlaceholderString.<br>
 22  
  * 
 23  
  * Consider the text:<br>
 24  
  * 
 25  
  * <pre>
 26  
  * A ${cat} is ${${cat}.speed}.
 27  
  * </pre>
 28  
  * 
 29  
  * The PlaceholderString representing this text contains two Placeholders:<br>
 30  
  * 
 31  
  * <pre>
 32  
  * 1 - ${cat}
 33  
  * 2 - ${${cat}.speed}
 34  
  * </pre>
 35  
  * 
 36  
  * The second Placeholder has a nested PlaceholderString <code>${cat}.speed</code> which must be resolved in order to
 37  
  * locate the correct replacement value.
 38  
  * 
 39  
  * Given a properties file (or other property source) with the following mappings:<br>
 40  
  * 
 41  
  * <pre>
 42  
  * cat=cheetah
 43  
  * cheetah.speed=fast
 44  
  * </pre>
 45  
  * 
 46  
  * The text <code>A ${cat} is ${${cat}.speed}.</code> should be resolved to <code>A cheetah is fast.</code><br>
 47  
  * 
 48  
  * The object graph created to model the data is:<br>
 49  
  * 
 50  
  * <pre>
 51  
  * PlaceholderString 
 52  
  *     + text: A ${cat} is ${${cat}.speed}.
 53  
  *     + resolvedText: A cheetah is fast.
 54  
  *  - Placeholder 
 55  
  *     + value: cheetah
 56  
  *     - PlaceholderString
 57  
  *       + text: cat
 58  
  *       + resolvedText: cat
 59  
  *  - Placeholder 
 60  
  *     + value: fast
 61  
  *     - PlaceholderString
 62  
  *           + text: ${cat}.speed
 63  
  *           + resolvedText: cheetah.speed
 64  
  *        - Placeholder 
 65  
  *           + value: cheetah
 66  
  *           - PlaceholderString
 67  
  *             + text: cat
 68  
  *             + resolvedText: cat
 69  
  * </pre>
 70  
  */
 71  
 public class PlaceholderStringResolver {
 72  
 
 73  1
     private static final Logger LOGGER = LoggerFactory.getLogger(PlaceholderStringResolver.class);
 74  
 
 75  1
     private static final Map<String, String> WELL_KNOWN_SIMPLE_PREFIXES = new HashMap<String, String>(4);
 76  
     public static final boolean DEFAULT_IS_IGNORE_UNRESOLVABLE_PLACEHOLDERS = false;
 77  
 
 78  
     static {
 79  1
         WELL_KNOWN_SIMPLE_PREFIXES.put("}", "{");
 80  1
         WELL_KNOWN_SIMPLE_PREFIXES.put("]", "[");
 81  1
         WELL_KNOWN_SIMPLE_PREFIXES.put(")", "(");
 82  1
     }
 83  
 
 84  20
     PropertyLogger plogger = new PropertyLogger();
 85  
     String placeholderPrefix;
 86  
     String placeholderSuffix;
 87  
     String valueSeparator;
 88  
     boolean ignoreUnresolvablePlaceholders;
 89  
     String simplePrefix;
 90  
 
 91  
     public PlaceholderStringResolver() {
 92  18
         this(DEFAULT_IS_IGNORE_UNRESOLVABLE_PLACEHOLDERS);
 93  18
     }
 94  
 
 95  
     public PlaceholderStringResolver(boolean ignoreUnresolvablePlaceholders) {
 96  18
         this(PropertyPlaceholderConfigurer.DEFAULT_PLACEHOLDER_PREFIX,
 97  
                 PropertyPlaceholderConfigurer.DEFAULT_PLACEHOLDER_SUFFIX, null, ignoreUnresolvablePlaceholders);
 98  18
     }
 99  
 
 100  
     public PlaceholderStringResolver(String placeholderPrefix, String placeholderSuffix) {
 101  1
         this(placeholderPrefix, placeholderSuffix, null, DEFAULT_IS_IGNORE_UNRESOLVABLE_PLACEHOLDERS);
 102  1
     }
 103  
 
 104  
     public PlaceholderStringResolver(String placeholderPrefix, String placeholderSuffix, String valueSeparator,
 105  20
             boolean ignoreUnresolvablePlaceholders) {
 106  
 
 107  20
         Assert.notNull(placeholderPrefix, "placeholderPrefix must not be null");
 108  20
         Assert.notNull(placeholderSuffix, "placeholderSuffix must not be null");
 109  
 
 110  20
         this.placeholderPrefix = placeholderPrefix;
 111  20
         this.placeholderSuffix = placeholderSuffix;
 112  
 
 113  20
         String simplePrefixForSuffix = WELL_KNOWN_SIMPLE_PREFIXES.get(this.placeholderSuffix);
 114  20
         if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
 115  18
             this.simplePrefix = simplePrefixForSuffix;
 116  
         } else {
 117  2
             this.simplePrefix = this.placeholderPrefix;
 118  
         }
 119  20
         this.valueSeparator = valueSeparator;
 120  20
         this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
 121  20
     }
 122  
 
 123  
     /**
 124  
      * Return true if the collection is null or has no elements
 125  
      */
 126  
     protected boolean isEmpty(Collection<?> c) {
 127  20
         return c == null || c.size() == 0;
 128  
     }
 129  
 
 130  
     /**
 131  
      * Trim the prefix and suffix off of the placeholder ie change ${foo}->foo
 132  
      */
 133  
     protected String getTrimmedText(String text) {
 134  27
         int prefixLength = this.placeholderPrefix.length();
 135  27
         int suffixLength = this.placeholderSuffix.length();
 136  27
         int beginIndex = prefixLength;
 137  27
         int endIndex = text.length() - suffixLength;
 138  27
         return text.substring(beginIndex, endIndex);
 139  
     }
 140  
 
 141  
     /**
 142  
      * Handle placeholders that have default values supplied with them ie ${jdbc.vendor=mysql}
 143  
      */
 144  
     protected String getDefaultValue(String key, ValueRetriever retriever) {
 145  
         // If they haven't specified a valueSeparator, we're done
 146  5
         if (this.valueSeparator == null) {
 147  2
             return null;
 148  
         }
 149  
 
 150  
         // If they supplied a valueSeparator but this key isn't using it, we are done
 151  3
         int separatorIndex = key.indexOf(this.valueSeparator);
 152  3
         if (separatorIndex == -1) {
 153  1
             return null;
 154  
         }
 155  
 
 156  
         // Extract 'jdbc.vendor' from 'jdbc.vendor=mysql'
 157  2
         String actualKey = key.substring(0, separatorIndex);
 158  
 
 159  
         // Extract 'mysql' from 'jdbc.vendor=mysql'
 160  2
         String defaultValue = key.substring(separatorIndex + this.valueSeparator.length());
 161  
 
 162  
         // Give the retriever a chance to locate a value for 'jdbc.vendor'
 163  2
         String value = retriever.retrieveValue(actualKey);
 164  
 
 165  
         // If the retriever found something, use it
 166  2
         if (value != null) {
 167  1
             return value;
 168  
         } else {
 169  
             // Otherwise return the default value
 170  1
             return defaultValue;
 171  
         }
 172  
     }
 173  
 
 174  
     /**
 175  
      * Get a value for this key
 176  
      */
 177  
     protected String getRawValue(String key, ValueRetriever retriever) {
 178  
         // Give the retriever first dibs at locating a value
 179  20
         String value = retriever.retrieveValue(key);
 180  
 
 181  
         // The retriever found something, use it
 182  20
         if (value != null) {
 183  15
             return value;
 184  
         }
 185  
 
 186  
         // There might be a default value
 187  5
         return getDefaultValue(key, retriever);
 188  
     }
 189  
 
 190  
     /**
 191  
      * Throw an exception unless they've activated the flag for ignoring unresolved placeholders
 192  
      */
 193  
     protected void handleUnresolvedPlaceholder(String placeholderText) {
 194  
         // Value is null, meaning we could not get find a value for the Placeholder
 195  3
         if (!this.ignoreUnresolvablePlaceholders) {
 196  
             // If we are not ignoring unresolved placeholder's, we are done
 197  1
             throw new IllegalArgumentException("Could not resolve a value for " + placeholderText);
 198  
         } else {
 199  2
             LOGGER.trace("Ignoring unresolvable placeholder: '" + placeholderText + "'");
 200  
         }
 201  2
     }
 202  
 
 203  
     /**
 204  
      * Return the value that should be substituted for the placeholder text
 205  
      */
 206  
     protected String getValue(String key, Placeholder placeholder, ValueRetriever retriever, Set<String> resolving) {
 207  
         // Get a value for this property key
 208  20
         String value = getRawValue(key, retriever);
 209  
 
 210  20
         if (value == null) {
 211  
             // No value could be located
 212  3
             String placeholderText = getPlaceholderPrefix() + placeholder.getPlaceholderString().getText()
 213  
                     + getPlaceholderSuffix();
 214  3
             handleUnresolvedPlaceholder(placeholderText);
 215  2
             return placeholderText;
 216  
         } else {
 217  
             // Resolve any placeholders in the value we located
 218  17
             PlaceholderString phs = new PlaceholderString(value);
 219  17
             resolvePlaceholderString(phs, retriever, resolving);
 220  15
             return phs.getResolvedText();
 221  
         }
 222  
     }
 223  
 
 224  
     /**
 225  
      * Return a String that has had all of it's placeholders resolved
 226  
      */
 227  
     public String resolve(String text, ValueRetriever retriever) {
 228  1031
         PlaceholderString phs = new PlaceholderString(text);
 229  1031
         resolve(phs, retriever);
 230  1029
         return phs.getResolvedText();
 231  
     }
 232  
 
 233  
     /**
 234  
      * Resolve any Placeholders in the supplied PlaceholderString. When this method completes,
 235  
      * PlaceholderString.getResolvedText() will return the fully resolved text.
 236  
      */
 237  
     public void resolve(PlaceholderString placeholderString, ValueRetriever retriever) {
 238  1031
         resolvePlaceholderString(placeholderString, retriever, new HashSet<String>());
 239  1029
     }
 240  
 
 241  
     /**
 242  
      * When this method completes, PlaceholderString.getResolvedText() will return the fully resolved text
 243  
      */
 244  
     protected void resolvePlaceholderString(PlaceholderString phs, ValueRetriever retriever, Set<String> resolving) {
 245  
         // Parse the PlaceholderString to find any Placeholder's it may contain
 246  
         // phs.getPlaceholders() returns any parsed Placeholders
 247  1052
         parse(phs);
 248  
 
 249  
         // Iterate through the Placeholder objects
 250  1052
         StringBuilder buffer = new StringBuilder(phs.getText());
 251  1052
         int offset = 0;
 252  1052
         for (Placeholder pholder : phs.getPlaceholders()) {
 253  
             // Resolve each placeholder
 254  21
             resolvePlaceholder(pholder, retriever, resolving);
 255  
             // Update the buffer with the new text
 256  17
             offset += updateBuffer(buffer, pholder, offset);
 257  
         }
 258  
         // Store the new text, after all placeholders have been replaced
 259  1048
         phs.setResolvedText(buffer.toString());
 260  1048
     }
 261  
 
 262  
     /**
 263  
      * Update the buffer to replace the placeholder text with a value
 264  
      */
 265  
     protected int updateBuffer(StringBuilder buffer, Placeholder pholder, int offset) {
 266  17
         int prefixLength = this.placeholderPrefix.length();
 267  17
         int suffixLength = this.placeholderSuffix.length();
 268  17
         int startIndex = pholder.getStartIndex() + offset;
 269  17
         int endIndex = pholder.getEndIndex() + offset;
 270  17
         buffer.replace(startIndex, endIndex, pholder.getValue());
 271  17
         int textLength = pholder.getPlaceholderString().getText().length();
 272  17
         int originalLength = prefixLength + textLength + suffixLength;
 273  17
         int newLength = pholder.getValue().length();
 274  17
         int diff = newLength - originalLength;
 275  17
         return diff;
 276  
     }
 277  
 
 278  
     /**
 279  
      * Make sure we aren't in an infinite loop. If someone does something like this in a properties file:<br>
 280  
      * <code>
 281  
      *  a=${b} 
 282  
      *  b=${a}
 283  
      * </code>
 284  
      * 
 285  
      * We would end up with a stack overflow error without this check
 286  
      */
 287  
     protected void circularReferenceCheck(String placeholderText, Set<String> resolving) {
 288  21
         LOGGER.trace("Adding '{}' to the list of placeholders being resolved", placeholderText);
 289  21
         boolean added = resolving.add(placeholderText);
 290  
 
 291  21
         if (!added) {
 292  
             // If we get here, one of the placeholders we are trying to resolve contains a reference to another
 293  
             // placeholder that we are already trying to resolve.
 294  1
             throw new IllegalArgumentException("Circular reference detected on " + placeholderText);
 295  
         }
 296  20
     }
 297  
 
 298  
     /**
 299  
      * When this method completes, Placeholder.getValue() will return the value that should be used in place of the
 300  
      * Placeholder
 301  
      */
 302  
     protected void resolvePlaceholder(Placeholder placeholder, ValueRetriever retriever, Set<String> resolving) {
 303  
         // Extract the PlaceholderString for this Placeholder
 304  21
         PlaceholderString phs = placeholder.getPlaceholderString();
 305  
 
 306  
         // Make sure we are not in an infinite loop
 307  21
         circularReferenceCheck(phs.getText(), resolving);
 308  
 
 309  
         // The base case is a placeholder that does not contain nested placeholder's
 310  20
         if (isEmpty(phs.getPlaceholders())) {
 311  16
             phs.setResolvedText(phs.getText());
 312  
         } else {
 313  
             // Recurse to handle nested placeholders
 314  4
             resolvePlaceholderString(phs, retriever, resolving);
 315  
         }
 316  
 
 317  
         // The resolved text is our property key
 318  20
         String key = phs.getResolvedText();
 319  
 
 320  
         // Get a value for the property
 321  20
         String value = getValue(key, placeholder, retriever, resolving);
 322  
 
 323  
         // Store the value in the placeholder object
 324  17
         placeholder.setValue(value);
 325  
 
 326  
         // The Placeholder is now resolved, remove it from the set
 327  17
         LOGGER.trace("Removing '{}' from the list of placeholders being resolved", phs.getText());
 328  17
         resolving.remove(phs.getText());
 329  17
     }
 330  
 
 331  
     /**
 332  
      * Parse the PlaceholderString to get the List of Placeholder objects (if any). If there are none, an empty list is
 333  
      * returned.
 334  
      */
 335  
     protected void parse(PlaceholderString phs) {
 336  
 
 337  
         // Extract the text we will be parsing
 338  1079
         String text = phs.getText();
 339  
 
 340  
         // Locate the first occurrence of our prefix
 341  1079
         int startIndex = text.indexOf(this.placeholderPrefix);
 342  1079
         if (startIndex == -1) {
 343  1056
             LOGGER.trace("Skip parsing.  No placeholders found in [{}]", text);
 344  
         }
 345  
 
 346  
         // Storage for the placeholders we find
 347  1079
         List<Placeholder> placeholders = new ArrayList<Placeholder>();
 348  
 
 349  
         // While there is at least one prefix remaining
 350  1106
         while (startIndex != -1) {
 351  
             // Attempt to get a placeholder object
 352  28
             Placeholder placeholder = getPlaceholder(text, startIndex);
 353  
 
 354  
             // No placeholder could be found
 355  28
             if (placeholder == null) {
 356  1
                 break;
 357  
             }
 358  
 
 359  
             // Add our placeholder to the list
 360  27
             placeholders.add(placeholder);
 361  
 
 362  
             // Start looking again, skipping past the placeholder we just found
 363  27
             int newFromIndex = placeholder.getEndIndex();
 364  
 
 365  
             // Attempt to locate another prefix
 366  27
             startIndex = text.indexOf(this.placeholderPrefix, newFromIndex);
 367  27
         }
 368  
 
 369  
         // Store the Placeholder's on the PlaceHolderString
 370  1079
         phs.setPlaceholders(placeholders);
 371  1079
     }
 372  
 
 373  
     /**
 374  
      * Get a Placeholder object representing the first placeholder after startIndex
 375  
      */
 376  
     protected Placeholder getPlaceholder(String source, int startIndex) {
 377  28
         int suffixLength = this.placeholderSuffix.length();
 378  
 
 379  
         // Attempt to locate the end of the placeholder
 380  28
         int endIndex = findPlaceholderEndIndex(source, startIndex);
 381  
 
 382  28
         if (endIndex == -1) {
 383  
             // Could not locate the end
 384  1
             return null;
 385  
         } else {
 386  
             // Move the endIndex past the suffix
 387  27
             endIndex = endIndex + suffixLength;
 388  
         }
 389  
 
 390  
         // The placeholder exactly as it appears in the source string ie ${foo}
 391  27
         String text = source.substring(startIndex, endIndex);
 392  
 
 393  
         // Trim off prefix and suffix
 394  27
         String trimmedText = getTrimmedText(text);
 395  
 
 396  
         // Create a new PlaceholderString
 397  27
         PlaceholderString phs = new PlaceholderString(trimmedText);
 398  
 
 399  
         // Recursive call to parse the new PlaceholderString (and any Placeholder's it may contain)
 400  27
         parse(phs);
 401  
 
 402  
         // Populate a placeholder object
 403  27
         Placeholder placeholder = new Placeholder();
 404  27
         placeholder.setStartIndex(startIndex);
 405  27
         placeholder.setEndIndex(endIndex);
 406  27
         placeholder.setPlaceholderString(phs);
 407  27
         return placeholder;
 408  
     }
 409  
 
 410  
     /**
 411  
      * Return true if the index into the buffer is pointing at the suffix, false otherwise
 412  
      */
 413  
     protected boolean haveArrivedAtSuffix(CharSequence buf, int index) {
 414  115
         return StringUtils.substringMatch(buf, index, this.placeholderSuffix);
 415  
     }
 416  
 
 417  
     /**
 418  
      * Return true if the index into the buffer is pointing at the simplePrefix, false otherwise
 419  
      */
 420  
     protected boolean haveArrivedAtPrefix(CharSequence buf, int index) {
 421  82
         return StringUtils.substringMatch(buf, index, this.simplePrefix);
 422  
     }
 423  
 
 424  
     /**
 425  
      * Given a buffer and a startingIndex, find the suffix that matches our prefix. The default prefix is "${" and the
 426  
      * default suffix is "}"
 427  
      */
 428  
     protected int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
 429  
         // Skip past the prefix
 430  28
         int index = startIndex + this.placeholderPrefix.length();
 431  
 
 432  
         // Track nested place holders as we encounter them
 433  28
         int nestedPlaceHolderCount = 0;
 434  
 
 435  
         // Iterate through the buffer looking for the closing bracket that matches our opening bracket
 436  116
         while (index < buf.length()) {
 437  
             // Check to see if we've arrived at a closing bracket
 438  115
             if (haveArrivedAtSuffix(buf, index)) {
 439  
 
 440  
                 // If we aren't nested, return the index, nothing more to do
 441  33
                 if (nestedPlaceHolderCount == 0) {
 442  27
                     return index;
 443  
                 }
 444  
 
 445  
                 // Otherwise, decrement the nested count
 446  6
                 nestedPlaceHolderCount--;
 447  
 
 448  
                 // and skip past the nested suffix
 449  6
                 index = index + this.placeholderSuffix.length();
 450  
 
 451  82
             } else if (haveArrivedAtPrefix(buf, index)) {
 452  
                 // If we get here, we've encountered a nested placeholder
 453  
 
 454  
                 // Increment the counter
 455  6
                 nestedPlaceHolderCount++;
 456  
 
 457  
                 // Skip past the nested prefix
 458  6
                 index = index + this.simplePrefix.length();
 459  
             } else {
 460  
                 // We are not on a prefix or a suffix, keep searching
 461  76
                 index++;
 462  
             }
 463  
         }
 464  
 
 465  
         // We never found a suffix to match our prefix
 466  1
         return -1;
 467  
     }
 468  
 
 469  
     public String getPlaceholderPrefix() {
 470  9
         return placeholderPrefix;
 471  
     }
 472  
 
 473  
     public void setPlaceholderPrefix(String placeholderPrefix) {
 474  2
         this.placeholderPrefix = placeholderPrefix;
 475  2
     }
 476  
 
 477  
     public String getPlaceholderSuffix() {
 478  8
         return placeholderSuffix;
 479  
     }
 480  
 
 481  
     public void setPlaceholderSuffix(String placeholderSuffix) {
 482  2
         this.placeholderSuffix = placeholderSuffix;
 483  2
     }
 484  
 
 485  
     public String getValueSeparator() {
 486  3
         return valueSeparator;
 487  
     }
 488  
 
 489  
     public void setValueSeparator(String valueSeparator) {
 490  2
         this.valueSeparator = valueSeparator;
 491  2
     }
 492  
 
 493  
     public boolean isIgnoreUnresolvablePlaceholders() {
 494  2
         return ignoreUnresolvablePlaceholders;
 495  
     }
 496  
 
 497  
     public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) {
 498  3
         this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
 499  3
     }
 500  
 
 501  
     public PropertyLogger getPlogger() {
 502  2
         return plogger;
 503  
     }
 504  
 
 505  
     public void setPlogger(PropertyLogger plogger) {
 506  5
         this.plogger = plogger;
 507  5
     }
 508  
 
 509  
     public String getSimplePrefix() {
 510  3
         return simplePrefix;
 511  
     }
 512  
 
 513  
     public void setSimplePrefix(String simplePrefix) {
 514  2
         this.simplePrefix = simplePrefix;
 515  2
     }
 516  
 
 517  
 }