Skip to content

Commit

Permalink
Add TCK tests for registering custom scopes/contexts with Build Compa…
Browse files Browse the repository at this point in the history
…tible Extensions
  • Loading branch information
Ladicek committed Dec 1, 2023
1 parent 16b7faa commit deeb980
Show file tree
Hide file tree
Showing 20 changed files with 560 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

/**
* A <em>command</em>. Should not be a bean. The {@link CommandExecutor CommandExecutor}
* should be used to execute a command with automatic activation/deactivation of the command context.
*/
@FunctionalInterface
public interface Command {
void execute();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.context.ContextNotActiveException;
import jakarta.enterprise.context.spi.AlterableContext;
import jakarta.enterprise.context.spi.Contextual;
import jakarta.enterprise.context.spi.CreationalContext;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

public class CommandContext implements AlterableContext {
private final ThreadLocal<Map<Contextual<?>, ContextualInstance<?>>> currentContext = new ThreadLocal<>();
private final ThreadLocal<CommandExecution> currentCommandExecution = new ThreadLocal<>();

public Class<? extends Annotation> getScope() {
return CommandScoped.class;
}

public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContext) {
Map<Contextual<?>, ContextualInstance<?>> store = currentContext.get();

if (store == null) {
throw new ContextNotActiveException();
}

ContextualInstance<T> instance = (ContextualInstance<T>) store.get(contextual);
if (instance == null && creationalContext != null) {
instance = new ContextualInstance<T>(contextual.create(creationalContext), creationalContext, contextual);
store.put(contextual, instance);
}
return instance != null ? instance.get() : null;
}

public <T> T get(Contextual<T> contextual) {
return get(contextual, null);
}

public boolean isActive() {
return currentContext.get() != null;
}

public void destroy(Contextual<?> contextual) {
Map<Contextual<?>, 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<Contextual<?>, 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<T> {
private final T value;
private final CreationalContext<T> creationalContext;
private final Contextual<T> contextual;

ContextualInstance(T instance, CreationalContext<T> creationalContext, Contextual<T> contextual) {
this.value = instance;
this.creationalContext = creationalContext;
this.contextual = contextual;
}

T get() {
return value;
}

Contextual<T> getContextual() {
return contextual;
}

void destroy() {
contextual.destroy(value, creationalContext);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.context.ContextNotActiveException;
import jakarta.enterprise.inject.spi.BeanContainer;

import java.util.concurrent.atomic.AtomicBoolean;

/**
* 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.
* <p>
* 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.
*/
public 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.build.compatible.spi.Parameters;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator;
import jakarta.enterprise.inject.spi.BeanContainer;

public class CommandContextControllerCreator implements SyntheticBeanCreator<CommandContextController> {
@Override
public CommandContextController create(Instance<Object> lookup, Parameters params) {
BeanContainer beanContainer = lookup.select(BeanContainer.class).get();
CommandContext ctx = (CommandContext) beanContainer.getContexts(CommandScoped.class).iterator().next();
return new CommandContextController(ctx, beanContainer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class CommandExecution {
private final Date startedAt;

private final Map<String, Object> data;

CommandExecution() {
this.startedAt = new Date();
this.data = new HashMap<>();
}

Date getStartedAt() {
return startedAt;
}

Map<String, Object> getData() {
return data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.build.compatible.spi.Parameters;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator;
import jakarta.enterprise.inject.spi.BeanContainer;

public class CommandExecutionCreator implements SyntheticBeanCreator<CommandExecution> {
@Override
public CommandExecution create(Instance<Object> lookup, Parameters params) {
CommandContext ctx = (CommandContext) lookup.select(BeanContainer.class).get().getContext(CommandScoped.class);
return ctx.getCurrentCommandExecution();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;

/**
* Executes a {@link Command Command} 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).
*/
@Dependent
public 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.context.NormalScope;

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;

/**
* Specifies that a bean belongs to the <em>command</em> normal scope.
* <p>
* A dependent-scoped bean of type {@link CommandContextController CommandContextController}
* is provided that may be used to manually activate and deactivate the command context.
* <p>
* A dependent-scoped bean of type {@link CommandExecutor CommandExecutor} is provided that
* may be used to execute a {@link Command Command} implementation, activating and deactivating
* the command scope automatically.
* <p>
* 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 {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.context.Dependent;
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.ScannedClasses;
import jakarta.enterprise.inject.build.compatible.spi.Synthesis;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents;

public class CustomNormalScopeExtension implements BuildCompatibleExtension {
@Discovery
public void discovery(MetaAnnotations meta, ScannedClasses scan) {
meta.addContext(CommandScoped.class, CommandContext.class);
scan.add(CommandExecutor.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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.cdi.tck.AbstractTest;
import org.jboss.cdi.tck.cdi.Sections;
import org.jboss.cdi.tck.shrinkwrap.WebArchiveBuilder;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.test.audit.annotations.SpecAssertion;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

// this test is basically a stripped down version of https://github.com/weld/command-context-example
public class CustomNormalScopeTest extends AbstractTest {
@Deployment
public static WebArchive createTestArchive() {
return new WebArchiveBuilder()
.withTestClassPackage(CustomNormalScopeTest.class)
.withBuildCompatibleExtension(CustomNormalScopeExtension.class)
.build();
}

@Test
@SpecAssertion(section = Sections.DISCOVERY_PHASE, id = "a", note = "Register custom normal scope")
public void commandContextController() {
CommandContextController control = getContextualReference(CommandContextController.class);
boolean activated = control.activate();
assertTrue(activated);
try {
assertEquals(getContextualReference(IdService.class).get(), getContextualReference(IdService.class).get());
} finally {
control.deactivate();
}
}

@Test
@SpecAssertion(section = Sections.DISCOVERY_PHASE, id = "a", note = "Register custom normal scope")
public void commandExecutor() {
CommandExecutor executor = getContextualReference(CommandExecutor.class);
executor.execute(() -> {
CommandExecution execution = getContextualReference(CommandExecution.class);
IdService idService = getContextualReference(IdService.class);

getContextualReference(MyService.class).process();
assertEquals(execution.getData().get("id"), idService.get());
assertNotNull(execution.getStartedAt());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import java.util.UUID;

@CommandScoped
public class IdService {
private final String id = UUID.randomUUID().toString();

public String get() {
return id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.jboss.cdi.tck.tests.build.compatible.extensions.customNormalScope;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class MyService {
@Inject
CommandExecution execution;

@Inject
IdService id;

public void process() {
execution.getData().put("id", id.get());
}
}
Loading

0 comments on commit deeb980

Please sign in to comment.