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}