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, X> 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