001/*
002 * Copyright 2012 The Kuali Foundation.
003 * 
004 * Licensed under the Educational Community License, Version 1.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 * http://www.opensource.org/licenses/ecl1.php
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.ole.utility;
017
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.Map;
021import java.util.Properties;
022import java.util.Set;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026import org.springframework.util.Assert;
027import org.springframework.util.StringUtils;
028
029/**
030 * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form <code>${name}</code>. Using
031 * <code>PropertyPlaceholderHelper</code> these placeholders can be substituted for user-supplied values.
032 * <p/>
033 * Values for substitution can be supplied using a {@link Properties} instance or using a {@link PlaceholderResolver}.
034 *
035 * @author Juergen Hoeller
036 * @author Rob Harrop
037 * @since 3.0
038 */
039public class PropertyPlaceholderHelper {
040
041    private static final Logger logger = LoggerFactory.getLogger(PropertyPlaceholderHelper.class);
042
043    private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<String, String>(4);
044
045    static {
046        wellKnownSimplePrefixes.put("}", "{");
047        wellKnownSimplePrefixes.put("]", "[");
048        wellKnownSimplePrefixes.put(")", "(");
049    }
050
051    private final String placeholderPrefix;
052
053    private final String placeholderSuffix;
054
055    private final String simplePrefix;
056
057    private final String valueSeparator;
058
059    private final boolean ignoreUnresolvablePlaceholders;
060
061    /**
062     * Creates a new <code>PropertyPlaceholderHelper</code> that uses the supplied prefix and suffix. Unresolvable placeholders are ignored.
063     *
064     * @param placeholderPrefix the prefix that denotes the start of a placeholder.
065     * @param placeholderSuffix the suffix that denotes the end of a placeholder.
066     */
067    public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) {
068        this(placeholderPrefix, placeholderSuffix, null, true);
069    }
070
071    /**
072     * Creates a new <code>PropertyPlaceholderHelper</code> that uses the supplied prefix and suffix.
073     *
074     * @param placeholderPrefix              the prefix that denotes the start of a placeholder
075     * @param placeholderSuffix              the suffix that denotes the end of a placeholder
076     * @param valueSeparator                 the separating character between the placeholder variable and the associated default value, if any
077     * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should be ignored (<code>true</code>) or cause an exception (
078     *                                       <code>false</code>).
079     */
080    public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, String valueSeparator, boolean ignoreUnresolvablePlaceholders) {
081
082        Assert.notNull(placeholderPrefix, "placeholderPrefix must not be null");
083        Assert.notNull(placeholderSuffix, "placeholderSuffix must not be null");
084        this.placeholderPrefix = placeholderPrefix;
085        this.placeholderSuffix = placeholderSuffix;
086        String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
087        if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
088            this.simplePrefix = simplePrefixForSuffix;
089        } else {
090            this.simplePrefix = this.placeholderPrefix;
091        }
092        this.valueSeparator = valueSeparator;
093        this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
094    }
095
096    /**
097     * Replaces all placeholders of format <code>${name}</code> with the corresponding property from the supplied {@link Properties} .
098     *
099     * @param value      the value containing the placeholders to be replaced.
100     * @param properties the <code>Properties</code> to use for replacement.
101     * @return the supplied value with placeholders replaced inline.
102     */
103    public String replacePlaceholders(String value, final Properties properties) {
104        Assert.notNull(properties, "Argument 'properties' must not be null.");
105        return replacePlaceholders(value, new PlaceholderResolver() {
106            @Override
107            public String resolvePlaceholder(String placeholderName) {
108                return properties.getProperty(placeholderName);
109            }
110        });
111    }
112
113    /**
114     * Replaces all placeholders of format <code>${name}</code> with the value returned from the supplied {@link PlaceholderResolver}.
115     *
116     * @param value               the value containing the placeholders to be replaced.
117     * @param placeholderResolver the <code>PlaceholderResolver</code> to use for replacement.
118     * @return the supplied value with placeholders replaced inline.
119     */
120    public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
121        Assert.notNull(value, "Argument 'value' must not be null.");
122        return parseStringValue(value, placeholderResolver, new HashSet<String>());
123    }
124
125    protected String parseStringValue(String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
126
127        StringBuilder buf = new StringBuilder(strVal);
128
129        int startIndex = strVal.indexOf(this.placeholderPrefix);
130        while (startIndex != -1) {
131            int endIndex = findPlaceholderEndIndex(buf, startIndex);
132            if (endIndex != -1) {
133                String placeholder = buf.substring(startIndex + this.placeholderPrefix.length(), endIndex);
134                if (!visitedPlaceholders.add(placeholder)) {
135                    throw new IllegalArgumentException("Circular placeholder reference '" + placeholder + "' in property definitions");
136                }
137                // Recursive invocation, parsing placeholders contained in the placeholder key.
138                placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
139
140                // Now obtain the value for the fully resolved key...
141                String propVal = placeholderResolver.resolvePlaceholder(placeholder);
142                if (propVal == null && this.valueSeparator != null) {
143                    int separatorIndex = placeholder.indexOf(this.valueSeparator);
144                    if (separatorIndex != -1) {
145                        String actualPlaceholder = placeholder.substring(0, separatorIndex);
146                        String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
147                        propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
148                        if (propVal == null) {
149                            propVal = defaultValue;
150                        }
151                    }
152                }
153                if (propVal != null) {
154                    // Recursive invocation, parsing placeholders contained in the
155                    // previously resolved placeholder value.
156                    propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
157                    buf.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
158                    if (logger.isTraceEnabled()) {
159                        logger.trace("Resolved placeholder '" + placeholder + "'");
160                    }
161                    startIndex = buf.indexOf(this.placeholderPrefix, startIndex + propVal.length());
162                } else if (this.ignoreUnresolvablePlaceholders) {
163                    // Proceed with unprocessed value.
164                    startIndex = buf.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
165                } else {
166                    throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'");
167                }
168
169                visitedPlaceholders.remove(placeholder);
170            } else {
171                startIndex = -1;
172            }
173        }
174
175        return buf.toString();
176    }
177
178    private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
179        int index = startIndex + this.placeholderPrefix.length();
180        int withinNestedPlaceholder = 0;
181        while (index < buf.length()) {
182            if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
183                if (withinNestedPlaceholder > 0) {
184                    withinNestedPlaceholder--;
185                    index = index + this.placeholderSuffix.length();
186                } else {
187                    return index;
188                }
189            } else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
190                withinNestedPlaceholder++;
191                index = index + this.simplePrefix.length();
192            } else {
193                index++;
194            }
195        }
196        return -1;
197    }
198
199    /**
200     * Strategy interface used to resolve replacement values for placeholders contained in Strings.
201     *
202     * @see PropertyPlaceholderHelper
203     */
204    public static interface PlaceholderResolver {
205
206        /**
207         * Resolves the supplied placeholder name into the replacement value.
208         *
209         * @param placeholderName the name of the placeholder to resolve.
210         * @return the replacement value or <code>null</code> if no replacement is to be made.
211         */
212        String resolvePlaceholder(String placeholderName);
213    }
214
215}