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}