001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.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 *      https://www.apache.org/licenses/LICENSE-2.0
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 */
016
017package org.springframework.util;
018
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Properties;
023import java.util.Set;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027
028/**
029 * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form
030 * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for
031 * user-supplied values. <p> Values for substitution can be supplied using a {@link Properties} instance or
032 * using a {@link PlaceholderResolver}.
033 *
034 * @author Juergen Hoeller
035 * @author Rob Harrop
036 * @since 3.0
037 */
038public class PropertyPlaceholderHelper {
039
040        private static final Log logger = LogFactory.getLog(PropertyPlaceholderHelper.class);
041
042        private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<String, String>(4);
043
044        static {
045                wellKnownSimplePrefixes.put("}", "{");
046                wellKnownSimplePrefixes.put("]", "[");
047                wellKnownSimplePrefixes.put(")", "(");
048        }
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        /**
063         * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix.
064         * Unresolvable placeholders are ignored.
065         * @param placeholderPrefix the prefix that denotes the start of a placeholder
066         * @param placeholderSuffix the suffix that denotes the end of a placeholder
067         */
068        public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) {
069                this(placeholderPrefix, placeholderSuffix, null, true);
070        }
071
072        /**
073         * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix.
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
077         * and the associated default value, if any
078         * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should
079         * be ignored ({@code true}) or cause an exception ({@code false})
080         */
081        public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
082                        String valueSeparator, boolean ignoreUnresolvablePlaceholders) {
083
084                Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
085                Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
086                this.placeholderPrefix = placeholderPrefix;
087                this.placeholderSuffix = placeholderSuffix;
088                String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
089                if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
090                        this.simplePrefix = simplePrefixForSuffix;
091                }
092                else {
093                        this.simplePrefix = this.placeholderPrefix;
094                }
095                this.valueSeparator = valueSeparator;
096                this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
097        }
098
099
100        /**
101         * Replaces all placeholders of format {@code ${name}} with the corresponding
102         * property from the supplied {@link Properties}.
103         * @param value the value containing the placeholders to be replaced
104         * @param properties the {@code Properties} to use for replacement
105         * @return the supplied value with placeholders replaced inline
106         */
107        public String replacePlaceholders(String value, final Properties properties) {
108                Assert.notNull(properties, "'properties' must not be null");
109                return replacePlaceholders(value, new PlaceholderResolver() {
110                        @Override
111                        public String resolvePlaceholder(String placeholderName) {
112                                return properties.getProperty(placeholderName);
113                        }
114                });
115        }
116
117        /**
118         * Replaces all placeholders of format {@code ${name}} with the value returned
119         * from the supplied {@link PlaceholderResolver}.
120         * @param value the value containing the placeholders to be replaced
121         * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement
122         * @return the supplied value with placeholders replaced inline
123         */
124        public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
125                Assert.notNull(value, "'value' must not be null");
126                return parseStringValue(value, placeholderResolver, new HashSet<String>());
127        }
128
129        protected String parseStringValue(
130                        String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
131
132                StringBuilder result = new StringBuilder(value);
133
134                int startIndex = value.indexOf(this.placeholderPrefix);
135                while (startIndex != -1) {
136                        int endIndex = findPlaceholderEndIndex(result, startIndex);
137                        if (endIndex != -1) {
138                                String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
139                                String originalPlaceholder = placeholder;
140                                if (!visitedPlaceholders.add(originalPlaceholder)) {
141                                        throw new IllegalArgumentException(
142                                                        "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
143                                }
144                                // Recursive invocation, parsing placeholders contained in the placeholder key.
145                                placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
146                                // Now obtain the value for the fully resolved key...
147                                String propVal = placeholderResolver.resolvePlaceholder(placeholder);
148                                if (propVal == null && this.valueSeparator != null) {
149                                        int separatorIndex = placeholder.indexOf(this.valueSeparator);
150                                        if (separatorIndex != -1) {
151                                                String actualPlaceholder = placeholder.substring(0, separatorIndex);
152                                                String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
153                                                propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
154                                                if (propVal == null) {
155                                                        propVal = defaultValue;
156                                                }
157                                        }
158                                }
159                                if (propVal != null) {
160                                        // Recursive invocation, parsing placeholders contained in the
161                                        // previously resolved placeholder value.
162                                        propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
163                                        result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
164                                        if (logger.isTraceEnabled()) {
165                                                logger.trace("Resolved placeholder '" + placeholder + "'");
166                                        }
167                                        startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
168                                }
169                                else if (this.ignoreUnresolvablePlaceholders) {
170                                        // Proceed with unprocessed value.
171                                        startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
172                                }
173                                else {
174                                        throw new IllegalArgumentException("Could not resolve placeholder '" +
175                                                        placeholder + "'" + " in value \"" + value + "\"");
176                                }
177                                visitedPlaceholders.remove(originalPlaceholder);
178                        }
179                        else {
180                                startIndex = -1;
181                        }
182                }
183
184                return result.toString();
185        }
186
187        private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
188                int index = startIndex + this.placeholderPrefix.length();
189                int withinNestedPlaceholder = 0;
190                while (index < buf.length()) {
191                        if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
192                                if (withinNestedPlaceholder > 0) {
193                                        withinNestedPlaceholder--;
194                                        index = index + this.placeholderSuffix.length();
195                                }
196                                else {
197                                        return index;
198                                }
199                        }
200                        else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
201                                withinNestedPlaceholder++;
202                                index = index + this.simplePrefix.length();
203                        }
204                        else {
205                                index++;
206                        }
207                }
208                return -1;
209        }
210
211
212        /**
213         * Strategy interface used to resolve replacement values for placeholders contained in Strings.
214         */
215        public interface PlaceholderResolver {
216
217                /**
218                 * Resolve the supplied placeholder name to the replacement value.
219                 * @param placeholderName the name of the placeholder to resolve
220                 * @return the replacement value, or {@code null} if no replacement is to be made
221                 */
222                String resolvePlaceholder(String placeholderName);
223        }
224
225}