001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 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.jaxrs.cdi;
018
019import java.lang.annotation.Annotation;
020import java.lang.annotation.Documented;
021import java.lang.annotation.ElementType;
022import java.lang.annotation.Inherited;
023import java.lang.annotation.Retention;
024import java.lang.annotation.RetentionPolicy;
025import java.lang.annotation.Target;
026
027import java.lang.reflect.ParameterizedType;
028import java.lang.reflect.Type;
029
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Iterator;
034import java.util.Map;
035import java.util.Map.Entry;
036import java.util.Set;
037
038import javax.enterprise.context.ContextNotActiveException;
039import javax.enterprise.context.Dependent;
040
041import javax.enterprise.context.spi.AlterableContext;
042import javax.enterprise.context.spi.Context;
043
044import javax.enterprise.context.spi.CreationalContext;
045
046import javax.enterprise.event.Observes;
047
048import javax.enterprise.inject.Any;
049
050import javax.enterprise.inject.spi.AfterBeanDiscovery;
051import javax.enterprise.inject.spi.AnnotatedType;
052import javax.enterprise.inject.spi.Bean;
053import javax.enterprise.inject.spi.BeanAttributes;
054import javax.enterprise.inject.spi.BeanManager;
055import javax.enterprise.inject.spi.Extension;
056import javax.enterprise.inject.spi.ProcessAnnotatedType;
057import javax.enterprise.inject.spi.ProcessBeanAttributes;
058import javax.enterprise.inject.spi.WithAnnotations;
059
060import javax.enterprise.util.AnnotationLiteral;
061
062import javax.inject.Qualifier;
063import javax.inject.Singleton;
064
065import javax.ws.rs.Path;
066
067import javax.ws.rs.core.Application;
068import javax.ws.rs.ApplicationPath;
069
070/**
071 * An {@link Extension} that makes {@link Application}s and resource
072 * classes available as CDI beans.
073 *
074 * @author <a href="https://about.me/lairdnelson"
075 * target="_parent">Laird Nelson</a>
076 */
077public class JaxRsExtension implements Extension {
078
079  private final Set<Class<?>> potentialResourceClasses;
080
081  private final Set<Class<?>> potentialProviderClasses;
082
083  private final Map<Class<?>, BeanAttributes<?>> resourceBeans;
084
085  private final Map<Class<?>, BeanAttributes<?>> providerBeans;
086
087  private final Set<Set<Annotation>> qualifiers;
088
089  /**
090   * Creates a new {@link JaxRsExtension}.
091   */
092  public JaxRsExtension() {
093    super();
094    this.potentialResourceClasses = new HashSet<>();
095    this.potentialProviderClasses = new HashSet<>();
096    this.resourceBeans = new HashMap<>();
097    this.providerBeans = new HashMap<>();
098    this.qualifiers = new HashSet<>();
099  }
100
101  private final <T> void discoverResourceClasses(@Observes
102                                                 @WithAnnotations({ Path.class })
103                                                 final ProcessAnnotatedType<T> event) {
104    if (event != null) {
105      final AnnotatedType<T> annotatedType = event.getAnnotatedType();
106      if (annotatedType != null) {
107        final Class<T> javaClass = annotatedType.getJavaClass();
108        if (javaClass != null) {
109          this.potentialResourceClasses.add(javaClass);
110        }
111      }
112    }
113  }
114
115  private final <T> void discoverProviderClasses(@Observes
116                                                 @WithAnnotations({ javax.ws.rs.ext.Provider.class })
117                                                 final ProcessAnnotatedType<T> event) {
118    if (event != null) {
119      final AnnotatedType<T> annotatedType = event.getAnnotatedType();
120      if (annotatedType != null) {
121        final Class<T> javaClass = annotatedType.getJavaClass();
122        if (javaClass != null) {
123          this.potentialProviderClasses.add(javaClass);
124        }
125      }
126    }
127  }
128
129  private final <T> void forAllBeanAttributes(@Observes
130                                              final ProcessBeanAttributes<T> event) {
131    if (event != null) {
132      final BeanAttributes<T> beanAttributes = event.getBeanAttributes();
133      if (beanAttributes != null) {
134        final Set<Type> beanTypes = beanAttributes.getTypes();
135        if (beanTypes != null && !beanTypes.isEmpty()) {
136          for (final Type beanType : beanTypes) {
137            final Class<?> beanTypeClass;
138            if (beanType instanceof Class) {
139              beanTypeClass = (Class<?>)beanType;
140            } else if (beanType instanceof ParameterizedType) {
141              final Object rawBeanType = ((ParameterizedType)beanType).getRawType();
142              if (rawBeanType instanceof Class) {
143                beanTypeClass = (Class<?>) rawBeanType;
144              } else {
145                beanTypeClass = null;
146              }
147            } else {
148              beanTypeClass = null;
149            }
150            if (beanTypeClass != null) {
151              if (Application.class.isAssignableFrom(beanTypeClass)) {
152                this.qualifiers.add(beanAttributes.getQualifiers()); // yes, add the set as an element, not the set's elements
153              }
154
155              // Edge case: it could be an application whose methods
156              // are annotated with @Path, so it could still be a
157              // resource class.  That's why this isn't an else if.
158              if (this.potentialResourceClasses.remove(beanTypeClass)) {
159                // This bean has a beanType that we previously identified as a JAX-RS resource.
160                event.configureBeanAttributes().addQualifiers(ResourceClass.Literal.INSTANCE);
161                this.resourceBeans.put(beanTypeClass, beanAttributes);
162              }
163
164              if (this.potentialProviderClasses.remove(beanTypeClass)) {
165                // This bean has a beanType that we previously identified as a Provider class.
166                this.providerBeans.put(beanTypeClass, beanAttributes);
167              }
168            }
169          }
170        }
171      }
172    }
173  }
174
175  /**
176   * Returns an {@linkplain Collections#unmodifiableSet(Set)
177   * unmodifiable <code>Set</code>} of {@link Set}s of {@linkplain
178   * Qualifier qualifier annotations} that have been found annotating
179   * {@link Application}s.
180   *
181   * <p>This method never returns {@code null}.</p>
182   *
183   * @return a non-{@code null}, {@linkplain Collections#unmodifiableSet(Set)
184   * unmodifiable <code>Set</code>} of {@link Set}s of {@linkplain
185   * Qualifier qualifier annotations} that have been found annotating
186   * {@link Application}s
187   */
188  public final Set<Set<Annotation>> getAllApplicationQualifiers() {
189    return Collections.unmodifiableSet(this.qualifiers);
190  }
191
192  private final void afterNonSyntheticBeansAreEnabled(@Observes
193                                                      final AfterBeanDiscovery event,
194                                                      final BeanManager beanManager) {
195    if (event != null && beanManager != null) {
196      final Set<Bean<?>> applicationBeans = beanManager.getBeans(Application.class, Any.Literal.INSTANCE);
197      if (applicationBeans != null && !applicationBeans.isEmpty()) {
198        for (final Bean<?> bean : applicationBeans) {
199          @SuppressWarnings("unchecked")
200          final Bean<Application> applicationBean = (Bean<Application>)bean;
201          final CreationalContext<Application> cc = beanManager.createCreationalContext(applicationBean);
202          final Class<? extends Annotation> applicationScope = applicationBean.getScope();
203          assert applicationScope != null;
204          Context context = beanManager.getContext(applicationScope);        
205          assert context != null;
206          AlterableContext alterableContext = context instanceof AlterableContext ? (AlterableContext)context : null;
207          Application application = null;                
208          try {
209            if (alterableContext == null) {
210              application = applicationBean.create(cc);
211            } else {
212              try {
213                application = alterableContext.get(applicationBean, cc);
214              } catch (final ContextNotActiveException ok) {
215                alterableContext = null;
216                application = applicationBean.create(cc);
217              }
218            }
219            if (application != null) {
220              final Set<Annotation> applicationQualifiers = applicationBean.getQualifiers();
221              final ApplicationPath applicationPath = application.getClass().getAnnotation(ApplicationPath.class);
222              if (applicationPath != null) {
223                event.addBean()
224                  .types(ApplicationPath.class)
225                  .scope(Singleton.class)
226                  .qualifiers(applicationQualifiers)
227                  .createWith(ignored -> applicationPath);
228              }
229              final Set<Class<?>> classes = application.getClasses();
230              if (classes != null && !classes.isEmpty()) {
231                for (final Class<?> cls : classes) {
232                  final Object resourceBean = this.resourceBeans.remove(cls);
233                  final Object providerBean = this.providerBeans.remove(cls);
234                  if (resourceBean == null && providerBean == null) {
235                    event.addBean()
236                      .scope(Dependent.class) // by default; possibly overridden by read()
237                      .read(beanManager.createAnnotatedType(cls))
238                      .addQualifiers(applicationQualifiers)
239                      .addQualifiers(ResourceClass.Literal.INSTANCE);
240                  }
241                }
242              }
243              // Deliberately don't try to deal with getSingletons().
244            }
245          } finally {
246            try {
247              if (application != null) {
248                if (alterableContext == null) {
249                  applicationBean.destroy(application, cc);
250                } else {
251                  try {
252                    alterableContext.destroy(applicationBean);
253                  } catch (final UnsupportedOperationException ok) {
254                    
255                  }
256                }
257              }
258            } finally {
259              cc.release();
260            }
261          }
262        }
263      }
264
265      // Any potentialResourceClasses left over here are annotated
266      // types we discovered, but for whatever reason were not made
267      // into beans.  Maybe they were vetoed.
268      this.potentialResourceClasses.clear();
269
270      // Any potentialProviderClasses left over here are annotated
271      // types we discovered, but for whatever reason were not made
272      // into beans.  Maybe they were vetoed.
273      this.potentialProviderClasses.clear();
274      
275      // OK, when we get here, if there are any resource beans left
276      // lying around they went "unclaimed".  Build a synthetic
277      // Application for them.
278      if (!this.resourceBeans.isEmpty()) {
279        final Set<Entry<Class<?>, BeanAttributes<?>>> resourceBeansEntrySet = this.resourceBeans.entrySet();
280        assert resourceBeansEntrySet != null;
281        assert !resourceBeansEntrySet.isEmpty();
282        final Map<Set<Annotation>, Set<Class<?>>> resourceClassesByQualifiers = new HashMap<>();
283        for (final Entry<Class<?>, BeanAttributes<?>> entry : resourceBeansEntrySet) {
284          assert entry != null;
285          final Set<Annotation> qualifiers = entry.getValue().getQualifiers();
286          Set<Class<?>> resourceClasses = resourceClassesByQualifiers.get(qualifiers);
287          if (resourceClasses == null) {
288            resourceClasses = new HashSet<>();
289            resourceClassesByQualifiers.put(qualifiers, resourceClasses);
290          }
291          resourceClasses.add(entry.getKey());
292        }
293
294        final Set<Entry<Set<Annotation>, Set<Class<?>>>> entrySet = resourceClassesByQualifiers.entrySet();
295        assert entrySet != null;
296        assert !entrySet.isEmpty();
297        for (final Entry<Set<Annotation>, Set<Class<?>>> entry : entrySet) {
298          assert entry != null;
299          final Set<Annotation> resourceBeanQualifiers = entry.getKey();
300          final Set<Class<?>> resourceClasses = entry.getValue();
301          assert resourceClasses != null;
302          assert !resourceClasses.isEmpty();
303          final Set<Class<?>> allClasses;
304          if (this.providerBeans.isEmpty()) {
305            allClasses = resourceClasses;
306          } else {
307            allClasses = new HashSet<>(resourceClasses);
308            final Set<Entry<Class<?>, BeanAttributes<?>>> providerBeansEntrySet = this.providerBeans.entrySet();
309            assert providerBeansEntrySet != null;
310            assert !providerBeansEntrySet.isEmpty();
311            final Iterator<Entry<Class<?>, BeanAttributes<?>>> providerBeansIterator = providerBeansEntrySet.iterator();
312            assert providerBeansIterator != null;
313            while (providerBeansIterator.hasNext()) {
314              final Entry<Class<?>, BeanAttributes<?>> providerBeansEntry = providerBeansIterator.next();
315              assert providerBeansEntry != null;
316              final Set<Annotation> providerBeanQualifiers = providerBeansEntry.getValue().getQualifiers();
317              boolean match = false;
318              if (resourceBeanQualifiers == null) {
319                if (providerBeanQualifiers == null) {
320                  match = true;
321                }
322              } else if (resourceBeanQualifiers.equals(providerBeanQualifiers)) {
323                match = true;
324              }
325              if (match) {
326                allClasses.add(providerBeansEntry.getKey());
327                providerBeansIterator.remove();
328              }              
329            }
330          }
331
332          assert resourceBeanQualifiers != null;
333          assert !resourceBeanQualifiers.isEmpty();
334          final Set<Annotation> syntheticApplicationQualifiers = new HashSet<>(resourceBeanQualifiers);
335          syntheticApplicationQualifiers.remove(ResourceClass.Literal.INSTANCE);
336
337          event.addBean()
338            .addTransitiveTypeClosure(SyntheticApplication.class)
339            .scope(Singleton.class)
340            .addQualifiers(syntheticApplicationQualifiers)
341            .createWith(cc -> new SyntheticApplication(allClasses));
342          this.qualifiers.add(syntheticApplicationQualifiers);
343        }
344
345        this.resourceBeans.clear();
346      }
347
348      if (!this.providerBeans.isEmpty()) {
349        // TODO: we found some provider class beans but never
350        // associated them with any application.  This would only
351        // happen if they were not qualified with qualifiers that also
352        // qualified unclaimed resource beans.  That would be odd.
353        // Either we should throw a deployment error or just ignore
354        // them.
355      }
356      this.providerBeans.clear();
357
358    }
359  }
360
361  /**
362   * An {@link Application} that has been synthesized out of resource
363   * classes found on the classpath that have not otherwise been
364   * {@linkplain Application#getClasses() claimed} by other {@link
365   * Application} instances.
366   *
367   * @author <a href="https://about.me/lairdnelson"
368   * target="_parent">Laird Nelson</a>
369   *
370   * @see Application
371   */
372  public static final class SyntheticApplication extends Application {
373
374    private final Set<Class<?>> classes;
375    
376    SyntheticApplication(final Set<Class<?>> classes) {
377      super();
378      if (classes == null || classes.isEmpty()) {
379        this.classes = Collections.emptySet();
380      } else {
381        this.classes = Collections.unmodifiableSet(classes);
382      }
383    }
384
385    /**
386     * Returns an {@linkplain Collections#unmodifiableSet(Set)
387     * unmodifiable <code>Set</code>} of resource and provider
388     * classes.
389     *
390     * <p>This method never returns {@code null}.</p>
391     *
392     * @return a non-{@code null}, {@linkplain
393     * Collections#unmodifiableSet(Set) unmodifiable <code>Set</code>}
394     * of resource and provider classes.
395     */
396    @Override
397    public final Set<Class<?>> getClasses() {
398      return this.classes;
399    }
400    
401  }
402
403  /**
404   * A {@link Qualifier} annotation indicating that a {@link
405   * BeanAttributes} implementation is a JAX-RS resource class.
406   *
407   * <p>This annotation cannot be applied manually to any Java element
408   * but can be used as an input to the {@link
409   * BeanManager#getBeans(Type, Annotation...)} method.</p>
410   *
411   * @author <a href="https://about.me/lairdnelson"
412   * target="_parent">Laird Nelson</a>
413   */
414  @Documented
415  @Inherited
416  @Qualifier
417  @Retention(RetentionPolicy.RUNTIME)
418  @Target({ })
419  public @interface ResourceClass {
420
421    /**
422     * A {@link ResourceClass} implementation.
423     *
424     * @author <a href="https://about.me/lairdnelson"
425     * target="_parent">Laird Nelson</a>
426     *
427     * @see #INSTANCE
428     */
429    public static final class Literal extends AnnotationLiteral<ResourceClass> implements ResourceClass {
430
431      private static final long serialVersionUID = 1L;
432
433      /**
434       * The sole instance of this class.
435       *
436       * <p>This field is never {@code null}.</p>
437       */
438      public static final ResourceClass INSTANCE = new Literal();
439      
440    }
441    
442  }
443  
444}