blob: 09c6cd6b3a26342361983c946ef23335cc6dc4f4 [file] [log] [blame]
/*
* 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.geronimo.config.cdi;
import org.apache.geronimo.config.ConfigImpl;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.AnnotatedMember;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.BeforeShutdown;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.ProcessInjectionPoint;
import javax.inject.Provider;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static org.eclipse.microprofile.config.ConfigProvider.getConfig;
/**
* @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
*/
public class ConfigExtension implements Extension {
private static final Object NULL = new Object();
private Config config;
private ConfigProviderResolver resolver;
private Set<Injection> injections = new HashSet<>();
private List<Throwable> deploymentProblems = new ArrayList<>();
private static final Map<Type, Type> REPLACED_TYPES = new HashMap<>();
static {
REPLACED_TYPES.put(double.class, Double.class);
REPLACED_TYPES.put(int.class, Integer.class);
REPLACED_TYPES.put(float.class, Float.class);
REPLACED_TYPES.put(long.class, Long.class);
REPLACED_TYPES.put(boolean.class, Boolean.class);
}
void init(@Observes final BeforeBeanDiscovery beforeBeanDiscovery, final BeanManager bm) {
resolver = ConfigProviderResolver.instance();
config = getConfig();
}
public void collectConfigProducer(@Observes ProcessInjectionPoint<?, ?> pip) {
final InjectionPoint injectionPoint = pip.getInjectionPoint();
final ConfigProperty configProperty = injectionPoint.getAnnotated().getAnnotation(ConfigProperty.class);
if (configProperty != null) {
Type replacedType = REPLACED_TYPES.getOrDefault(injectionPoint.getType(), injectionPoint.getType());
Injection injection = new Injection(replacedType);
final String key = getConfigKey(injectionPoint, configProperty);
final boolean defaultUnset = isDefaultUnset(configProperty.defaultValue());
if (!injections.add(injection)) {
final Injection ref = injection;
injection = injections.stream().filter(i -> i.equals(ref)).findFirst().get();
}
injection.keys.add(key);
injection.defaultValues.add(configProperty.defaultValue());
final ConfigImpl configImpl = unwrapConfig();
// what about lazy runtime lookup, not consistent with tck and system prop usage, for now assume optional=optional ;)
boolean hasValue = true;
if (defaultUnset) { // value validation
if (ParameterizedType.class.isInstance(injection.type)) {
final ParameterizedType pt = ParameterizedType.class.cast(injection.type);
if (pt.getRawType() != Optional.class && !configImpl.getOptionalValue(key, String.class).isPresent()) {
hasValue = false;
}
} else if (!configImpl.getOptionalValue(key, String.class).isPresent()) {
hasValue = false;
}
if (!hasValue) {
deploymentProblems.add(new IllegalArgumentException("No configured value for '" + key + "' from " + injectionPoint));
}
}
Class<?> instanceType = null;
if (ParameterizedType.class.isInstance(injection.type)) { // converters validation
final ParameterizedType pt = ParameterizedType.class.cast(injection.type);
if (pt.getRawType() == Provider.class && pt.getActualTypeArguments().length == 1 && Class.class.isInstance(pt.getActualTypeArguments()[0])
&& !configImpl.getConverters().containsKey(Class.class.cast(pt.getActualTypeArguments()[0]))) {
instanceType = Class.class.cast(pt.getActualTypeArguments()[0]);
} // else if Optional it is fine, else we don't know how to process
} else if (Class.class.isInstance(injection.type)) {
instanceType = Class.class.cast(injection.type);
}
if (instanceType != null) { // validate we have a converter + we can convert the existing value
if (!configImpl.getConverters().containsKey(instanceType)) {
deploymentProblems.add(new IllegalArgumentException("Missing converter for '" + key + "' from " + injectionPoint));
} else if (hasValue) {
try {
configImpl.getConverters().get(injection.type).convert(configImpl.getValue(key));
} catch (final RuntimeException re) {
deploymentProblems.add(re);
}
}
}
}
}
public void registerConfigProducer(@Observes AfterBeanDiscovery abd, BeanManager bm) {
injections.stream()
.flatMap(injection -> {
final BiFunction<CreationalContext<?>, ConfigInjectionBean<?>, String> keyProvider;
if (injection.keys.size() == 1) {
final String key = injection.keys.iterator().next();
keyProvider = (ctx, bean) -> key;
} else {
keyProvider = (ctx, bean) -> getName(findInjectionPoint(bm, ctx, bean));
}
if (ParameterizedType.class.isInstance(injection.type)) {
final ParameterizedType paramType = ParameterizedType.class.cast(injection.type);
final Type rawType = paramType.getRawType();
// todo: do we care of Instance injection? doesnt make much sense right?
if (Provider.class == rawType && paramType.getActualTypeArguments().length == 1) {
if (!Class.class.isInstance(paramType.getActualTypeArguments()[0])) {
deploymentProblems.add(new IllegalArgumentException("@ConfigProperty can only be used with Provider<T> where T is a Class"));
return Stream.empty();
}
final Class<?> providerType = Class.class.cast(paramType.getActualTypeArguments()[0]);
return Stream.of(new ConfigInjectionBean<Provider<?>>(injection.type, true) {
@Override
public Provider<?> create(final CreationalContext<Provider<?>> context) {
return () -> config.getValue(keyProvider.apply(context, this), providerType);
}
});
} else if (Optional.class == rawType && paramType.getActualTypeArguments().length == 1) {
if (!Class.class.isInstance(paramType.getActualTypeArguments()[0])) {
deploymentProblems.add(new IllegalArgumentException("@ConfigProperty can only be used with Optional<T> where T is a Class"));
return null;
}
final Class<?> optionalType = Class.class.cast(paramType.getActualTypeArguments()[0]);
return Stream.of(new ConfigInjectionBean<Optional<?>>(injection.type) {
@Override
public Optional<?> create(final CreationalContext<Optional<?>> context) {
return config.getOptionalValue(keyProvider.apply(context, this), optionalType);
}
});
} else {
deploymentProblems.add(new IllegalArgumentException("Unsupported parameterized type " + paramType));
return Stream.empty();
}
} else if (Class.class.isInstance(injection.type)) {
final Class clazz = Class.class.cast(injection.type);
final ConfigInjectionBean bean;
if (injection.defaultValues.isEmpty()) {
bean = new ConfigInjectionBean<Object>(injection.type) {
@Override
public Object create(final CreationalContext<Object> context) {
return config.getOptionalValue(keyProvider.apply(context, this), clazz);
}
};
} else if (injection.defaultValues.size() == 1) { // common enough to be optimized
final String defVal = injection.defaultValues.iterator().next();
final Object alternativeVal = isDefaultUnset(defVal) ? null : unwrapConfig().convert(defVal, clazz);
bean = new ConfigInjectionBean<Object>(injection.type) {
@Override
public Object create(final CreationalContext<Object> context) {
final Optional optionalValue = config.getOptionalValue(keyProvider.apply(context, this), clazz);
return optionalValue.orElse(alternativeVal);
}
};
} else { // sadly we need to get back to the injection point to know which one we need to use
final Map<String, Object> prepared = injection.defaultValues.stream()
.collect(toMap(identity(), k -> isDefaultUnset(k) ? NULL : unwrapConfig().convert(k, clazz), (a, b) -> b));
bean = new ConfigInjectionBean<Object>(injection.type) {
@Override
public Object create(final CreationalContext<Object> context) {
final InjectionPoint ip = findInjectionPoint(bm, context, this);
if (ip == null) {
throw new IllegalStateException("Could not retrieve InjectionPoint");
}
return config.getOptionalValue(ConfigExtension.this.getName(ip), clazz)
.orElseGet(() -> {
final Object val = prepared.get(ip.getAnnotated().getAnnotation(ConfigProperty.class).defaultValue());
return val == NULL ? null : val;
});
}
};
}
final Collection<ConfigInjectionBean<?>> beans = new ArrayList<>();
beans.add(bean);
// is adding these beans is that useful? we captured them all so only a programmatic lookup would justify it
// and not sure it would be done this way anyway
final ParameterizedTypeImpl providerType = new ParameterizedTypeImpl(Provider.class, injection.type);
if (injections.stream().noneMatch(i -> i.type.equals(providerType))) {
beans.add(new ConfigInjectionBean<Provider<?>>(providerType, true) {
@Override
public Provider<?> create(final CreationalContext<Provider<?>> context) {
return () -> bean.create(context);
}
});
}
final ParameterizedTypeImpl optionalType = new ParameterizedTypeImpl(Optional.class, injection.type);
if (injections.stream().noneMatch(i -> i.type.equals(optionalType))) {
beans.add(new ConfigInjectionBean<Optional<?>>(optionalType) {
@Override
public Optional<?> create(final CreationalContext<Optional<?>> context) {
return Optional.ofNullable(bean.create(context));
}
});
}
return beans.stream();
} else {
deploymentProblems.add(new IllegalArgumentException("Unknown type " + injection.type));
return Stream.empty();
}
})
.forEach(abd::addBean);
}
public void validate(@Observes AfterDeploymentValidation add) {
deploymentProblems.forEach(add::addDeploymentProblem);
injections.clear();
deploymentProblems.clear();
}
public void shutdown(@Observes BeforeShutdown bsd) {
resolver.releaseConfig(config);
}
private ConfigImpl unwrapConfig() {
return ConfigImpl.class.cast(config);
}
private static String getName(final InjectionPoint ip) {
final ConfigProperty annotation = ip.getAnnotated().getAnnotation(ConfigProperty.class);
final String name = annotation.name();
return isDefaultUnset(name) ? getConfigKey(ip, annotation) : name;
}
/**
* Get the property key to use.
* In case the {@link ConfigProperty#name()} is empty we will try to determine the key name from the InjectionPoint.
*/
private static String getConfigKey(InjectionPoint ip, ConfigProperty configProperty) {
String key = configProperty.name();
if (!key.isEmpty()) {
return key;
}
if (ip.getAnnotated() instanceof AnnotatedMember) {
AnnotatedMember member = (AnnotatedMember) ip.getAnnotated();
AnnotatedType declaringType = member.getDeclaringType();
if (declaringType != null) {
String[] parts = declaringType.getJavaClass().getCanonicalName().split("\\.");
String cn = parts[parts.length - 1];
parts[parts.length - 1] = Character.toLowerCase(cn.charAt(0)) + (cn.length() > 1 ? cn.substring(1) : "");
StringBuilder sb = new StringBuilder(parts[0]);
for (int i = 1; i < parts.length; i++) {
sb.append(".").append(parts[i]);
}
// now add the field name
sb.append(".").append(member.getJavaMember().getName());
return sb.toString();
}
}
throw new IllegalStateException("Could not find default name for @ConfigProperty InjectionPoint " + ip);
}
private static boolean isDefaultUnset(String defaultValue) {
return defaultValue == null || defaultValue.length() == 0 || defaultValue.equals(ConfigProperty.UNCONFIGURED_VALUE);
}
private static InjectionPoint findInjectionPoint(final BeanManager bm, final CreationalContext<?> ctx,
ConfigInjectionBean bean) {
return InjectionPoint.class.cast(bm.getInjectableReference(bean.getSimpleInjectionPoint(), ctx));
}
private static final class Injection {
private final Type type;
private final Collection<String> keys = new ArrayList<>();
private final Collection<String> defaultValues = new ArrayList<>();
private Injection(final Type type) {
this.type = type;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || Injection.class != o.getClass()) {
return false;
}
final Injection injection = Injection.class.cast(o);
return Objects.equals(type, injection.type);
}
@Override
public int hashCode() {
return type.hashCode();
}
}
private class ParameterizedTypeImpl implements ParameterizedType {
private final Type rawType;
private final Type[] types;
private ParameterizedTypeImpl(final Type raw, final Type... types) {
this.rawType = raw;
this.types = types;
}
@Override
public Type[] getActualTypeArguments() {
return types.clone();
}
@Override
public Type getOwnerType() {
return null;
}
@Override
public Type getRawType() {
return rawType;
}
@Override
public int hashCode() {
return Arrays.hashCode(types) ^ rawType.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (ParameterizedType.class.isInstance(obj)) {
final ParameterizedType that = ParameterizedType.class.cast(obj);
final Type thatRawType = that.getRawType();
return (rawType == null ? thatRawType == null : rawType.equals(thatRawType))
&& Arrays.equals(types, that.getActualTypeArguments());
}
return false;
}
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder();
buffer.append(Class.class.cast(rawType).getName());
final Type[] actualTypes = getActualTypeArguments();
if (actualTypes.length > 0) {
buffer.append("<");
final int length = actualTypes.length;
for (int i = 0; i < length; i++) {
if (actualTypes[i] instanceof Class) {
buffer.append(((Class<?>) actualTypes[i]).getSimpleName());
} else {
buffer.append(actualTypes[i].toString());
}
if (i != actualTypes.length - 1) {
buffer.append(",");
}
}
buffer.append(">");
}
return buffer.toString();
}
}
}