All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.component.properties.DefaultPropertiesParser Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.properties;

import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.apache.camel.PropertiesLookupListener;
import org.apache.camel.spi.PropertiesFunction;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.OrderedLocationProperties;
import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.camel.spi.PropertiesComponent.OPTIONAL_TOKEN;
import static org.apache.camel.spi.PropertiesComponent.PREFIX_TOKEN;
import static org.apache.camel.spi.PropertiesComponent.SUFFIX_TOKEN;
import static org.apache.camel.util.IOHelper.lookupEnvironmentVariable;

/**
 * A parser to parse a string which contains property placeholders.
 */
public class DefaultPropertiesParser implements PropertiesParser {

    private static final String UNRESOLVED_PREFIX_TOKEN = "@@[";

    private static final String UNRESOLVED_SUFFIX_TOKEN = "]@@";

    private static final String GET_OR_ELSE_TOKEN = ":";

    protected final Logger log = LoggerFactory.getLogger(getClass());

    private PropertiesComponent propertiesComponent;

    public DefaultPropertiesParser() {
    }

    public DefaultPropertiesParser(PropertiesComponent propertiesComponent) {
        this.propertiesComponent = propertiesComponent;
    }

    public PropertiesComponent getPropertiesComponent() {
        return propertiesComponent;
    }

    public void setPropertiesComponent(PropertiesComponent propertiesComponent) {
        this.propertiesComponent = propertiesComponent;
    }

    @Override
    public String parseUri(
            String text, PropertiesLookup properties, boolean defaultFallbackEnabled, boolean keepUnresolvedOptional,
            boolean nestedPlaceholder)
            throws IllegalArgumentException {
        ParsingContext context
                = new ParsingContext(properties, defaultFallbackEnabled, keepUnresolvedOptional, nestedPlaceholder);
        String answer = context.parse(text);
        if (keepUnresolvedOptional && answer != null && answer.contains(UNRESOLVED_PREFIX_TOKEN)) {
            // replace temporary unresolved keys back to with placeholders so they are kept as-is
            answer = answer.replace(UNRESOLVED_PREFIX_TOKEN, PREFIX_TOKEN);
            answer = answer.replace(UNRESOLVED_SUFFIX_TOKEN, SUFFIX_TOKEN);
        }
        return answer;
    }

    @Override
    public String parseProperty(String key, String value, PropertiesLookup properties) {
        return value;
    }

    /**
     * This inner class helps replacing properties.
     */
    private final class ParsingContext {
        private final PropertiesLookup properties;
        private final boolean defaultFallbackEnabled;
        private final boolean keepUnresolvedOptional;
        private final boolean nestedPlaceholder;

        ParsingContext(PropertiesLookup properties, boolean defaultFallbackEnabled, boolean keepUnresolvedOptional,
                       boolean nestedPlaceholder) {
            this.properties = properties;
            this.defaultFallbackEnabled = defaultFallbackEnabled;
            this.keepUnresolvedOptional = keepUnresolvedOptional;
            this.nestedPlaceholder = nestedPlaceholder;
        }

        /**
         * Parses the given input string and replaces all properties
         *
         * @param  input Input string
         * @return       Evaluated string
         */
        public String parse(String input) {
            // does the key turn on or off nested?
            boolean nested = nestedPlaceholder;
            if (input.contains("?nested=true")) {
                nested = true;
                input = input.replace("?nested=true", "");
            } else if (input.contains("?nested=false")) {
                nested = false;
                input = input.replace("?nested=false", "");
            }
            if (nested) {
                return doParseNested(null, input, new HashSet<>());
            } else {
                return doParse(input);
            }
        }

        /**
         * Parses the given input string and replaces all properties (not nested)
         *
         * @param  input Input string
         * @return       Evaluated string
         */
        private String doParse(String input) {
            if (input == null) {
                return null;
            }

            StringBuilder answer = new StringBuilder(input.length());
            Property property;
            String prevKey = null;
            while ((property = readProperty(prevKey, input)) != null) {
                String before = input.substring(0, property.getBeginIndex());
                String after = input.substring(property.getEndIndex());
                String parsed = property.getValue();
                if (parsed != null) {
                    answer.append(before);
                    answer.append(parsed);
                } else if (property.getBeginIndex() == 0 && input.length() == property.getEndIndex()) {
                    // its only a single placeholder which is parsed as null
                    return null;
                }
                input = after;
                prevKey = property.getKey();
            }
            if (!input.isEmpty()) {
                answer.append(input);
            }
            return answer.toString();
        }

        /**
         * Recursively parses the given input string and replaces all properties
         *
         * @param  input                Input string
         * @param  replacedPropertyKeys Already replaced property keys used for tracking circular references
         * @return                      Evaluated string
         */
        private String doParseNested(String prevKey, String input, Set replacedPropertyKeys) {
            if (input == null) {
                return null;
            }
            String answer = input;
            Property property;
            while ((property = readProperty(prevKey, answer)) != null) {
                if (replacedPropertyKeys.contains(property.getKey())) {
                    // Check for circular references (skip optional)
                    boolean optional = property.getKey().startsWith(OPTIONAL_TOKEN);
                    if (optional) {
                        break;
                    } else {
                        throw new IllegalArgumentException(
                                "Circular reference detected with key [" + property.getKey() + "] from text: " + input);
                    }
                }

                if (propertiesComponent != null) {
                    // nested placeholder so update resolved property with new value
                    String k = prevKey != null ? prevKey : property.getKey();
                    propertiesComponent.updateResolvedValue(k, property.getValue(), null);
                }

                Set newReplaced = new HashSet<>(replacedPropertyKeys);
                newReplaced.add(property.getKey());

                int beginIndex = property.getBeginIndex();
                if (beginIndex > 0 && answer.charAt(beginIndex - 1) == '\\') {
                    // The escape character has been escaped, so we need to restore it
                    beginIndex--;
                }
                String before = answer.substring(0, beginIndex);
                String after = answer.substring(property.getEndIndex());
                String parsed = doParseNested(property.getKey(), property.getValue(), newReplaced);
                if (parsed != null) {
                    answer = before + parsed + after;
                } else {
                    if (beginIndex == 0 && input.length() == property.getEndIndex()) {
                        // its only a single placeholder which is parsed as null
                        answer = null;
                        break;
                    } else {
                        answer = before + after;
                    }
                }
            }
            return answer;
        }

        /**
         * Finds a property in the given string. It returns {@code null} if there's no property defined.
         *
         * @param  input Input string
         * @return       A property in the given string or {@code null} if not found
         */
        private Property readProperty(String prevKey, String input) {
            // Find the index of the first valid suffix token
            int suffix = getSuffixIndex(input);

            // If not found, ensure that there is no valid prefix token in the string
            if (suffix == -1) {
                if (getMatchingPrefixIndex(input, input.length()) != -1) {
                    throw new IllegalArgumentException(String.format("Missing %s from the text: %s", SUFFIX_TOKEN, input));
                }
                return null;
            }

            // Find the index of the prefix token that matches the suffix token
            int prefix = getMatchingPrefixIndex(input, suffix);
            if (prefix == -1) {
                throw new IllegalArgumentException(String.format("Missing %s from the text: %s", PREFIX_TOKEN, input));
            }

            String key = input.substring(prefix + PREFIX_TOKEN.length(), suffix);
            String value = getPropertyValue(prevKey, key, input);
            return new Property(prefix, suffix + SUFFIX_TOKEN.length(), key, value);
        }

        /**
         * Gets the first index of the suffix token that is not surrounded by quotes
         *
         * @param  input Input string
         * @return       First index of the suffix token that is not surrounded by quotes
         */
        private int getSuffixIndex(String input) {
            int index = -1;
            do {
                index = input.indexOf(SUFFIX_TOKEN, index + 1);
            } while (index != -1 && (isQuoted(input, index, SUFFIX_TOKEN) || isEscaped(input, index - 1)));
            return index;
        }

        /**
         * Gets the index of the prefix token that matches the suffix at the given index and that is not surrounded by
         * quotes
         *
         * @param  input       Input string
         * @param  suffixIndex Index of the suffix token
         * @return             Index of the prefix token that matches the suffix at the given index and that is not
         *                     surrounded by quotes
         */
        private int getMatchingPrefixIndex(String input, int suffixIndex) {
            int index = suffixIndex;
            do {
                index = input.lastIndexOf(PREFIX_TOKEN, index - 1);
            } while (index != -1 && (isQuoted(input, index, PREFIX_TOKEN) || isEscaped(input, index - 1)));
            return index;
        }

        /**
         * Indicates whether the token at the given index is surrounded by single or double quotes
         *
         * @param  input Input string
         * @param  index Index of the token
         * @param  token Token
         * @return       {@code true}
         */
        private boolean isQuoted(String input, int index, String token) {
            int beforeIndex = index - 1;
            int afterIndex = index + token.length();
            if (beforeIndex >= 0 && afterIndex < input.length()) {
                char before = input.charAt(beforeIndex);
                char after = input.charAt(afterIndex);
                return before == after && (before == '\'' || before == '"');
            }
            return false;
        }

        /**
         * Indicates whether the escape character is at the given index.
         *
         * @param  input Input string
         * @param  index Index where the escape character is checked.
         * @return       {@code true} if the escape character is at the given index, and it is not itself escaped,
         *               {@code false} otherwise.
         */
        private boolean isEscaped(String input, int index) {
            if (index >= 0) {
                return input.charAt(index) == '\\' && (index == 0 || input.charAt(index - 1) != '\\');
            }
            return false;
        }

        /**
         * Gets the value of the property with given key
         *
         * @param  key   Key of the property
         * @param  input Input string (used for exception message if value not found)
         * @return       Value of the property with the given key
         */
        private String getPropertyValue(String prevKey, String key, String input) {
            if (key == null) {
                return null;
            }

            boolean optional = key.startsWith(OPTIONAL_TOKEN);
            if (optional) {
                key = key.substring(OPTIONAL_TOKEN.length());
            }

            // the key may be a function, so lets check this first
            if (propertiesComponent != null) {
                String prefix = StringHelper.before(key, ":");
                PropertiesFunction function = propertiesComponent.getPropertiesFunction(prefix);
                if (function != null) {
                    String remainder = StringHelper.after(key, ":");
                    boolean remainderOptional = remainder.startsWith(OPTIONAL_TOKEN);
                    if (function.lookupFirst(remainder)) {
                        String value = getPropertyValue(prevKey, remainder, input);
                        if (value == null && (remainderOptional || function.optional(remainder))) {
                            return null;
                        }
                        // it was not possible to resolve
                        if (value != null && value.startsWith(UNRESOLVED_PREFIX_TOKEN)) {
                            return value;
                        } else {
                            remainder = value;
                        }
                    }
                    log.debug("Property with key [{}] is applied by function [{}]", key, function.getName());
                    String value = function.apply(remainder);
                    if (value == null) {
                        if (!remainderOptional) {
                            remainderOptional = function.optional(remainder);
                        }
                        if (!remainderOptional && propertiesComponent != null
                                && propertiesComponent.isIgnoreMissingProperty()) {
                            // property is missing, but we should ignore this and return the placeholder unresolved
                            return UNRESOLVED_PREFIX_TOKEN + key + UNRESOLVED_SUFFIX_TOKEN;
                        }
                        if (!remainderOptional) {
                            throw new IllegalArgumentException(
                                    "Property with key [" + key + "] using function [" + function.getName() + "]"
                                                               + " returned null value which is not allowed, from input: "
                                                               + input);
                        } else {
                            if (keepUnresolvedOptional) {
                                // mark the key as unresolved
                                return UNRESOLVED_PREFIX_TOKEN + OPTIONAL_TOKEN + key + UNRESOLVED_SUFFIX_TOKEN;
                            } else {
                                return null;
                            }
                        }
                    } else {
                        if (log.isDebugEnabled()) {
                            log.debug("Property with key [{}] applied by function [{}] -> {}", key, function.getName(),
                                    value);
                        }
                        String k = prevKey != null ? prevKey : key;
                        propertiesComponent.updateResolvedValue(k, value, function.getName());
                        return value;
                    }
                }
            }

            // they key may have a get or else expression
            String defaultValue = null;
            if (defaultFallbackEnabled && key.contains(GET_OR_ELSE_TOKEN)) {
                defaultValue = StringHelper.after(key, GET_OR_ELSE_TOKEN);
                key = StringHelper.before(key, GET_OR_ELSE_TOKEN);
            }

            String value = doGetPropertyValue(key, defaultValue);
            if (value == null && defaultValue != null) {
                log.debug("Property with key [{}] not found, using default value: {}", key, defaultValue);
                value = defaultValue;
                for (PropertiesLookupListener listener : propertiesComponent.getPropertiesLookupListeners()) {
                    try {
                        listener.onLookup(key, value, defaultValue, null);
                    } catch (Exception e) {
                        // ignore
                    }
                }
            }

            if (value == null) {
                if (!optional && propertiesComponent != null && propertiesComponent.isIgnoreMissingProperty()) {
                    // property is missing, but we should ignore this and return the placeholder unresolved
                    return UNRESOLVED_PREFIX_TOKEN + key + UNRESOLVED_SUFFIX_TOKEN;
                }
                if (!optional) {
                    StringBuilder esb = new StringBuilder(256);
                    esb.append("Property with key [").append(key).append("] ");
                    esb.append("not found in properties from text: ").append(input);
                    throw new IllegalArgumentException(esb.toString());
                } else {
                    if (keepUnresolvedOptional) {
                        // mark the key as unresolved
                        return UNRESOLVED_PREFIX_TOKEN + OPTIONAL_TOKEN + key + UNRESOLVED_SUFFIX_TOKEN;
                    } else {
                        return null;
                    }
                }
            }

            return value;
        }

        /**
         * Gets the property with the given key, it returns {@code null} if the property is not found
         *
         * @param  key Key of the property
         * @return     Value of the property or {@code null} if not found
         */
        private String doGetPropertyValue(String key, String defaultValue) {
            if (ObjectHelper.isEmpty(key)) {
                return parseProperty(key, null, properties);
            }

            String value = null;

            // favour local properties if
            Properties local = propertiesComponent != null ? propertiesComponent.getLocalProperties() : null;
            if (local != null) {
                value = local.getProperty(key);
                if (value != null) {
                    String localDefaultValue = null;
                    String loc = location(local, key, "LocalProperties");
                    if (local instanceof OrderedLocationProperties propSource) {
                        Object val = propSource.getDefaultValue(key);
                        if (val != null) {
                            localDefaultValue
                                    = propertiesComponent.getCamelContext().getTypeConverter().tryConvertTo(String.class, val);
                        }
                    }
                    onLookup(key, value, localDefaultValue, loc);
                    log.debug("Found local property: {} with value: {} to be used.", key, value);
                }
            }

            // override is the default mode for ENV
            int envMode = propertiesComponent != null
                    ? propertiesComponent.getEnvironmentVariableMode()
                    : PropertiesComponent.ENVIRONMENT_VARIABLES_MODE_FALLBACK;
            // override is the default mode for SYS
            int sysMode = propertiesComponent != null
                    ? propertiesComponent.getSystemPropertiesMode() : PropertiesComponent.SYSTEM_PROPERTIES_MODE_OVERRIDE;

            if (value == null && envMode == PropertiesComponent.ENVIRONMENT_VARIABLES_MODE_OVERRIDE) {
                value = lookupEnvironmentVariable(key);
                if (value != null) {
                    onLookup(key, value, defaultValue, "ENV");
                    log.debug("Found an OS environment property: {} with value: {} to be used.", key, value);
                }
            }
            if (value == null && sysMode == PropertiesComponent.SYSTEM_PROPERTIES_MODE_OVERRIDE) {
                value = System.getProperty(key);
                if (value != null) {
                    onLookup(key, value, defaultValue, "SYS");
                    log.debug("Found a JVM system property: {} with value: {} to be used.", key, value);
                }
            }

            if (value == null && properties != null) {
                value = properties.lookup(key, defaultValue);
                if (value != null) {
                    log.debug("Found property: {} with value: {} to be used.", key, value);
                }
            }

            if (value == null) {
                // custom lookup in spring boot or other runtimes
                value = customLookup(key);
                if (value != null) {
                    log.debug("Found property (custom lookup): {} with value: {} to be used.", key, value);
                }
            }

            if (value == null && envMode == PropertiesComponent.ENVIRONMENT_VARIABLES_MODE_FALLBACK) {
                value = lookupEnvironmentVariable(key);
                if (value != null) {
                    onLookup(key, value, defaultValue, "ENV");
                    log.debug("Found an OS environment property: {} with value: {} to be used.", key, value);
                }
            }
            if (value == null && sysMode == PropertiesComponent.SYSTEM_PROPERTIES_MODE_FALLBACK) {
                value = System.getProperty(key);
                if (value != null) {
                    onLookup(key, value, defaultValue, "SYS");
                    log.debug("Found a JVM system property: {} with value: {} to be used.", key, value);
                }
            }

            // parse property may return null (such as when using route templates)
            String answer = parseProperty(key, value, properties);
            if (answer == null) {
                answer = value;
            }
            return answer;
        }
    }

    private void onLookup(String name, String value, String defaultValue, String source) {
        for (PropertiesLookupListener listener : propertiesComponent.getPropertiesLookupListeners()) {
            try {
                listener.onLookup(name, value, defaultValue, source);
            } catch (Exception e) {
                // ignore
            }
        }
    }

    private static String location(Properties prop, String name, String defaultLocation) {
        String loc = null;
        if (prop instanceof OrderedLocationProperties olp) {
            loc = olp.getLocation(name);
        }
        if (loc == null) {
            loc = defaultLocation;
        }
        return loc;
    }

    /**
     * This inner class is the definition of a property used in a string
     */
    private static final class Property {
        private final int beginIndex;
        private final int endIndex;
        private final String key;
        private final String value;

        private Property(int beginIndex, int endIndex, String key, String value) {
            this.beginIndex = beginIndex;
            this.endIndex = endIndex;
            this.key = key;
            this.value = value;
        }

        /**
         * Gets the beginning index of the property (including the prefix token).
         */
        public int getBeginIndex() {
            return beginIndex;
        }

        /**
         * Gets the ending index of the property (including the suffix token).
         */
        public int getEndIndex() {
            return endIndex;
        }

        /**
         * Gets the key of the property.
         */
        public String getKey() {
            return key;
        }

        /**
         * Gets the value of the property.
         */
        public String getValue() {
            return value;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy