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

io.helidon.microprofile.config.ConfigCdiExtension Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2022, 2024 Contributors to Eclipse Foundation.
 * Copyright (c) 2018, 2021 Oracle and/or its affiliates.
 *
 * 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
 *
 *     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 io.helidon.microprofile.config;

import io.helidon.common.NativeImageHelper;
import io.helidon.config.ConfigException;
import io.helidon.config.mp.MpConfig;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.AfterDeploymentValidation;
import jakarta.enterprise.inject.spi.Annotated;
import jakarta.enterprise.inject.spi.AnnotatedField;
import jakarta.enterprise.inject.spi.AnnotatedMethod;
import jakarta.enterprise.inject.spi.AnnotatedParameter;
import jakarta.enterprise.inject.spi.AnnotatedType;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.Extension;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
import jakarta.enterprise.inject.spi.ProcessBean;
import jakarta.enterprise.inject.spi.ProcessObserverMethod;
import jakarta.enterprise.inject.spi.ProcessSyntheticObserverMethod;
import jakarta.enterprise.inject.spi.WithAnnotations;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.ConfigValue;
import org.eclipse.microprofile.config.inject.ConfigProperties;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.config.spi.Converter;

import static java.util.Optional.ofNullable;

/**
 * Extension to enable config injection in CDI container (all of {@link io.helidon.config.Config},
 * {@link org.eclipse.microprofile.config.Config} and {@link ConfigProperty}).
 */
public class ConfigCdiExtension implements Extension {
    private static final Logger LOGGER = Logger.getLogger(ConfigCdiExtension.class.getName());
    private static final Pattern SPLIT_PATTERN = Pattern.compile("(?, Class> REPLACED_TYPES = new HashMap<>();

    static {
        // this code is duplicated in mapper manager in Config
        REPLACED_TYPES.put(byte.class, Byte.class);
        REPLACED_TYPES.put(short.class, Short.class);
        REPLACED_TYPES.put(int.class, Integer.class);
        REPLACED_TYPES.put(long.class, Long.class);
        REPLACED_TYPES.put(float.class, Float.class);
        REPLACED_TYPES.put(double.class, Double.class);
        REPLACED_TYPES.put(boolean.class, Boolean.class);
        REPLACED_TYPES.put(char.class, Character.class);
    }

    private final List ips = new LinkedList<>();
    private final Map, ConfigBeanDescriptor> configBeans = new HashMap<>();

    /**
     * Constructor invoked by CDI container.
     */
    public ConfigCdiExtension() {
        LOGGER.fine("ConfigCdiExtension instantiated");
    }

    public void harvestConfigPropertyInjectionPointsFromEnabledBean(@Observes ProcessBean event) {
        Bean bean = event.getBean();
        Set beanInjectionPoints = bean.getInjectionPoints();
        if (beanInjectionPoints != null) {
            for (InjectionPoint beanInjectionPoint : beanInjectionPoints) {
                if (beanInjectionPoint != null) {
                    Set qualifiers = beanInjectionPoint.getQualifiers();
                    assert qualifiers != null;
                    for (Annotation qualifier : qualifiers) {
                        if (qualifier instanceof ConfigProperty) {
                            ips.add(beanInjectionPoint);
                        }
                    }
                }
            }
        }
    }

    public void processAnnotatedType(@Observes @WithAnnotations(ConfigProperties.class) ProcessAnnotatedType event) {
        AnnotatedType annotatedType = event.getAnnotatedType();
        ConfigProperties configProperties = annotatedType.getAnnotation(ConfigProperties.class);
        if (configProperties == null) {
            // Ignore classes that do not have this annotation on class level
            return;
        }

        configBeans.put(annotatedType.getJavaClass(), ConfigBeanDescriptor.create(annotatedType.getJavaClass(), configProperties));

        // We must veto this annotated type, as we need to create a custom bean to create an instance
        event.veto();
    }

    public  void harvestConfigPropertyInjectionPointsFromEnabledObserverMethod(@Observes ProcessObserverMethod event,
                                                                                   BeanManager beanManager) {
        // Synthetic events won't have an annotated method
        if (event instanceof ProcessSyntheticObserverMethod) {
            return;
        }

        ofNullable(event.getAnnotatedMethod())
                .map(AnnotatedMethod::getParameters)
                .stream().flatMap(Collection::stream)
                .filter(parameter -> !parameter.isAnnotationPresent(Observes.class)
                                && parameter.isAnnotationPresent(ConfigProperty.class))
                .map(beanManager::createInjectionPoint)
                .forEach(ips::add);
    }

    /**
     * Register a config producer bean for each {@link org.eclipse.microprofile.config.inject.ConfigProperty} injection.
     *
     * @param abd event from CDI container
     */
    private void registerConfigProducer(@Observes AfterBeanDiscovery abd) {
        // we also must support injection of Config itself
        abd.addBean()
                .addTransitiveTypeClosure(org.eclipse.microprofile.config.Config.class)
                .beanClass(org.eclipse.microprofile.config.Config.class)
                .scope(ApplicationScoped.class)
                .createWith(creationalContext -> new SerializableConfig());

        abd.addBean()
                .addTransitiveTypeClosure(io.helidon.config.Config.class)
                .beanClass(io.helidon.config.Config.class)
                .scope(ApplicationScoped.class)
                .createWith(creationalContext -> {
                    Config config = ConfigProvider.getConfig();
                    if (config instanceof io.helidon.config.Config) {
                        return config;
                    } else {
                        return MpConfig.toHelidonConfig(config);
                    }
                });

        Set types = ips.stream()
                .map(InjectionPoint::getType)
                .map(it -> {
                    if (it instanceof Class) {
                        Class clazz = (Class) it;
                        if (clazz.isPrimitive()) {
                            return REPLACED_TYPES.getOrDefault(clazz, clazz);
                        }
                    }
                    return it;
                })
                .collect(Collectors.toSet());

        types.forEach(type -> {
            abd.addBean()
                    .addType(type)
                    .scope(Dependent.class)
                    .addQualifier(CONFIG_PROPERTY_LITERAL)
                    .produceWith(it -> produce(it.select(InjectionPoint.class).get()));
        });

        configBeans.values().forEach(beanDescriptor -> abd.addBean()
                .addType(beanDescriptor.type())
                .addTransitiveTypeClosure(beanDescriptor.type())
                // it is non-binding
                .qualifiers(ConfigProperties.Literal.NO_PREFIX)
                .scope(Dependent.class)
                .produceWith(it -> beanDescriptor.produce(it.select(InjectionPoint.class).get(), ConfigProvider.getConfig())));
    }

    /**
     * Validate all injection points are valid.
     *
     * @param add event from CDI container
     */
    private void validate(@Observes AfterDeploymentValidation add, BeanManager beanManager) {
        CreationalContext cc = beanManager.createCreationalContext(null);
        try {
            ips.forEach(ip -> {
                try {
                    beanManager.getInjectableReference(ip, cc);
                } catch (NoSuchElementException e) {
                    add.addDeploymentProblem(new ConfigException("Failed to validate injection point: " + ip, e));
                } catch (Exception e) {
                    add.addDeploymentProblem(e);
                }
            });
        } finally {
            cc.release();
        }
        ips.clear();
    }

    private Object produce(InjectionPoint ip) {
        ConfigProperty annotation = ip.getAnnotated().getAnnotation(ConfigProperty.class);
        String injectedName = injectedName(ip);
        String fullPath = ip.getMember().getDeclaringClass().getName()
                + "." + injectedName;
        String configKey = configKey(annotation, fullPath);

        if (NativeImageHelper.isBuildTime()) {
            // this is build-time of native-image - e.g. run from command line or maven
            // logging may not be working/configured to deliver this message as it should
            System.err.println("You are accessing configuration key '" + configKey + "' during"
                    + " container initialization. This will not work nicely with Graal native-image");
        }

        return produce(configKey, ip.getType(), defaultValue(annotation));
    }

    private Object produce(String configKey, Type type, String defaultValue) {
        /*
             Supported types
             group x:
                primitive types + String
                any java class (except for parameterized types)
             group y = group x + the ones listed here:
                List - where x is one of the above
                x[] - where x is one of the above

             group z:
                Provider
                Optional
                Supplier

             group z':
                Map - a detached key/value mapping of whole subtree
             */
        FieldTypes fieldTypes = FieldTypes.create(type);
        org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig();

        try {
            config = config.unwrap(Config.class);
        } catch (Exception ignored) {
            // this is to get the delegated config instance from MpConfigProviderResolver
        }

        if (fieldTypes.field0().rawType().equals(ConfigValue.class)) {
            ConfigValue configValue = config.getConfigValue(configKey);
            if (defaultValue != null && configValue.getValue() == null) {
                return new DefaultConfigValue(configKey, defaultValue);
            } else {
                return configValue;
            }
        }

        Object value = configValue(config, fieldTypes, configKey, defaultValue);

        if (value == null) {
            throw new NoSuchElementException("Cannot find value for key: " + configKey);
        }
        return value;
    }

    private Object configValue(Config config, FieldTypes fieldTypes, String configKey, String defaultValue) {
        Class type0 = fieldTypes.field0().rawType();
        Class type1 = fieldTypes.field1().rawType();
        Class type2 = fieldTypes.field2().rawType();
        if (type0.equals(type1)) {
            // not a generic
            return withDefault(config, configKey, type0, defaultValue, true);
        }

        // generic declaration
        return parameterizedConfigValue(config,
                configKey,
                defaultValue,
                type0,
                type1,
                type2);
    }

    private static  T withDefault(Config config, String key, Class type, String configuredDefault, boolean required) {
        String defaultValue = (configuredDefault == null || configuredDefault.isEmpty()) ? null : configuredDefault;
        // our type may be one of the explicit optionals
        if (OptionalInt.class.equals(type)) {
            return type.cast(config.getOptionalValue(key, Integer.class)
                    .map(OptionalInt::of)
                    .orElseGet(OptionalInt::empty));
        } else if (OptionalLong.class.equals(type)) {
            return type.cast(config.getOptionalValue(key, Long.class)
                    .map(OptionalLong::of)
                    .orElseGet(OptionalLong::empty));
        } else if (OptionalDouble.class.equals(type)) {
            return type.cast(config.getOptionalValue(key, Double.class)
                    .map(OptionalDouble::of)
                    .orElseGet(OptionalDouble::empty));
        }

        // If converter returns null, we should not resolve default value
        Optional stringValue = config.getOptionalValue(key, String.class);

        if (stringValue.isEmpty()) {
            return convert(key, config, defaultValue, type);
        }

        // we have a value
        Converter converter = config.getConverter(type)
                .orElseThrow(() -> new IllegalArgumentException("There is no converter for type \"" + type
                        .getName() + "\""));

        T value = converter.convert(stringValue.get());
        if (value == null && required) {
            throw new NoSuchElementException(
                    "Converter returned null for a required property. This is not allowed as per section 6.4. of "
                            + "the specification. Key: " + key + ", configured value: " + stringValue + ", converter: "
                            + converter.getClass().getName());
        }
        return value;
    }

    @SuppressWarnings("unchecked")
    private static  T convert(String key, Config config, String value, Class type) {
        if (null == value) {
            return null;
        }
        if (String.class.equals(type)) {
            return (T) value;
        }

        return config.getConverter(type)
                .orElseThrow(() -> new IllegalArgumentException("Did not find converter for type "
                        + type.getName()
                        + ", for key "
                        + key))
                .convert(value);
    }

    private static Object parameterizedConfigValue(Config config,
                                                   String configKey,
                                                   String defaultValue,
                                                   Class rawType,
                                                   Class typeArg,
                                                   Class typeArg2) {
        if (Optional.class.isAssignableFrom(rawType)) {
            if (typeArg.equals(typeArg2)) {
                return ofNullable(withDefault(config, configKey, typeArg, defaultValue, false));
            } else {
                return ofNullable(parameterizedConfigValue(config,
                                configKey,
                                defaultValue,
                                typeArg,
                                typeArg2,
                                typeArg2));
            }
        } else if (List.class.isAssignableFrom(rawType)) {
            return asList(config, configKey, typeArg, defaultValue);
        } else if (Supplier.class.isAssignableFrom(rawType)) {
            if (typeArg.equals(typeArg2)) {
                return (Supplier) () -> withDefault(config, configKey, typeArg, defaultValue, true);
            } else {
                return (Supplier) () -> parameterizedConfigValue(config,
                        configKey,
                        defaultValue,
                        typeArg,
                        typeArg2,
                        typeArg2);
            }
        } else if (Map.class.isAssignableFrom(rawType)) {
            Map result = new HashMap<>();
            config.getPropertyNames()
                    .forEach(name -> {
                        // workaround for race condition (if key disappears from source after we call getPropertyNames
                        config.getOptionalValue(name, String.class).ifPresent(value -> result.put(name, value));
                    });
            return result;
        } else if (Set.class.isAssignableFrom(rawType)) {
            return new LinkedHashSet<>(asList(config, configKey, typeArg, defaultValue));
        } else {
            throw new IllegalArgumentException("Cannot create config property for " + rawType + "<" + typeArg + ">, key: "
                    + configKey);
        }
    }

    static String[] toArray(String stringValue) {
        String[] values = SPLIT_PATTERN.split(stringValue, -1);

        for (int i = 0; i < values.length; i++) {
            String value = values[i];
            values[i] = ESCAPED_COMMA_PATTERN.matcher(value).replaceAll(Matcher.quoteReplacement(","));
        }
        return values;
    }

    private static  List asList(Config config, String configKey, Class typeArg, String defaultValue) {
        // first try to see if we have a direct value
        Optional optionalValue = config.getOptionalValue(configKey, String.class);
        if (optionalValue.isPresent()) {
            return toList(configKey, config, optionalValue.get(), typeArg);
        }

        /*
         we also support indexed value
         e.g. for key "my.list" you can have both:
         my.list=12,13,14
         or (not and):
         my.list.0=12
         my.list.1=13
         */

        String indexedConfigKey = configKey + ".0";
        optionalValue = config.getOptionalValue(indexedConfigKey, String.class);
        if (optionalValue.isPresent()) {
            List result = new LinkedList<>();

            // first element is already in
            result.add(convert(indexedConfigKey, config, optionalValue.get(), typeArg));

            // hardcoded limit to lists of 1000 elements
            for (int i = 1; i < 1000; i++) {
                indexedConfigKey = configKey + "." + i;
                optionalValue = config.getOptionalValue(indexedConfigKey, String.class);
                if (optionalValue.isPresent()) {
                    result.add(convert(indexedConfigKey, config, optionalValue.get(), typeArg));
                } else {
                    // finish the iteration on first missing index
                    break;
                }
            }
            return result;
        } else {
            if (null == defaultValue) {
                throw new NoSuchElementException("Missing list value for key " + configKey);
            }

            return toList(configKey, config, defaultValue, typeArg);
        }
    }

    private static  List toList(String configKey, Config config, String stringValue, Class typeArg) {
        if (stringValue.isEmpty()) {
            return List.of();
        }
        // we have a comma separated list
        List result = new LinkedList<>();
        for (String value : toArray(stringValue)) {
            result.add(convert(configKey, config, value, typeArg));
        }
        return result;
    }

    private String defaultValue(ConfigProperty annotation) {
        String defaultFromAnnotation = annotation.defaultValue();
        if (defaultFromAnnotation.equals(ConfigProperty.UNCONFIGURED_VALUE)) {
            return null;
        }
        return defaultFromAnnotation;
    }

    private String configKey(ConfigProperty annotation, String fullPath) {
        String keyFromAnnotation = annotation.name();

        if (keyFromAnnotation.isEmpty()) {
            return fullPath.replace('$', '.');
        }
        return keyFromAnnotation;
    }

    private static String injectedName(InjectionPoint ip) {
        Annotated annotated = ip.getAnnotated();
        if (annotated instanceof AnnotatedField) {
            AnnotatedField f = (AnnotatedField) annotated;
            return f.getJavaMember().getName();
        }

        if (annotated instanceof AnnotatedParameter) {
            AnnotatedParameter p = (AnnotatedParameter) annotated;

            Member member = ip.getMember();
            if (member instanceof Method) {
                return member.getName() + "_" + p.getPosition();
            }
            if (member instanceof Constructor) {
                return "new_" + p.getPosition();
            }
        }

        return ip.getMember().getName();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy