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