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 *      http://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.camel.component.properties;
018
019import java.util.HashSet;
020import java.util.Properties;
021import java.util.Set;
022
023import org.apache.camel.util.ObjectHelper;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * A parser to parse a string which contains property placeholders.
029 */
030public class DefaultPropertiesParser implements AugmentedPropertyNameAwarePropertiesParser {
031    private static final String GET_OR_ELSE_TOKEN = ":";
032
033    protected final Logger log = LoggerFactory.getLogger(getClass());
034
035    private PropertiesComponent propertiesComponent;
036
037    public DefaultPropertiesParser() {
038    }
039
040    public DefaultPropertiesParser(PropertiesComponent propertiesComponent) {
041        this.propertiesComponent = propertiesComponent;
042    }
043
044    public PropertiesComponent getPropertiesComponent() {
045        return propertiesComponent;
046    }
047
048    public void setPropertiesComponent(PropertiesComponent propertiesComponent) {
049        this.propertiesComponent = propertiesComponent;
050    }
051
052    @Override
053    public String parseUri(String text, Properties properties, String prefixToken, String suffixToken) throws IllegalArgumentException {
054        return parseUri(text, properties, prefixToken, suffixToken, null, null, false, false);
055    }
056
057    @Override
058    public String parseUri(String text, Properties properties,
059                           String prefixToken, String suffixToken, String propertyPrefix, String propertySuffix,
060            boolean fallbackToUnaugmentedProperty, boolean defaultFallbackEnabled) throws IllegalArgumentException {
061        ParsingContext context = new ParsingContext(properties, prefixToken, suffixToken, propertyPrefix, propertySuffix, fallbackToUnaugmentedProperty, defaultFallbackEnabled);
062        return context.parse(text);
063    }
064
065    public String parseProperty(String key, String value, Properties properties) {
066        return value;
067    }
068
069    /**
070     * This inner class helps replacing properties.
071     */
072    private final class ParsingContext {
073        private final Properties properties;
074        private final String prefixToken;
075        private final String suffixToken;
076        private final String propertyPrefix;
077        private final String propertySuffix;
078        private final boolean fallbackToUnaugmentedProperty;
079        private final boolean defaultFallbackEnabled;
080
081        ParsingContext(Properties properties, String prefixToken, String suffixToken, String propertyPrefix, String propertySuffix,
082                              boolean fallbackToUnaugmentedProperty, boolean defaultFallbackEnabled) {
083            this.properties = properties;
084            this.prefixToken = prefixToken;
085            this.suffixToken = suffixToken;
086            this.propertyPrefix = propertyPrefix;
087            this.propertySuffix = propertySuffix;
088            this.fallbackToUnaugmentedProperty = fallbackToUnaugmentedProperty;
089            this.defaultFallbackEnabled = defaultFallbackEnabled;
090        }
091
092        /**
093         * Parses the given input string and replaces all properties
094         *
095         * @param input Input string
096         * @return Evaluated string
097         */
098        public String parse(String input) {
099            return doParse(input, new HashSet<String>());
100        }
101
102        /**
103         * Recursively parses the given input string and replaces all properties
104         *
105         * @param input                Input string
106         * @param replacedPropertyKeys Already replaced property keys used for tracking circular references
107         * @return Evaluated string
108         */
109        private String doParse(String input, Set<String> replacedPropertyKeys) {
110            if (input == null) {
111                return null;
112            }
113            String answer = input;
114            Property property;
115            while ((property = readProperty(answer)) != null) {
116                // Check for circular references
117                if (replacedPropertyKeys.contains(property.getKey())) {
118                    throw new IllegalArgumentException("Circular reference detected with key [" + property.getKey() + "] from text: " + input);
119                }
120
121                Set<String> newReplaced = new HashSet<String>(replacedPropertyKeys);
122                newReplaced.add(property.getKey());
123
124                String before = answer.substring(0, property.getBeginIndex());
125                String after = answer.substring(property.getEndIndex());
126                answer = before + doParse(property.getValue(), newReplaced) + after;
127            }
128            return answer;
129        }
130
131        /**
132         * Finds a property in the given string. It returns {@code null} if there's no property defined.
133         *
134         * @param input Input string
135         * @return A property in the given string or {@code null} if not found
136         */
137        private Property readProperty(String input) {
138            // Find the index of the first valid suffix token
139            int suffix = getSuffixIndex(input);
140
141            // If not found, ensure that there is no valid prefix token in the string
142            if (suffix == -1) {
143                if (getMatchingPrefixIndex(input, input.length()) != -1) {
144                    throw new IllegalArgumentException(String.format("Missing %s from the text: %s", suffixToken, input));
145                }
146                return null;
147            }
148
149            // Find the index of the prefix token that matches the suffix token
150            int prefix = getMatchingPrefixIndex(input, suffix);
151            if (prefix == -1) {
152                throw new IllegalArgumentException(String.format("Missing %s from the text: %s", prefixToken, input));
153            }
154
155            String key = input.substring(prefix + prefixToken.length(), suffix);
156            String value = getPropertyValue(key, input);
157            return new Property(prefix, suffix + suffixToken.length(), key, value);
158        }
159
160        /**
161         * Gets the first index of the suffix token that is not surrounded by quotes
162         *
163         * @param input Input string
164         * @return First index of the suffix token that is not surrounded by quotes
165         */
166        private int getSuffixIndex(String input) {
167            int index = -1;
168            do {
169                index = input.indexOf(suffixToken, index + 1);
170            } while (index != -1 && isQuoted(input, index, suffixToken));
171            return index;
172        }
173
174        /**
175         * Gets the index of the prefix token that matches the suffix at the given index and that is not surrounded by quotes
176         *
177         * @param input       Input string
178         * @param suffixIndex Index of the suffix token
179         * @return Index of the prefix token that matches the suffix at the given index and that is not surrounded by quotes
180         */
181        private int getMatchingPrefixIndex(String input, int suffixIndex) {
182            int index = suffixIndex;
183            do {
184                index = input.lastIndexOf(prefixToken, index - 1);
185            } while (index != -1 && isQuoted(input, index, prefixToken));
186            return index;
187        }
188
189        /**
190         * Indicates whether or not the token at the given index is surrounded by single or double quotes
191         *
192         * @param input Input string
193         * @param index Index of the token
194         * @param token Token
195         * @return {@code true}
196         */
197        private boolean isQuoted(String input, int index, String token) {
198            int beforeIndex = index - 1;
199            int afterIndex = index + token.length();
200            if (beforeIndex >= 0 && afterIndex < input.length()) {
201                char before = input.charAt(beforeIndex);
202                char after = input.charAt(afterIndex);
203                return (before == after) && (before == '\'' || before == '"');
204            }
205            return false;
206        }
207
208        /**
209         * Gets the value of the property with given key
210         *
211         * @param key   Key of the property
212         * @param input Input string (used for exception message if value not found)
213         * @return Value of the property with the given key
214         */
215        private String getPropertyValue(String key, String input) {
216
217            // the key may be a function, so lets check this first
218            if (propertiesComponent != null) {
219                for (PropertiesFunction function : propertiesComponent.getFunctions().values()) {
220                    String token = function.getName() + ":";
221                    if (key.startsWith(token)) {
222                        String remainder = key.substring(token.length());
223                        log.debug("Property with key [{}] is applied by function [{}]", key, function.getName());
224                        String value = function.apply(remainder);
225                        if (value == null) {
226                            throw new IllegalArgumentException("Property with key [" + key + "] using function [" + function.getName() + "]"
227                                    + " returned null value which is not allowed, from input: " + input);
228                        } else {
229                            if (log.isDebugEnabled()) {
230                                log.debug("Property with key [{}] applied by function [{}] -> {}", new Object[]{key, function.getName(), value});
231                            }
232                            return value;
233                        }
234                    }
235                }
236            }
237
238            // they key may have a get or else expression
239            String defaultValue = null;
240            if (defaultFallbackEnabled && key.contains(GET_OR_ELSE_TOKEN)) {
241                defaultValue = ObjectHelper.after(key, GET_OR_ELSE_TOKEN);
242                key = ObjectHelper.before(key, GET_OR_ELSE_TOKEN);
243            }
244
245            String augmentedKey = getAugmentedKey(key);
246            boolean shouldFallback = fallbackToUnaugmentedProperty && !key.equals(augmentedKey);
247
248            String value = doGetPropertyValue(augmentedKey);
249            if (value == null && shouldFallback) {
250                log.debug("Property with key [{}] not found, attempting with unaugmented key: {}", augmentedKey, key);
251                value = doGetPropertyValue(key);
252            }
253
254            if (value == null && defaultValue != null) {
255                log.debug("Property with key [{}] not found, using default value: {}", augmentedKey, defaultValue);
256                value = defaultValue;
257            }
258
259            if (value == null) {
260                StringBuilder esb = new StringBuilder();
261                if (propertiesComponent == null || propertiesComponent.isDefaultCreated()) {
262                    // if the component was auto created then include more information that the end user should define it
263                    esb.append("PropertiesComponent with name properties must be defined in CamelContext to support property placeholders. ");
264                }
265                esb.append("Property with key [").append(augmentedKey).append("] ");
266                if (shouldFallback) {
267                    esb.append("(and original key [").append(key).append("]) ");
268                }
269                esb.append("not found in properties from text: ").append(input);
270                throw new IllegalArgumentException(esb.toString());
271            }
272
273            return value;
274        }
275
276        /**
277         * Gets the augmented key of the given base key
278         *
279         * @param key Base key
280         * @return Augmented key
281         */
282        private String getAugmentedKey(String key) {
283            String augmentedKey = key;
284            if (propertyPrefix != null) {
285                log.debug("Augmenting property key [{}] with prefix: {}", key, propertyPrefix);
286                augmentedKey = propertyPrefix + augmentedKey;
287            }
288            if (propertySuffix != null) {
289                log.debug("Augmenting property key [{}] with suffix: {}", key, propertySuffix);
290                augmentedKey = augmentedKey + propertySuffix;
291            }
292            return augmentedKey;
293        }
294
295        /**
296         * Gets the property with the given key, it returns {@code null} if the property is not found
297         *
298         * @param key Key of the property
299         * @return Value of the property or {@code null} if not found
300         */
301        private String doGetPropertyValue(String key) {
302            if (ObjectHelper.isEmpty(key)) {
303                return parseProperty(key, null, properties);
304            }
305
306            String value = null;
307
308            // override is the default mode
309            int mode = propertiesComponent != null ? propertiesComponent.getSystemPropertiesMode() : PropertiesComponent.SYSTEM_PROPERTIES_MODE_OVERRIDE;
310
311            if (mode == PropertiesComponent.SYSTEM_PROPERTIES_MODE_OVERRIDE) {
312                value = System.getProperty(key);
313                if (value != null) {
314                    log.debug("Found a JVM system property: {} with value: {} to be used.", key, value);
315                }
316            }
317
318            if (value == null && properties != null) {
319                value = properties.getProperty(key);
320                if (value != null) {
321                    log.debug("Found property: {} with value: {} to be used.", key, value);
322                }
323            }
324
325            if (value == null && mode == PropertiesComponent.SYSTEM_PROPERTIES_MODE_FALLBACK) {
326                value = System.getProperty(key);
327                if (value != null) {
328                    log.debug("Found a JVM system property: {} with value: {} to be used.", key, value);
329                }
330            }
331
332            return parseProperty(key, value, properties);
333        }
334    }
335
336    /**
337     * This inner class is the definition of a property used in a string
338     */
339    private static final class Property {
340        private final int beginIndex;
341        private final int endIndex;
342        private final String key;
343        private final String value;
344
345        private Property(int beginIndex, int endIndex, String key, String value) {
346            this.beginIndex = beginIndex;
347            this.endIndex = endIndex;
348            this.key = key;
349            this.value = value;
350        }
351
352        /**
353         * Gets the begin index of the property (including the prefix token).
354         */
355        public int getBeginIndex() {
356            return beginIndex;
357        }
358
359        /**
360         * Gets the end index of the property (including the suffix token).
361         */
362        public int getEndIndex() {
363            return endIndex;
364        }
365
366        /**
367         * Gets the key of the property.
368         */
369        public String getKey() {
370            return key;
371        }
372
373        /**
374         * Gets the value of the property.
375         */
376        public String getValue() {
377            return value;
378        }
379    }
380}