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

io.micronaut.context.env.DefaultPropertyPlaceholderResolver Maven / Gradle / Ivy

/*
 * Copyright 2017-2020 original authors
 *
 * Licensed 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
 *
 * https://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 io.micronaut.context.env;

import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.io.service.SoftServiceLoader;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.value.PropertyResolver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The default {@link PropertyPlaceholderResolver}.
 *
 * @author graemerocher
 * @since 1.0
 */
public class DefaultPropertyPlaceholderResolver implements PropertyPlaceholderResolver, AutoCloseable {

    /**
     * Prefix for placeholder in properties.
     */
    public static final String PREFIX = "${";

    /**
     * Suffix for placeholder in properties.
     */
    public static final String SUFFIX = "}";

    private static final Pattern ESCAPE_SEQUENCE = Pattern.compile("(.+)?:`([^`]+?)`");
    private static final char COLON = ':';

    private final PropertyResolver environment;
    private final ConversionService conversionService;
    private final String prefix;
    private Collection expressionResolvers;

    /**
     * @param environment The property resolver for the environment
     * @param conversionService The conversion service
     */
    public DefaultPropertyPlaceholderResolver(PropertyResolver environment, ConversionService conversionService) {
        this.environment = environment;
        this.conversionService = conversionService;
        this.prefix = PREFIX;
    }

    private Collection getExpressionResolvers() {
        Collection exResolvers = this.expressionResolvers;
        if (exResolvers == null) {
            synchronized (this) { // double check
                exResolvers = this.expressionResolvers;
                if (exResolvers == null) {
                    exResolvers = new ArrayList<>();
                    ClassLoader classLoader = (environment instanceof Environment e) ? e.getClassLoader() : environment.getClass().getClassLoader();
                    SoftServiceLoader.load(PropertyExpressionResolver.class, classLoader).collectAll(exResolvers);
                    this.expressionResolvers = exResolvers;
                }
            }
        }
        return exResolvers;
    }

    @Override
    public String getPrefix() {
        return this.prefix;
    }

    @Override
    public Optional resolvePlaceholders(String str) {
        List segments = buildSegments(str, false);
        StringBuilder value = new StringBuilder();
        for (Segment segment: segments) {
            Optional resolved = segment.findValue(String.class);
            if (resolved.isPresent()) {
                value.append(resolved.get());
            } else {
                return Optional.empty();
            }
        }
        return Optional.of(value.toString());
    }

    @Override
    public String resolveRequiredPlaceholders(String str) throws ConfigurationException {
        List segments = buildSegments(str);
        return resolveRequiredPlaceholdersString(segments);
    }

    @Override
    public Object resolveRequiredPlaceholdersObject(String str) throws ConfigurationException {
        List segments = buildSegments(str);
        if (segments.size() == 1) {
            return segments.get(0).getValue(Object.class);
        }
        return resolveRequiredPlaceholdersString(segments);
    }

    private static String resolveRequiredPlaceholdersString(List segments) {
        StringBuilder value = new StringBuilder();
        for (Segment segment: segments) {
            value.append(segment.getValue(String.class));
        }
        return value.toString();
    }

    @Override
    public  T resolveRequiredPlaceholder(String str, Class type) throws ConfigurationException {
        List segments = buildSegments(str);
        if (segments.size() == 1) {
            return segments.get(0).getValue(type);
        } else {
            throw new ConfigurationException("Cannot convert a multi segment placeholder to a specified type");
        }
    }

    @Override
    public  Optional resolveOptionalPlaceholder(String str, Class type) throws ConfigurationException {
        List segments = buildSegments(str, false);
        if (segments.size() == 1) {
            return segments.get(0).findValue(type);
        } else {
            return Optional.empty();
        }
    }

    /**
     * Split a placeholder value into logic segments.
     *
     * @param str The placeholder
     * @return The list of segments
     */
    public List buildSegments(String str) {
        return buildSegments(str, true);
    }

    /**
     * Split a placeholder value into logic segments.
     *
     * @param str The placeholder
     * @param failOnIncomplete True if should fail on incomplete placeholder, empty list will be returned otherwise
     * @return The list of segments
     * @since 4.2.0
     */
    private List buildSegments(String str, boolean failOnIncomplete) {
        List segments = new ArrayList<>();
        String value = str;
        int i = value.indexOf(PREFIX);
        while (i > -1) {
            //the text before the prefix
            if (i > 0) {
                String rawSegment = value.substring(0, i);
                segments.add(new RawSegment(rawSegment));
            }
            //everything after the prefix
            value = value.substring(i + PREFIX.length());
            int suffixIdx = value.indexOf(SUFFIX);
            if (suffixIdx > -1) {
                String expr = value.substring(0, suffixIdx).trim();
                segments.add(new PlaceholderSegment(expr));
                value = value.substring(suffixIdx + SUFFIX.length());
            } else if (failOnIncomplete) {
                throw new ConfigurationException("Incomplete placeholder definitions detected: " + str);
            } else {
                return List.of();
            }
            i = value.indexOf(PREFIX);
        }
        if (!value.isEmpty()) {
            segments.add(new RawSegment(value));
        }
        return segments;
    }

    /**
     * Resolves a single expression.
     *
     * @param context The context of the expression
     * @param expression The expression
     * @param type The class
     * @param  The type the expression should be converted to
     * @return The resolved and converted expression
     */
    @Nullable
    protected  T resolveExpression(String context, String expression, Class type) {
        for (PropertyExpressionResolver expressionResolver : getExpressionResolvers()) {
            Optional value = expressionResolver.resolve(environment, conversionService, expression, type);
            if (value.isPresent()) {
                return value.get();
            }
        }
        if (environment.containsProperty(expression)) {
            return environment.getProperty(expression, type)
                    .orElseThrow(() ->
                            new ConfigurationException("Could not resolve expression: [" + expression + "] in placeholder ${" + context + "}"));
        }
        if (NameUtils.isEnvironmentName(expression)) {
            String envVar = CachedEnvironment.getenv(expression);
            if (StringUtils.isNotEmpty(envVar)) {
                return conversionService.convert(envVar, type)
                        .orElseThrow(() ->
                                new ConfigurationException("Could not resolve expression: [" + expression + "] in placeholder ${" + context + "}"));
            }
        }
        return null;
    }

    /**
     * Resolves a single optional expression.
     *
     * @param expression The expression
     * @param type The class
     * @param  The type the expression should be converted to
     * @return The resolved and converted expression
     */
    private  Optional resolveOptionalExpression(String expression, Class type) {
        for (PropertyExpressionResolver expressionResolver : getExpressionResolvers()) {
            Optional value = expressionResolver.resolve(environment, conversionService, expression, type);
            if (value.isPresent()) {
                return value;
            }
        }
        Optional property = environment.getProperty(expression, type);
        if (property.isPresent()) {
            return property;
        }
        if (NameUtils.isEnvironmentName(expression)) {
            String envVar = CachedEnvironment.getenv(expression);
            if (StringUtils.isNotEmpty(envVar)) {
                return conversionService.convert(envVar, type);
            }
        }
        return Optional.empty();
    }

    @Override
    public void close() throws Exception {
        if (expressionResolvers != null) {
            for (PropertyExpressionResolver expressionResolver : expressionResolvers) {
                if (expressionResolver instanceof AutoCloseable closeable) {
                    closeable.close();
                }
            }
        }
    }

    /**
     * A segment of placeholder resolution.
     *
     * @author James Kleeh
     * @since 1.1.0
     */
    public interface Segment {

        /**
         * Returns the value of a given segment converted to
         * the provided type.
         *
         * @param type The class
         * @param  The type to convert the value to
         * @return The converted value
         * @throws ConfigurationException If any error occurs
         */
        T getValue(Class type) throws ConfigurationException;

        /**
         * Returns the optional value of a given segment converted to
         * the provided type. Any conversions errors are ignored.
         *
         * @param type The class
         * @param  The type to convert the value to
         * @return The converted optional value
         * @since 4.2.0
         */
       default  Optional findValue(Class type) {
           try {
               return Optional.of(getValue(type));
           } catch (ConfigurationException e) {
               return Optional.empty();
           }
       }
    }

    /**
     * A segment that represents static text.
     *
     * @author James Kleeh
     * @since 1.1.0
     */
    public class RawSegment implements Segment {

        private final String text;

        /**
         * Default constructor.
         *
         * @param text The static text
         */
        RawSegment(String text) {
            this.text = text;
        }

        @Override
        public  T getValue(Class type) throws ConfigurationException {
            if (type.isInstance(text)) {
                return (T) text;
            } else {
                return conversionService.convert(text, type)
                        .orElseThrow(() ->
                                new ConfigurationException("Could not convert: [" + text + "] to the required type: [" + type.getName() + "]"));
            }
        }

        @Override
        public  Optional findValue(Class type) {
            if (type.isInstance(text)) {
                return Optional.of((T) text);
            } else {
                return Optional.empty();
            }
        }
    }

    /**
     * A segment that represents one or more expressions
     * that should be searched for in the environment.
     *
     * @author James Kleeh
     * @since 1.1.0
     */
    public class PlaceholderSegment implements Segment {

        private final String placeholder;
        private final List expressions = new ArrayList<>();
        private String defaultValue;

        /**
         * Default constructor.
         *
         * @param placeholder The placeholder value without
         *                    any prefix or suffix
         */
        PlaceholderSegment(String placeholder) {
            this.placeholder = placeholder;
            findExpressions(placeholder);
        }

        /**
         * @return The list of expressions that may be looked
         * up in the environment
         */
        public List getExpressions() {
            return Collections.unmodifiableList(expressions);
        }

        @Override
        public  T getValue(Class type) throws ConfigurationException {
            for (String expression: expressions) {
                T value = resolveExpression(placeholder, expression, type);
                if (value != null) {
                    return value;
                }
            }
            if (defaultValue != null) {
                return conversionService.convert(defaultValue, type)
                        .orElseThrow(() ->
                                new ConfigurationException("Could not convert default value [%s] in placeholder ${%s}".formatted(defaultValue, placeholder)));
            } else {
                throw new ConfigurationException("Could not resolve placeholder ${" + placeholder + "}");
            }
        }

        @Override
        public  Optional findValue(Class type) {
            try {
                for (String expression: expressions) {
                    Optional optionalValue = resolveOptionalExpression(expression, type);
                    if (optionalValue.isPresent()) {
                        return optionalValue;
                    }
                }
                if (defaultValue != null) {
                    return conversionService.convert(defaultValue, type);
                }
            } catch (ConfigurationException e) {
                // Swallow exception.
            }
            return Optional.empty();
        }

        private void findExpressions(String placeholder) {
            String defaultValue = null;
            String expression;
            Matcher matcher = ESCAPE_SEQUENCE.matcher(placeholder);

            boolean escaped = false;
            if (matcher.find()) {
                defaultValue = matcher.group(2);
                expression = matcher.group(1);
                escaped = true;
            } else {
                int j = placeholder.indexOf(COLON);
                if (j > -1) {
                    defaultValue = placeholder.substring(j + 1);
                    expression = placeholder.substring(0, j);
                } else {
                    expression = placeholder;
                }
            }

            expressions.add(expression);

            if (defaultValue != null) {
                if (!escaped && (ESCAPE_SEQUENCE.matcher(defaultValue).find() || defaultValue.indexOf(COLON) > -1)) {
                    findExpressions(defaultValue);
                } else {
                    this.defaultValue = defaultValue;
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy