| /* |
| * 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(); |
| } |
| } |
| } |