001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2018–2019 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.microprofile.config.cdi;
018
019import java.lang.annotation.Annotation;
020
021import java.lang.reflect.Executable;
022import java.lang.reflect.Member;
023import java.lang.reflect.Parameter;
024import java.lang.reflect.Type;
025
026import java.util.Collection;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Map;
033import java.util.Map.Entry;
034import java.util.Objects;
035import java.util.Set;
036
037import javax.enterprise.context.spi.CreationalContext;
038
039import javax.enterprise.event.Observes;
040
041import javax.enterprise.inject.spi.AfterBeanDiscovery;
042import javax.enterprise.inject.spi.AfterDeploymentValidation;
043import javax.enterprise.inject.spi.Annotated;
044import javax.enterprise.inject.spi.AnnotatedCallable;
045import javax.enterprise.inject.spi.AnnotatedField;
046import javax.enterprise.inject.spi.AnnotatedMethod;
047import javax.enterprise.inject.spi.AnnotatedParameter;
048import javax.enterprise.inject.spi.AnnotatedType;
049import javax.enterprise.inject.spi.Bean;
050import javax.enterprise.inject.spi.BeanManager;
051import javax.enterprise.inject.spi.DeploymentException;
052import javax.enterprise.inject.spi.Extension;
053import javax.enterprise.inject.spi.InjectionPoint;
054import javax.enterprise.inject.spi.ObserverMethod;
055import javax.enterprise.inject.spi.ProcessBean;
056import javax.enterprise.inject.spi.ProcessObserverMethod;
057
058import org.eclipse.microprofile.config.Config;
059
060import org.eclipse.microprofile.config.inject.ConfigProperty;
061
062/**
063 * An {@link Extension} that enables injection of {@link
064 * ConfigProperty}-annotated configuration property values.
065 *
066 * @author <a href="https://about.me/lairdnelson"
067 * target="_parent">Laird Nelson</a>
068 */
069public final class ConfigExtension implements Extension {
070
071  private final Map<Set<Annotation>, Set<Type>> configPropertyTypes;
072
073  private final Set<InjectionPoint> configPropertyInjectionPointsToValidate;
074
075  private final Set<Set<Annotation>> allConfigQualifiers;
076
077  /**
078   * Creates a new {@link ConfigExtension}.
079   */
080  public ConfigExtension() {
081    super();
082    this.configPropertyInjectionPointsToValidate = new HashSet<>();
083    this.configPropertyTypes = new HashMap<>();
084    this.allConfigQualifiers = new HashSet<>();
085  }
086
087  private final <X> void processBean(@Observes final ProcessBean<X> event) {
088    if (event != null) {
089      final Bean<X> bean = event.getBean();
090      if (bean != null) {
091        final Set<InjectionPoint> beanInjectionPoints = bean.getInjectionPoints();
092        if (beanInjectionPoints != null && !beanInjectionPoints.isEmpty()) {
093          for (final InjectionPoint beanInjectionPoint : beanInjectionPoints) {
094            if (beanInjectionPoint != null) {
095              final Set<Annotation> qualifiers = beanInjectionPoint.getQualifiers();
096              assert qualifiers != null;
097              final Type type = beanInjectionPoint.getType();
098              assert type != null;
099              if (Config.class.equals(type)) {
100                if (!qualifiers.isEmpty()) {
101                  final Set<Annotation> configQualifiers = new HashSet<>(qualifiers);
102                  if (configQualifiers.removeIf(q -> q instanceof ConfigProperty)) {
103                    configQualifiers.add(ConfigPropertyLiteral.INSTANCE);
104                  }
105                  configQualifiers.add(AnyLiteral.INSTANCE);
106                  allConfigQualifiers.add(configQualifiers);
107                }
108              } else {
109                final Annotated annotated = beanInjectionPoint.getAnnotated();
110                if (annotated != null && annotated.isAnnotationPresent(ConfigProperty.class)) {
111                  this.configPropertyInjectionPointsToValidate.add(beanInjectionPoint);
112                  Set<Annotation> configPropertyQualifiers = new HashSet<>(qualifiers);
113                  configPropertyQualifiers.removeIf(q -> q instanceof ConfigProperty);
114                  configPropertyQualifiers.add(ConfigPropertyLiteral.INSTANCE);
115                  configPropertyQualifiers = Collections.unmodifiableSet(configPropertyQualifiers);
116                  Set<Type> configPropertyTypes = this.configPropertyTypes.get(configPropertyQualifiers);
117                  if (configPropertyTypes == null) {
118                    configPropertyTypes = new HashSet<>();
119                    this.configPropertyTypes.put(configPropertyQualifiers, configPropertyTypes);
120                  }
121                  configPropertyTypes.add(type);
122                }
123              }
124            }
125          }
126        }
127      }
128    }
129  }
130
131  private final <T, X> void processObserverMethod(@Observes final ProcessObserverMethod<T, X> event,
132                                                  final BeanManager beanManager) {
133    if (event != null && beanManager != null) {
134      final AnnotatedMethod<X> annotatedMethod = event.getAnnotatedMethod();
135      if (annotatedMethod != null) {
136        final List<AnnotatedParameter<X>> annotatedParameters = annotatedMethod.getParameters();
137        if (annotatedParameters != null && annotatedParameters.size() > 1) {
138          for (final AnnotatedParameter<X> annotatedParameter : annotatedParameters) {
139            if (annotatedParameter != null &&
140                !annotatedParameter.isAnnotationPresent(Observes.class)) {
141              final InjectionPoint injectionPoint = beanManager.createInjectionPoint(annotatedParameter);
142              assert injectionPoint != null;
143              final Type type = injectionPoint.getType();
144              final Set<Annotation> qualifiers = injectionPoint.getQualifiers();
145              if (Config.class.equals(type)) {
146                if (qualifiers != null && !qualifiers.isEmpty()) {
147                  final Set<Annotation> configQualifiers = new HashSet<>(qualifiers);
148                  if (configQualifiers.removeIf(q -> q instanceof ConfigProperty)) {
149                    configQualifiers.add(ConfigPropertyLiteral.INSTANCE);
150                  }
151                  configQualifiers.add(AnyLiteral.INSTANCE);
152                  allConfigQualifiers.add(Collections.unmodifiableSet(configQualifiers));
153                }
154              } else if (annotatedParameter.isAnnotationPresent(ConfigProperty.class)) {
155                this.configPropertyInjectionPointsToValidate.add(injectionPoint);
156                Set<Annotation> configPropertyQualifiers = new HashSet<>(qualifiers);
157                configPropertyQualifiers.removeIf(q -> q instanceof ConfigProperty);
158                configPropertyQualifiers.add(ConfigPropertyLiteral.INSTANCE);
159                configPropertyQualifiers = Collections.unmodifiableSet(configPropertyQualifiers);
160                Set<Type> configPropertyTypes = this.configPropertyTypes.get(configPropertyQualifiers);
161                if (configPropertyTypes == null) {
162                  configPropertyTypes = new HashSet<>();
163                  this.configPropertyTypes.put(configPropertyQualifiers, configPropertyTypes);
164                }
165                configPropertyTypes.add(type);
166              }
167            }
168          }
169        }
170      }
171    }
172  }
173
174  private final void afterBeanDiscovery(@Observes final AfterBeanDiscovery event,
175                                        final BeanManager beanManager) {
176    if (event != null) {
177
178      final AnnotatedType<Config> configAnnotatedType = beanManager.createAnnotatedType(Config.class);
179      assert configAnnotatedType != null;
180
181      final Set<Annotation> defaultConfigQualifiers = new HashSet<>();
182      defaultConfigQualifiers.add(AnyLiteral.INSTANCE);
183      defaultConfigQualifiers.add(DefaultLiteral.INSTANCE);
184      this.allConfigQualifiers.add(defaultConfigQualifiers);
185
186      if (!this.configPropertyTypes.isEmpty()) {
187        final Set<Entry<Set<Annotation>, Set<Type>>> entrySet = this.configPropertyTypes.entrySet();
188        assert entrySet != null;
189        assert !entrySet.isEmpty();
190        for (final Entry<Set<Annotation>, Set<Type>> entry : entrySet) {
191          assert entry != null;
192          final Set<Annotation> qualifiers = entry.getKey();
193          assert qualifiers != null;
194          final Set<Type> types = entry.getValue();
195          assert types != null;
196          final Set<Type> newTypes = new HashSet<>(types);
197          final Iterator<Type> iterator = newTypes.iterator();
198          assert iterator != null;
199          while (iterator.hasNext()) {
200            final Type type = iterator.next();
201            assert type != null;
202            if (!noBeans(beanManager, type, qualifiers)) {
203              // A user-supplied bean is already present for the type
204              // and qualifiers so don't install a default bean.
205              iterator.remove();
206            }
207          }
208          event.addBean(new ConfigPropertyBean<>(newTypes, qualifiers));
209        }
210      }
211
212      for (final Set<Annotation> configQualifiers : this.allConfigQualifiers) {
213        if (noBeans(beanManager, Config.class, configQualifiers)) {
214          event.addBean(new ConfigBean(configQualifiers));
215        }
216      }
217
218    }
219    this.allConfigQualifiers.clear();
220  }
221
222  private final void afterDeploymentValidation(@Observes final AfterDeploymentValidation event,
223                                               final BeanManager beanManager) {
224    if (event != null && beanManager != null) {
225      final CreationalContext<?> cc = beanManager.createCreationalContext(null);
226      try {
227        for (final InjectionPoint injectionPoint : this.configPropertyInjectionPointsToValidate) {
228          assert injectionPoint != null;
229          if (beanManager.getInjectableReference(injectionPoint, cc) == null) {
230            event.addDeploymentProblem(new DeploymentException("No value exists for the mandatory configuration property named " +
231                                                               getConfigPropertyName(injectionPoint)));
232          }
233        }
234      } finally {
235        cc.release();
236      }
237    }
238    this.configPropertyInjectionPointsToValidate.clear();
239    this.configPropertyTypes.clear();
240  }
241
242  private static final boolean noBeans(final BeanManager beanManager, final Type type, final Set<Annotation> qualifiers) {
243    Objects.requireNonNull(beanManager);
244    Objects.requireNonNull(type);
245    final Collection<?> beans;
246    if (qualifiers == null || qualifiers.isEmpty()) {
247      beans = beanManager.getBeans(type);
248    } else {
249      beans = beanManager.getBeans(type, qualifiers.toArray(new Annotation[qualifiers.size()]));
250    }
251    return beans == null || beans.isEmpty();
252  }
253
254  static final String getConfigPropertyName(final InjectionPoint injectionPoint) {
255    return getConfigPropertyName(injectionPoint, null);
256  }
257
258  static final String getConfigPropertyName(final InjectionPoint injectionPoint, ConfigProperty configProperty) {
259    String name = null;
260    if (injectionPoint != null) {
261      if (configProperty == null) {
262        final Collection<? extends Annotation> qualifiers = injectionPoint.getQualifiers();
263        if (qualifiers != null) {
264          for (final Annotation qualifier : qualifiers) {
265            if (qualifier != null && ConfigProperty.class.equals(qualifier.annotationType())) {
266              configProperty = (ConfigProperty)qualifier;
267              break;
268            }
269          }
270        }
271      }
272      if (configProperty != null) {
273        name = configProperty.name();
274        assert name != null;
275        if (name.isEmpty()) {
276          final Annotated annotated = injectionPoint.getAnnotated();
277          if (annotated instanceof AnnotatedField) {
278            final AnnotatedField<?> field = (AnnotatedField<?>)annotated;
279            final Member member = field.getJavaMember();
280            if (member != null) {
281              final AnnotatedType<?> declaringType = field.getDeclaringType();
282              if (declaringType != null) {
283                name = declaringType.getJavaClass().getCanonicalName() + "." + member.getName();
284              }
285            }
286          } else if (annotated instanceof AnnotatedParameter) {
287            final AnnotatedParameter<?> annotatedParameter = (AnnotatedParameter<?>)annotated;
288            // The specification says: "[The ConfigProperty name that
289            // will be synthesized if one is not provided] will be
290            // derived automatically as
291            // <class_name>.<injetion_point_name> [sic], where
292            // injection_point_name [sic] is the field name or
293            // parameter name, class_name is the fully qualified name
294            // of the class being injected to [sic]."
295            final AnnotatedCallable<?> declaringCallable = annotatedParameter.getDeclaringCallable();
296            if (declaringCallable != null) {
297              final Member member = declaringCallable.getJavaMember();
298              assert member instanceof Executable;
299              final Executable executable = (Executable)member;
300              final int position = annotatedParameter.getPosition();
301              final Parameter[] parameters = executable.getParameters();
302              if (parameters != null && parameters.length > position) {
303                final Parameter parameter = parameters[position];
304                assert parameter != null;
305                if (parameter.isNamePresent()) {
306                  final AnnotatedType<?> declaringType = declaringCallable.getDeclaringType();
307                  if (declaringType != null) {
308                    name = declaringType.getJavaClass().getCanonicalName() + "." + parameter.getName();
309                  }
310                }
311              }
312            }
313          } else {
314            assert false;
315          }
316        }
317      }
318    }
319    return name;
320  }
321
322}