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