From 8123224a914d42f10621796d7321ec2fddd0b372 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 28 Jun 2023 09:38:08 +0200 Subject: [PATCH] ArC: add support for custom AlterableContext implementations 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, with this commit, we simply take the user-supplied implementation of `AlterableContext` and generate 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. --- .../quarkus/arc/processor/BeanProcessor.java | 24 +- .../processor/CustomAlterableContexts.java | 64 +++ .../CustomAlterableContextsGenerator.java | 79 ++++ .../bcextensions/ExtensionsEntryPoint.java | 21 +- .../bcextensions/CustomNormalScopeTest.java | 422 ++++++++++++++++++ .../bcextensions/CustomPseudoScopeTest.java | 178 ++++++++ 6 files changed, 781 insertions(+), 7 deletions(-) create mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java create mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContextsGenerator.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomNormalScopeTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomPseudoScopeTest.java diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 7fefa74de31da..fb8ae68c42553 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -34,6 +34,7 @@ import io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext; import io.quarkus.arc.processor.BuildExtension.BuildContext; import io.quarkus.arc.processor.BuildExtension.Key; +import io.quarkus.arc.processor.CustomAlterableContexts.CustomAlterableContextInfo; import io.quarkus.arc.processor.ResourceOutput.Resource; import io.quarkus.arc.processor.ResourceOutput.Resource.SpecialType; import io.quarkus.arc.processor.bcextensions.ExtensionsEntryPoint; @@ -86,11 +87,13 @@ public static Builder builder() { protected final Predicate 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); } @@ -162,6 +165,7 @@ public void initialize(Consumer bytecodeTransformerConsumer */ public BeanDeploymentValidator.ValidationContext validate(Consumer 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)); @@ -225,6 +229,9 @@ public List generateResources(ReflectionRegistration reflectionRegistr observerGenerator.precomputeGeneratedName(observer); } + CustomAlterableContextsGenerator alterableContextsGenerator = new CustomAlterableContextsGenerator(generateSources); + List alterableContexts = customAlterableContexts.getRegistered(); + List resources = new ArrayList<>(); if (executor != null) { @@ -333,6 +340,16 @@ public Collection call() throws Exception { })); } + // Generate `_InjectableContext` subclasses for custom `AlterableContext`s + for (CustomAlterableContextInfo info : alterableContexts) { + primaryTasks.add(executor.submit(new Callable>() { + @Override + public Collection call() throws Exception { + return alterableContextsGenerator.generate(info); + } + })); + } + for (Future> future : primaryTasks) { resources.addAll(future.get()); } @@ -391,6 +408,11 @@ public Collection call() throws Exception { resources.addAll(observerGenerator.generate(observer)); } + // Generate `_InjectableContext` subclasses for custom `AlterableContext`s + for (CustomAlterableContextInfo info : alterableContexts) { + resources.addAll(alterableContextsGenerator.generate(info)); + } + // Generate _ComponentsProvider resources.addAll( new ComponentsProviderGenerator(annotationLiterals, generateSources, detectUnusedFalsePositives).generate( diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java new file mode 100644 index 0000000000000..aa4dcf4189f03 --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContexts.java @@ -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 registered = new ArrayList<>(); + private final Predicate applicationClassPredicate; + + CustomAlterableContexts(Predicate applicationClassPredicate) { + this.applicationClassPredicate = applicationClassPredicate; + } + + public CustomAlterableContextInfo add(Class 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 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 getRegistered() { + return registered; + } + + public static class CustomAlterableContextInfo { + public final Class contextClass; + public final Boolean isNormal; + public final String generatedName; + public final boolean isApplicationClass; + + CustomAlterableContextInfo(Class contextClass, Boolean isNormal, + String generatedName, boolean isApplicationClass) { + this.contextClass = contextClass; + this.isNormal = isNormal; + this.generatedName = generatedName; + this.isApplicationClass = isApplicationClass; + } + } +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContextsGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContextsGenerator.java new file mode 100644 index 0000000000000..bc5f3888acf7e --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/CustomAlterableContextsGenerator.java @@ -0,0 +1,79 @@ +package io.quarkus.arc.processor; + +import java.util.Collection; + +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. + * + * @return the generated classes, never {@code null} + */ + Collection generate(CustomAlterableContexts.CustomAlterableContextInfo info) { + ResourceClassOutput classOutput = new ResourceClassOutput(info.isApplicationClass, generateSources); + createInjectableContextSubclass(classOutput, info); + return classOutput.getResources(); + } + + private void createInjectableContextSubclass(ClassOutput classOutput, CustomAlterableContextInfo info) { + String generatedName = info.generatedName.replace('.', '/'); + + 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", info.generatedName); + } +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java index 8e1bee97b96da..2fec16043a592 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java @@ -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; @@ -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; @@ -144,7 +147,7 @@ public void runDiscovery(org.jboss.jandex.IndexView applicationIndex, Set * 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; } @@ -212,11 +215,17 @@ public Set getAdditionalStereotypes() { @Override public void register(RegistrationContext registrationContext) { Class scopeAnnotation = context.scopeAnnotation; - // TODO how many changes in ArC will be needed to support AlterableContext? - Class contextClass = (Class) context.contextClass; - - ContextConfigurator config = registrationContext.configure(scopeAnnotation) - .contextClass(contextClass); + Class contextClass = context.contextClass; + + ContextConfigurator config = registrationContext.configure(scopeAnnotation); + if (InjectableContext.class.isAssignableFrom(contextClass)) { + config.contextClass((Class) 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); } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomNormalScopeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomNormalScopeTest.java new file mode 100644 index 0000000000000..8977f4d755b4d --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomNormalScopeTest.java @@ -0,0 +1,422 @@ +package io.quarkus.arc.test.cdi.bcextensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.annotation.Priority; +import jakarta.decorator.Decorator; +import jakarta.decorator.Delegate; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.ContextNotActiveException; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.NormalScope; +import jakarta.enterprise.context.spi.AlterableContext; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Discovery; +import jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations; +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.ScannedClasses; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; +import jakarta.enterprise.inject.spi.BeanContainer; +import jakarta.inject.Inject; +import jakarta.interceptor.Interceptor; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +// this test is basically a copy of https://github.com/weld/command-context-example +public class CustomNormalScopeTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(TestCommand.class, MyService.class, IdService.class) + .buildCompatibleExtensions(new MyExtension()) + .build(); + + @Test + public void commandContextController() { + CommandContextController control = Arc.container().select(CommandContextController.class).get(); + boolean activated = control.activate(); + assertTrue(activated); + try { + ArcContainer container = Arc.container(); + assertEquals(container.select(IdService.class).get().get(), container.select(IdService.class).get().get()); + } finally { + control.deactivate(); + } + } + + @Test + public void commandExecutor() { + CommandExecutor executor = Arc.container().select(CommandExecutor.class).get(); + executor.execute(() -> { + ArcContainer container = Arc.container(); + assertEquals(container.select(IdService.class).get().get(), container.select(IdService.class).get().get()); + }); + } + + @Test + public void commandDecorator() { + TestCommand command = Arc.container().select(TestCommand.class).get(); + command.execute(); // contains assertions + assertTrue(TestCommand.EXECUTED); + } + + @Dependent + static class TestCommand implements Command { + static boolean EXECUTED = false; + + @Inject + CommandExecution execution; + + @Inject + MyService service; + + @Inject + IdService id; + + @Override + public void execute() { + service.process(); + assertEquals(id.get(), execution.getData().get("id")); + assertNotNull(execution.getStartedAt()); + EXECUTED = true; + } + } + + @ApplicationScoped + static class MyService { + @Inject + CommandExecution execution; + + @Inject + IdService id; + + void process() { + execution.getData().put("id", id.get()); + } + } + + @CommandScoped + static class IdService { + private final String id = UUID.randomUUID().toString(); + + public String get() { + return id; + } + } + + // --- + + public static class MyExtension implements BuildCompatibleExtension { + @Discovery + public void discovery(MetaAnnotations meta, ScannedClasses scan) { + meta.addContext(CommandScoped.class, CommandContext.class); + scan.add(CommandExecutor.class.getName()); + scan.add(CommandDecorator.class.getName()); + } + + @Synthesis + public void synthesis(SyntheticComponents syn) { + syn.addBean(CommandContextController.class) + .type(CommandContextController.class) + .scope(Dependent.class) + .createWith(CommandContextControllerCreator.class); + + syn.addBean(CommandExecution.class) + .type(CommandExecution.class) + .scope(CommandScoped.class) + .createWith(CommandExecutionCreator.class); + } + } + + static class CommandContextControllerCreator implements SyntheticBeanCreator { + @Override + public CommandContextController create(Instance lookup, Parameters params) { + BeanContainer beanContainer = lookup.select(BeanContainer.class).get(); + // TODO use `BeanContainer.getContexts()` since CDI 4.1 + CommandContext ctx = (CommandContext) Arc.container().getContexts(CommandScoped.class).get(0); + return new CommandContextController(ctx, beanContainer); + } + } + + static class CommandExecutionCreator implements SyntheticBeanCreator { + @Override + public CommandExecution create(Instance lookup, Parameters params) { + CommandContext ctx = (CommandContext) lookup.select(BeanContainer.class).get().getContext(CommandScoped.class); + return ctx.getCurrentCommandExecution(); + } + } + + // --- + + /** + * A command. Commands may be beans, but don't necessarily have to. + * For commands that are beans, the {@linkplain CommandScoped command context} + * is automatically activated by the {@link CommandDecorator CommandDecorator}. + * For commands that are not beans, the {@link CommandExecutor CommandExecutor} + * should be used to activate/deactivate the command context. + */ + @FunctionalInterface + public interface Command { + void execute(); + } + + /** + * Specifies that a bean belongs to the command normal scope. + *

+ * A dependent-scoped bean of type {@link CommandContextController CommandContextController} + * is provided that may be used to manually activate and deactivate the command context. + *

+ * A dependent-scoped bean of type {@link CommandExecutor CommandExecutor} is provided that + * may be used to execute a {@link Command Command} implementation which is not a bean. + *

+ * All beans that implement {@link Command Command} are decorated by the {@link CommandDecorator CommandDecorator}, + * which automatically activates (and deactivates) the command context for the duration of the + * {@link Command#execute() Command.execute()} method. + *

+ * A command-scoped bean of type {@link CommandExecution CommandExecution} is provided that contains + * certain details about the command execution and allows exchanging data between beans in the same command scope. + */ + @NormalScope + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + public @interface CommandScoped { + } + + static class CommandExecution { + private final Date startedAt; + + private final Map data; + + CommandExecution() { + this.startedAt = new Date(); + this.data = new HashMap<>(); + } + + Date getStartedAt() { + return startedAt; + } + + Map getData() { + return data; + } + } + + /** + * Allows manual activation and deactivation of the {@linkplain CommandScoped command} context. + * The {@code activate()} method returns {@code true} if the command context was not + * active on the current thread at the moment of the call and hence was activated by the call. + * When the command context was active on the current thread when {@code activate()} is called, + * {@code false} is returned and the operation is otherwise a noop. + *

+ * When {@code activate()} returns {@code true}, the caller is supposed to call + * {@code deactivate()} later on. Calling {@code deactivate()} when the command context + * is not active leads to {@code ContextNotActiveException}. Calling {@code deactivate()} + * when the command context is active but was not activated by this controller is a noop. + */ + static final class CommandContextController { + private final CommandContext context; + + private final BeanContainer beanContainer; + + private final AtomicBoolean activated = new AtomicBoolean(false); + + CommandContextController(CommandContext context, BeanContainer beanContainer) { + this.context = context; + this.beanContainer = beanContainer; + } + + public boolean activate() { + try { + beanContainer.getContext(CommandScoped.class); + return false; + } catch (ContextNotActiveException e) { + context.activate(); + activated.set(true); + return true; + } + } + + public void deactivate() throws ContextNotActiveException { + beanContainer.getContext(CommandScoped.class); + if (activated.compareAndSet(true, false)) { + context.deactivate(); + } + } + } + + /** + * Executes a {@link Command Command} implementation which is not a bean in the command scope. + * That is, the command context is activated before calling {@code Command.execute()} + * and deactivated when {@code Command.execute()} returns (or throws). + *

+ * {@code CommandExecutor} should not be used with {@code Command}s that are beans. Their + * {@code execute()} invocation is automatically decorated by {@link CommandDecorator CommandDecorator}, + * so context activation and deactivation is handled automatically. + */ + @Dependent + static class CommandExecutor { + private final CommandContextController control; + + @Inject + CommandExecutor(CommandContextController control) { + this.control = control; + } + + public void execute(Command command) { + try { + control.activate(); + command.execute(); + } finally { + control.deactivate(); + } + } + } + + /** + * Decorates all {@code Command}s that are beans and automatically activates + * (and deactivates) the {@linkplain CommandScoped command} context for the duration + * of the {@code execute()} invocation. + */ + @Decorator + @Priority(Interceptor.Priority.LIBRARY_BEFORE) + static abstract class CommandDecorator implements Command { + @Inject + @Delegate + Command delegate; + + private CommandContextController control; + + @Inject + CommandDecorator(CommandContextController control) { + this.control = control; + } + + @Override + public void execute() { + try { + control.activate(); + delegate.execute(); + } finally { + control.deactivate(); + } + } + } + + public static class CommandContext implements AlterableContext { + private final ThreadLocal, ContextualInstance>> currentContext = new ThreadLocal<>(); + private final ThreadLocal currentCommandExecution = new ThreadLocal<>(); + + public Class getScope() { + return CommandScoped.class; + } + + public T get(Contextual contextual, CreationalContext creationalContext) { + Map, ContextualInstance> store = currentContext.get(); + + if (store == null) { + throw new ContextNotActiveException(); + } + + ContextualInstance instance = (ContextualInstance) store.get(contextual); + if (instance == null && creationalContext != null) { + instance = new ContextualInstance(contextual.create(creationalContext), creationalContext, contextual); + store.put(contextual, instance); + } + return instance != null ? instance.get() : null; + } + + public T get(Contextual contextual) { + return get(contextual, null); + } + + public boolean isActive() { + return currentContext.get() != null; + } + + public void destroy(Contextual contextual) { + Map, ContextualInstance> ctx = currentContext.get(); + if (ctx == null) { + return; + } + ContextualInstance contextualInstance = ctx.remove(contextual); + if (contextualInstance != null) { + contextualInstance.destroy(); + } + } + + void activate() { + currentContext.set(new HashMap<>()); + currentCommandExecution.set(new CommandExecution()); + } + + void deactivate() { + Map, ContextualInstance> ctx = currentContext.get(); + if (ctx == null) { + return; + } + for (ContextualInstance instance : ctx.values()) { + try { + instance.destroy(); + } catch (Exception e) { + System.err.println("Unable to destroy instance" + instance.get() + " for bean: " + + instance.getContextual()); + } + } + ctx.clear(); + currentContext.remove(); + currentCommandExecution.remove(); + } + + CommandExecution getCurrentCommandExecution() { + return currentCommandExecution.get(); + } + + static final class ContextualInstance { + private final T value; + private final CreationalContext creationalContext; + private final Contextual contextual; + + ContextualInstance(T instance, CreationalContext creationalContext, Contextual contextual) { + this.value = instance; + this.creationalContext = creationalContext; + this.contextual = contextual; + } + + T get() { + return value; + } + + Contextual getContextual() { + return contextual; + } + + void destroy() { + contextual.destroy(value, creationalContext); + } + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomPseudoScopeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomPseudoScopeTest.java new file mode 100644 index 0000000000000..376327a3d99f4 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/CustomPseudoScopeTest.java @@ -0,0 +1,178 @@ +package io.quarkus.arc.test.cdi.bcextensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.context.spi.AlterableContext; +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Discovery; +import jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations; +import jakarta.inject.Inject; +import jakarta.inject.Scope; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class CustomPseudoScopeTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(SingletonBean.class, ApplicationScopedBean.class, RequestScopedBean.class, DependentBean.class, + PrototypeBean.class) + .buildCompatibleExtensions(new MyExtension()) + .build(); + + @Test + public void test() { + ArcContainer container = Arc.container(); + container.requestContext().activate(); + + PrototypeBean prototypeBean = container.select(PrototypeBean.class).get(); + + assertNotEquals(prototypeBean.getId(), + container.select(PrototypeBean.class).get().getId()); + + SingletonBean singletonBean = Arc.container().select(SingletonBean.class).get(); + assertEquals(singletonBean.getPrototypeId(), + Arc.container().select(SingletonBean.class).get().getPrototypeId()); + assertNotEquals(prototypeBean.getId(), singletonBean.getPrototypeId()); + + ApplicationScopedBean applicationScopedBean = Arc.container().select(ApplicationScopedBean.class).get(); + assertEquals(applicationScopedBean.getPrototypeId(), + Arc.container().select(ApplicationScopedBean.class).get().getPrototypeId()); + assertNotEquals(prototypeBean.getId(), applicationScopedBean.getPrototypeId()); + + RequestScopedBean requestScopedBean = Arc.container().select(RequestScopedBean.class).get(); + assertEquals(requestScopedBean.getPrototypeId(), + Arc.container().select(RequestScopedBean.class).get().getPrototypeId()); + assertNotEquals(prototypeBean.getId(), requestScopedBean.getPrototypeId()); + + DependentBean dependentBean = Arc.container().select(DependentBean.class).get(); + assertNotEquals(dependentBean.getPrototypeId(), + Arc.container().select(DependentBean.class).get().getPrototypeId()); + assertNotEquals(prototypeBean.getId(), dependentBean.getPrototypeId()); + } + + @Singleton + static class SingletonBean { + @Inject + PrototypeBean prototype; + + public String getPrototypeId() { + return prototype.getId(); + } + } + + @ApplicationScoped + static class ApplicationScopedBean { + @Inject + PrototypeBean prototype; + + public String getPrototypeId() { + return prototype.getId(); + } + } + + @RequestScoped + static class RequestScopedBean { + @Inject + PrototypeBean prototype; + + public String getPrototypeId() { + return prototype.getId(); + } + } + + @Dependent + static class DependentBean { + @Inject + PrototypeBean prototype; + + public String getPrototypeId() { + return prototype.getId(); + } + } + + @Prototype + static class PrototypeBean { + private final String id = UUID.randomUUID().toString(); + + public String getId() { + return id; + } + } + + // --- + + public static class MyExtension implements BuildCompatibleExtension { + @Discovery + public void discovery(MetaAnnotations meta) { + meta.addContext(Prototype.class, PrototypeContext.class); + } + } + + // --- + + /** + * Specifies that a bean belongs to the prototype pseudo-scope. + *

+ * When a bean is declared to have the {@code @Prototype} scope: + *

    + *
  • Each injection point or dynamic lookup receives a new instance; instances are never shared.
  • + *
  • Lifecycle of instances is not managed by the CDI container.
  • + *
+ *

+ * Every invocation of the {@link Context#get(Contextual, CreationalContext)} operation on the + * context object for the {@code @Prototype} scope returns a new instance of given bean. + *

+ * Every invocation of the {@link Context#get(Contextual)} operation on the context object for the + * {@code @Prototype} scope returns a {@code null} value. + *

+ * The {@code @Prototype} scope is always active. + */ + @Scope + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + public @interface Prototype { + } + + public static class PrototypeContext implements AlterableContext { + public Class getScope() { + return Prototype.class; + } + + public T get(Contextual contextual, CreationalContext creationalContext) { + return creationalContext != null ? contextual.create(creationalContext) : null; + } + + public T get(Contextual contextual) { + return null; + } + + public boolean isActive() { + return true; + } + + public void destroy(Contextual contextual) { + } + } +}