Skip to content

Commit

Permalink
ArC: add support for custom AlterableContext implementations
Browse files Browse the repository at this point in the history
Custom contexts in ArC must implement the `InjectableContext`
interface, which is ArC-specific. To implement CDI Lite properly,
ArC must also support custom implementations of `AlterableContext`.

Fortunately, the `InjectableContext` interface adds just a few
methods on top of `AlterableContext`, and none of them are critical
for custom contexts to function. Therefore, this commit simply
takes the user-supplied implementation of `AlterableContext` and
generates a subclass that implements `InjectableContext`, where
all the additional methods throw `UnsupportedOperationException`.
This subclass is then registered as the custom context, instead
of the original user-supplied class.
  • Loading branch information
Ladicek committed Jun 29, 2023
1 parent 7076120 commit 25b1a5f
Show file tree
Hide file tree
Showing 6 changed files with 734 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,13 @@ public static Builder builder() {
protected final Predicate<DotName> injectionPointAnnotationsPredicate;

private final ExtensionsEntryPoint buildCompatibleExtensions;
private final CustomAlterableContexts customAlterableContexts; // generic but currently only used for BCE

private BeanProcessor(Builder builder) {
this.buildCompatibleExtensions = builder.buildCompatibleExtensions;
this.customAlterableContexts = new CustomAlterableContexts(builder.applicationClassPredicate);
if (buildCompatibleExtensions != null) {
buildCompatibleExtensions.registerMetaAnnotations(builder);
buildCompatibleExtensions.registerMetaAnnotations(builder, customAlterableContexts);
buildCompatibleExtensions.runEnhancement(builder.beanArchiveComputingIndex, builder);
}

Expand Down Expand Up @@ -162,6 +164,7 @@ public void initialize(Consumer<BytecodeTransformer> bytecodeTransformerConsumer
*/
public BeanDeploymentValidator.ValidationContext validate(Consumer<BytecodeTransformer> bytecodeTransformerConsumer) {
ValidationContext validationContext = beanDeployment.validate(beanDeploymentValidators, bytecodeTransformerConsumer);
customAlterableContexts.validate(validationContext, transformUnproxyableClasses, bytecodeTransformerConsumer);
if (buildCompatibleExtensions != null) {
buildCompatibleExtensions.runValidation(beanDeployment.getBeanArchiveIndex(),
validationContext.get(Key.BEANS), validationContext.get(Key.OBSERVERS));
Expand Down Expand Up @@ -333,6 +336,10 @@ public Collection<Resource> call() throws Exception {
}));
}

// Generate `_InjectableContext` subclasses for custom `AlterableContext`s
primaryTasks.addAll(new CustomAlterableContextsGenerator(generateSources)
.generate(customAlterableContexts, existingClasses, executor));

for (Future<Collection<Resource>> future : primaryTasks) {
resources.addAll(future.get());
}
Expand Down Expand Up @@ -391,6 +398,10 @@ public Collection<Resource> call() throws Exception {
resources.addAll(observerGenerator.generate(observer));
}

// Generate `_InjectableContext` subclasses for custom `AlterableContext`s
resources.addAll(new CustomAlterableContextsGenerator(generateSources)
.generate(customAlterableContexts, existingClasses));

// Generate _ComponentsProvider
resources.addAll(
new ComponentsProviderGenerator(annotationLiterals, generateSources, detectUnusedFalsePositives).generate(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.quarkus.arc.processor;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

import jakarta.enterprise.context.spi.AlterableContext;
import jakarta.enterprise.inject.spi.DeploymentException;

import org.jboss.jandex.DotName;

public class CustomAlterableContexts {
private final List<CustomAlterableContextInfo> registered = new ArrayList<>();
private final Predicate<DotName> applicationClassPredicate;

CustomAlterableContexts(Predicate<DotName> applicationClassPredicate) {
this.applicationClassPredicate = applicationClassPredicate;
}

public CustomAlterableContextInfo add(Class<? extends AlterableContext> contextClass, Boolean isNormal) {
String generatedName = contextClass.getName() + "_InjectableContext";
boolean isApplicationClass = applicationClassPredicate.test(DotName.createSimple(contextClass));
CustomAlterableContextInfo result = new CustomAlterableContextInfo(contextClass, isNormal, generatedName,
isApplicationClass);
registered.add(result);
return result;
}

void validate(BeanDeploymentValidator.ValidationContext validationContext, boolean transformUnproxyableClasses,
Consumer<BytecodeTransformer> bytecodeTransformerConsumer) {
for (CustomAlterableContextInfo info : registered) {
if (Modifier.isFinal(info.contextClass.getModifiers())) {
if (transformUnproxyableClasses) {
bytecodeTransformerConsumer.accept(new BytecodeTransformer(info.contextClass.getName(),
new Beans.FinalClassTransformFunction()));
} else {
validationContext.addDeploymentProblem(
new DeploymentException("Custom context class may not be final: " + info.contextClass));
}
}
}
}

List<CustomAlterableContextInfo> getRegistered() {
return registered;
}

public static class CustomAlterableContextInfo {
public final Class<? extends AlterableContext> contextClass;
public final Boolean isNormal;
public final String generatedName;
public final boolean isApplicationClass;

CustomAlterableContextInfo(Class<? extends AlterableContext> contextClass, Boolean isNormal,
String generatedName, boolean isApplicationClass) {
this.contextClass = contextClass;
this.isNormal = isNormal;
this.generatedName = generatedName;
this.isApplicationClass = isApplicationClass;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.quarkus.arc.processor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.jboss.logging.Logger;

import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.processor.CustomAlterableContexts.CustomAlterableContextInfo;
import io.quarkus.arc.processor.ResourceOutput.Resource;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;

/**
* This is an internal companion of {@link CustomAlterableContexts} that handles generating
* subclasses of given context classes that implement {@code InjectableContext}.
*/
class CustomAlterableContextsGenerator extends AbstractGenerator {
private static final Logger LOGGER = Logger.getLogger(CustomAlterableContextsGenerator.class);

CustomAlterableContextsGenerator(boolean generateSources) {
super(generateSources);
}

/**
* Creator of an {@link CustomAlterableContexts} must call this method at an appropriate point
* in time and write the result to an appropriate output. If not, the bytecode sequences generated
* using the result of {@code CustomAlterableContexts.add()} will refer to non-existing classes.
*
* @param existingClasses names of classes that already exist and should not be generated again
* @return the generated classes, never {@code null}
*/
Collection<Resource> generate(CustomAlterableContexts customAlterableContexts, Set<String> existingClasses) {
List<Resource> resources = new ArrayList<>();
for (CustomAlterableContextInfo info : customAlterableContexts.getRegistered()) {
ResourceClassOutput classOutput = new ResourceClassOutput(info.isApplicationClass, generateSources);
createInjectableContextSubclass(classOutput, info, existingClasses);
resources.addAll(classOutput.getResources());
}
return resources;
}

/**
* Creator of an {@link AnnotationLiteralProcessor} must call this method at an appropriate point
* in time and write the result to an appropriate output. If not, the bytecode sequences generated
* using the result of {@code CustomAlterableContexts.add()} will refer to non-existing classes.
*
* @param existingClasses names of classes that already exist and should not be generated again
* @return the generated classes, never {@code null}
*/
Collection<Future<Collection<Resource>>> generate(CustomAlterableContexts customAlterableContexts,
Set<String> existingClasses, ExecutorService executor) {
List<Future<Collection<Resource>>> futures = new ArrayList<>();
for (CustomAlterableContextInfo info : customAlterableContexts.getRegistered()) {
futures.add(executor.submit(new Callable<Collection<Resource>>() {
@Override
public Collection<Resource> call() throws Exception {
ResourceClassOutput classOutput = new ResourceClassOutput(info.isApplicationClass, generateSources);
createInjectableContextSubclass(classOutput, info, existingClasses);
return classOutput.getResources();
}
}));
}
return futures;
}

private void createInjectableContextSubclass(ClassOutput classOutput, CustomAlterableContextInfo info,
Set<String> existingClasses) {

String dottedGeneratedName = info.generatedName;
String generatedName = dottedGeneratedName.replace('.', '/');
if (existingClasses.contains(generatedName)) {
return;
}

ClassCreator injectableContextSubclass = ClassCreator.builder()
.classOutput(classOutput)
.className(generatedName)
.superClass(info.contextClass)
.interfaces(InjectableContext.class)
.build();

// constructor
MethodCreator constructor = injectableContextSubclass.getMethodCreator(Methods.INIT, void.class);
constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(info.contextClass), constructor.getThis());
constructor.returnVoid();

// implement `isNormal()` if needed
if (info.isNormal != null) {
MethodCreator isNormal = injectableContextSubclass.getMethodCreator("isNormal", boolean.class);
isNormal.returnBoolean(info.isNormal);
}

// implement `destroy()`
MethodCreator destroy = injectableContextSubclass.getMethodCreator("destroy", void.class);
destroy.throwException(UnsupportedOperationException.class, "Custom AlterableContext cannot destroy all instances");
destroy.returnVoid();

// implement `getState()`
MethodCreator getState = injectableContextSubclass.getMethodCreator("getState", InjectableContext.ContextState.class);
getState.throwException(UnsupportedOperationException.class, "Custom AlterableContext has no state");
getState.returnNull();

// implement `destroy(ContextState)`
MethodCreator destroyState = injectableContextSubclass.getMethodCreator("destroy", void.class,
InjectableContext.ContextState.class);
destroyState.throwException(UnsupportedOperationException.class, "Custom AlterableContext has no state");
destroyState.returnVoid();

injectableContextSubclass.close();
LOGGER.debugf("InjectableContext subclass generated: %s", dottedGeneratedName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.stream.Collectors;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.spi.AlterableContext;
import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.event.TransactionPhase;
import jakarta.enterprise.inject.Instance;
Expand Down Expand Up @@ -45,6 +46,8 @@
import io.quarkus.arc.processor.ConfiguratorBase;
import io.quarkus.arc.processor.ContextConfigurator;
import io.quarkus.arc.processor.ContextRegistrar;
import io.quarkus.arc.processor.CustomAlterableContexts;
import io.quarkus.arc.processor.CustomAlterableContexts.CustomAlterableContextInfo;
import io.quarkus.arc.processor.InterceptorBindingRegistrar;
import io.quarkus.arc.processor.ObserverConfigurator;
import io.quarkus.arc.processor.ObserverInfo;
Expand Down Expand Up @@ -144,7 +147,7 @@ public void runDiscovery(org.jboss.jandex.IndexView applicationIndex, Set<String
* <p>
* It is a no-op if no {@link BuildCompatibleExtension} was found.
*/
public void registerMetaAnnotations(BeanProcessor.Builder builder) {
public void registerMetaAnnotations(BeanProcessor.Builder builder, CustomAlterableContexts customAlterableContexts) {
if (invoker.isEmpty()) {
return;
}
Expand Down Expand Up @@ -212,11 +215,17 @@ public Set<DotName> getAdditionalStereotypes() {
@Override
public void register(RegistrationContext registrationContext) {
Class<? extends Annotation> scopeAnnotation = context.scopeAnnotation;
// TODO how many changes in ArC will be needed to support AlterableContext?
Class<? extends InjectableContext> contextClass = (Class) context.contextClass;

ContextConfigurator config = registrationContext.configure(scopeAnnotation)
.contextClass(contextClass);
Class<? extends AlterableContext> contextClass = context.contextClass;

ContextConfigurator config = registrationContext.configure(scopeAnnotation);
if (InjectableContext.class.isAssignableFrom(contextClass)) {
config.contextClass((Class<? extends InjectableContext>) contextClass);
} else {
CustomAlterableContextInfo info = customAlterableContexts.add(contextClass, context.isNormal);
config.creator(bytecode -> {
return bytecode.newInstance(MethodDescriptor.ofConstructor(info.generatedName));
});
}
if (context.isNormal != null) {
config.normal(context.isNormal);
}
Expand Down
Loading

0 comments on commit 25b1a5f

Please sign in to comment.