This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch concurency
in repository https://gitbox.apache.org/repos/asf/tomee.git
The following commit(s) were added to refs/heads/concurency by this push:
new 93da38e8f7 Add CDI qualifier support for Concurrency 3.1 resource
definitions
93da38e8f7 is described below
commit 93da38e8f77cfff263870621576d51873c60521f
Author: Richard Zowalla <[email protected]>
AuthorDate: Fri Apr 3 20:30:34 2026 +0200
Add CDI qualifier support for Concurrency 3.1 resource definitions
Register concurrency resources (ManagedExecutorService,
ManagedScheduledExecutorService,
ManagedThreadFactory, ContextService) as CDI beans with qualifier support
per
Concurrency 3.1 spec Section 5.4.1.
- ConcurrencyCDIExtension: CDI extension that observes AfterBeanDiscovery
and creates
synthetic ApplicationScoped beans for resources with qualifiers. Also
registers
default beans (@Default/@Any) for all four concurrency resource types.
- AnnotationDeployer: Extract qualifiers() from @ManagedExecutorDefinition,
@ManagedScheduledExecutorDefinition, @ManagedThreadFactoryDefinition,
@ContextServiceDefinition annotations into JEE model objects.
- Convert*Definitions: Pass Qualifiers as comma-separated Resource property.
- OptimizedLoaderService: Register ConcurrencyCDIExtension alongside
JMS2CDIExtension.
TCK Web profile: 196/196 passing (0 failures, 0 errors).
---
.../apache/openejb/cdi/OptimizedLoaderService.java | 2 +
.../cdi/concurrency/ConcurrencyCDIExtension.java | 530 +++++++++++++++++++++
.../apache/openejb/config/AnnotationDeployer.java | 24 +
.../config/ConvertContextServiceDefinitions.java | 3 +
.../ConvertManagedExecutorServiceDefinitions.java | 5 +
...ManagedScheduledExecutorServiceDefinitions.java | 5 +
.../ConvertManagedThreadFactoryDefinitions.java | 5 +
.../concurrency/ConcurrencyCDIExtensionTest.java | 184 +++++++
8 files changed, 758 insertions(+)
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java
b/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java
index 815eba8f0d..99f3ec075d 100644
---
a/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java
+++
b/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java
@@ -126,6 +126,8 @@ public class OptimizedLoaderService implements
LoaderService {
list.add(new JMS2CDIExtension());
}
+ list.add(new
org.apache.openejb.cdi.concurrency.ConcurrencyCDIExtension());
+
final Collection<Extension> extensionCopy = new ArrayList<>(list);
final Iterator<Extension> it = list.iterator();
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/ConcurrencyCDIExtension.java
b/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/ConcurrencyCDIExtension.java
new file mode 100644
index 0000000000..1ff05d711d
--- /dev/null
+++
b/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/ConcurrencyCDIExtension.java
@@ -0,0 +1,530 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.openejb.cdi.concurrency;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Default;
+import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.enterprise.inject.spi.Extension;
+import jakarta.enterprise.util.Nonbinding;
+import jakarta.inject.Qualifier;
+import org.apache.openejb.AppContext;
+import org.apache.openejb.assembler.classic.OpenEjbConfiguration;
+import org.apache.openejb.assembler.classic.ResourceInfo;
+import org.apache.openejb.loader.SystemInstance;
+import org.apache.openejb.spi.ContainerSystem;
+import org.apache.openejb.util.LogCategory;
+import org.apache.openejb.util.Logger;
+import org.apache.webbeans.config.WebBeansContext;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * CDI extension that registers concurrency resources as CDI beans
+ * with qualifier support per Concurrency 3.1 spec (Section 5.4.1).
+ *
+ * <p>Resources defined via {@code @ManagedExecutorDefinition} (and similar)
+ * or deployment descriptor {@code <managed-executor>} elements that specify
+ * {@code qualifiers} become injectable via {@code @Inject @MyQualifier}.
+ *
+ * <p>Default resources (e.g. {@code java:comp/DefaultManagedExecutorService})
+ * are always registered with {@code @Default} and {@code @Any} qualifiers.
+ */
+public class ConcurrencyCDIExtension implements Extension {
+
+ private static final Logger logger =
Logger.getInstance(LogCategory.OPENEJB.createChild("cdi"),
ConcurrencyCDIExtension.class);
+
+ private static final String QUALIFIERS_PROPERTY = "Qualifiers";
+
+ private static final String DEFAULT_MES_JNDI =
"java:comp/DefaultManagedExecutorService";
+ private static final String DEFAULT_MSES_JNDI =
"java:comp/DefaultManagedScheduledExecutorService";
+ private static final String DEFAULT_MTF_JNDI =
"java:comp/DefaultManagedThreadFactory";
+ private static final String DEFAULT_CS_JNDI =
"java:comp/DefaultContextService";
+
+ private static final String DEFAULT_MES_ID = "Default Executor Service";
+ private static final String DEFAULT_MSES_ID = "Default Scheduled Executor
Service";
+ private static final String DEFAULT_MTF_ID = "Default Managed Thread
Factory";
+ private static final String DEFAULT_CS_ID = "Default Context Service";
+
+ private enum ResourceKind {
+
MANAGED_EXECUTOR(jakarta.enterprise.concurrent.ManagedExecutorService.class),
+
MANAGED_SCHEDULED_EXECUTOR(jakarta.enterprise.concurrent.ManagedScheduledExecutorService.class),
+
MANAGED_THREAD_FACTORY(jakarta.enterprise.concurrent.ManagedThreadFactory.class),
+ CONTEXT_SERVICE(jakarta.enterprise.concurrent.ContextService.class);
+
+ private final Class<?> type;
+
+ ResourceKind(final Class<?> type) {
+ this.type = type;
+ }
+ }
+
+ void registerBeans(@Observes final AfterBeanDiscovery afterBeanDiscovery,
final BeanManager beanManager) {
+ final OpenEjbConfiguration openEjbConfiguration =
SystemInstance.get().getComponent(OpenEjbConfiguration.class);
+ if (openEjbConfiguration == null || openEjbConfiguration.facilities ==
null) {
+ return;
+ }
+
+ final List<ResourceInfo> resources =
openEjbConfiguration.facilities.resources;
+ final Set<String> currentAppIds = findCurrentAppIds();
+
+ for (final ResourceInfo resource : resources) {
+ if (!isVisibleInCurrentApp(resource, currentAppIds)) {
+ continue;
+ }
+
+ final ResourceKind resourceKind = findResourceKind(resource);
+ if (resourceKind == null) {
+ continue;
+ }
+
+ final List<String> qualifierNames = parseQualifiers(resource);
+ if (qualifierNames.isEmpty()) {
+ continue;
+ }
+
+ // Spec: qualifiers must not be used with java:global names
+ if (isJavaGlobalName(resource.jndiName)) {
+ afterBeanDiscovery.addDefinitionError(new
IllegalArgumentException(resourceKind.type.getName()
+ + " with qualifiers must not use a java:global name: "
+ normalizeJndiName(resource.jndiName)));
+ continue;
+ }
+
+ final Set<Annotation> qualifiers =
validateAndCreateQualifiers(qualifierNames, resourceKind, afterBeanDiscovery);
+ if (qualifiers == null) {
+ continue;
+ }
+
+ logger.info("Registering CDI bean for " +
resourceKind.type.getSimpleName()
+ + " resource '" + resource.id + "' with qualifiers " +
qualifierNames);
+ addQualifiedBean(afterBeanDiscovery, resourceKind.type,
resource.id, qualifiers);
+ }
+
+ // Register default beans with @Default + @Any if no bean with
@Default exists yet
+ registerDefaultBeanIfMissing(afterBeanDiscovery, beanManager,
resources,
+ jakarta.enterprise.concurrent.ManagedExecutorService.class,
DEFAULT_MES_JNDI, DEFAULT_MES_ID);
+ registerDefaultBeanIfMissing(afterBeanDiscovery, beanManager,
resources,
+
jakarta.enterprise.concurrent.ManagedScheduledExecutorService.class,
DEFAULT_MSES_JNDI, DEFAULT_MSES_ID);
+ registerDefaultBeanIfMissing(afterBeanDiscovery, beanManager,
resources,
+ jakarta.enterprise.concurrent.ManagedThreadFactory.class,
DEFAULT_MTF_JNDI, DEFAULT_MTF_ID);
+ registerDefaultBeanIfMissing(afterBeanDiscovery, beanManager,
resources,
+ jakarta.enterprise.concurrent.ContextService.class,
DEFAULT_CS_JNDI, DEFAULT_CS_ID);
+ }
+
+ /**
+ * Validates qualifier class names per Concurrency 3.1 spec:
+ * <ul>
+ * <li>Must be loadable annotation types</li>
+ * <li>Must be annotated with {@code @Qualifier}</li>
+ * <li>All members must have default values</li>
+ * <li>All members must be annotated with {@code @Nonbinding}</li>
+ * </ul>
+ */
+ private Set<Annotation> validateAndCreateQualifiers(final List<String>
qualifierNames,
+ final ResourceKind
resourceKind,
+ final
AfterBeanDiscovery afterBeanDiscovery) {
+ final Set<Annotation> qualifiers = new LinkedHashSet<>();
+ qualifiers.add(Any.Literal.INSTANCE);
+ final ClassLoader loader =
Thread.currentThread().getContextClassLoader();
+
+ for (final String qualifierName : qualifierNames) {
+ final Class<?> qualifierClass;
+ try {
+ qualifierClass = loader.loadClass(qualifierName);
+ } catch (final ClassNotFoundException e) {
+ afterBeanDiscovery.addDefinitionError(new
IllegalArgumentException("Qualifier class " + qualifierName
+ + " for " + resourceKind.type.getName() + " cannot be
loaded", e));
+ return null;
+ }
+
+ if (!qualifierClass.isAnnotation()) {
+ afterBeanDiscovery.addDefinitionError(new
IllegalArgumentException("Qualifier " + qualifierName
+ + " for " + resourceKind.type.getName() + " must be an
annotation type"));
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ final Class<? extends Annotation> annotationClass = (Class<?
extends Annotation>) qualifierClass;
+ if (!annotationClass.isAnnotationPresent(Qualifier.class)) {
+ afterBeanDiscovery.addDefinitionError(new
IllegalArgumentException("Qualifier " + qualifierName
+ + " for " + resourceKind.type.getName() + " must be
annotated with @Qualifier"));
+ return null;
+ }
+
+ for (final Method member : annotationClass.getDeclaredMethods()) {
+ if (member.getDefaultValue() == null) {
+ afterBeanDiscovery.addDefinitionError(new
IllegalArgumentException("Qualifier " + qualifierName
+ + " for " + resourceKind.type.getName() + " must
not declare members without defaults"));
+ return null;
+ }
+ if (!member.isAnnotationPresent(Nonbinding.class)) {
+ afterBeanDiscovery.addDefinitionError(new
IllegalArgumentException("Qualifier " + qualifierName
+ + " for " + resourceKind.type.getName() + " must
use @Nonbinding on member " + member.getName()));
+ return null;
+ }
+ }
+
+ qualifiers.add(createQualifierAnnotation(annotationClass));
+ }
+
+ return qualifiers;
+ }
+
+ private Annotation createQualifierAnnotation(final Class<? extends
Annotation> qualifierType) {
+ final Map<String, Object> values = new LinkedHashMap<>();
+ for (final Method method : qualifierType.getDeclaredMethods()) {
+ values.put(method.getName(), method.getDefaultValue());
+ }
+
+ final InvocationHandler handler = (final Object proxy, final Method
method, final Object[] args) -> {
+ final String name = method.getName();
+ if ("annotationType".equals(name) && method.getParameterCount() ==
0) {
+ return qualifierType;
+ }
+ if ("equals".equals(name) && method.getParameterCount() == 1) {
+ return annotationEquals(qualifierType, values, args[0]);
+ }
+ if ("hashCode".equals(name) && method.getParameterCount() == 0) {
+ return annotationHashCode(values);
+ }
+ if ("toString".equals(name) && method.getParameterCount() == 0) {
+ return annotationToString(qualifierType, values);
+ }
+ if (values.containsKey(name)) {
+ return values.get(name);
+ }
+ throw new IllegalStateException("Unsupported annotation method: "
+ method);
+ };
+
+ return Annotation.class.cast(Proxy.newProxyInstance(
+ qualifierType.getClassLoader(),
+ new Class<?>[] { qualifierType },
+ handler));
+ }
+
+ private boolean annotationEquals(final Class<? extends Annotation>
qualifierType,
+ final Map<String, Object> values,
+ final Object other) {
+ if (other == null || !qualifierType.isInstance(other)) {
+ return false;
+ }
+ for (final Map.Entry<String, Object> entry : values.entrySet()) {
+ try {
+ final Method method = qualifierType.getMethod(entry.getKey());
+ if (!memberValueEquals(entry.getValue(),
method.invoke(other))) {
+ return false;
+ }
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private int annotationHashCode(final Map<String, Object> values) {
+ int hash = 0;
+ for (final Map.Entry<String, Object> entry : values.entrySet()) {
+ hash += (127 * entry.getKey().hashCode()) ^
memberValueHashCode(entry.getValue());
+ }
+ return hash;
+ }
+
+ private String annotationToString(final Class<? extends Annotation>
qualifierType,
+ final Map<String, Object> values) {
+ final StringBuilder builder = new
StringBuilder("@").append(qualifierType.getName()).append("(");
+ boolean first = true;
+ for (final Map.Entry<String, Object> entry : values.entrySet()) {
+ if (!first) {
+ builder.append(", ");
+ }
+
builder.append(entry.getKey()).append("=").append(entry.getValue());
+ first = false;
+ }
+ return builder.append(")").toString();
+ }
+
+ private int memberValueHashCode(final Object value) {
+ final Class<?> valueType = value.getClass();
+ if (!valueType.isArray()) {
+ return value.hashCode();
+ }
+ if (valueType == byte[].class) {
+ return Arrays.hashCode((byte[]) value);
+ }
+ if (valueType == short[].class) {
+ return Arrays.hashCode((short[]) value);
+ }
+ if (valueType == int[].class) {
+ return Arrays.hashCode((int[]) value);
+ }
+ if (valueType == long[].class) {
+ return Arrays.hashCode((long[]) value);
+ }
+ if (valueType == char[].class) {
+ return Arrays.hashCode((char[]) value);
+ }
+ if (valueType == float[].class) {
+ return Arrays.hashCode((float[]) value);
+ }
+ if (valueType == double[].class) {
+ return Arrays.hashCode((double[]) value);
+ }
+ if (valueType == boolean[].class) {
+ return Arrays.hashCode((boolean[]) value);
+ }
+ return Arrays.hashCode((Object[]) value);
+ }
+
+ private boolean memberValueEquals(final Object left, final Object right) {
+ if (left == right) {
+ return true;
+ }
+ if (left == null || right == null) {
+ return false;
+ }
+ final Class<?> valueType = left.getClass();
+ if (!valueType.isArray()) {
+ return left.equals(right);
+ }
+ if (valueType == byte[].class) {
+ return Arrays.equals((byte[]) left, (byte[]) right);
+ }
+ if (valueType == short[].class) {
+ return Arrays.equals((short[]) left, (short[]) right);
+ }
+ if (valueType == int[].class) {
+ return Arrays.equals((int[]) left, (int[]) right);
+ }
+ if (valueType == long[].class) {
+ return Arrays.equals((long[]) left, (long[]) right);
+ }
+ if (valueType == char[].class) {
+ return Arrays.equals((char[]) left, (char[]) right);
+ }
+ if (valueType == float[].class) {
+ return Arrays.equals((float[]) left, (float[]) right);
+ }
+ if (valueType == double[].class) {
+ return Arrays.equals((double[]) left, (double[]) right);
+ }
+ if (valueType == boolean[].class) {
+ return Arrays.equals((boolean[]) left, (boolean[]) right);
+ }
+ return Arrays.equals((Object[]) left, (Object[]) right);
+ }
+
+ private <T> void addQualifiedBean(final AfterBeanDiscovery
afterBeanDiscovery,
+ final Class<T> type,
+ final String resourceId,
+ final Set<Annotation> qualifiers) {
+ afterBeanDiscovery.addBean()
+ .id("tomee.concurrency." + type.getName() + "#" + resourceId +
"#" + qualifiers.hashCode())
+ .beanClass(type)
+ .types(Object.class, type)
+ .qualifiers(qualifiers.toArray(new Annotation[0]))
+ .scope(ApplicationScoped.class)
+ .createWith(creationalContext -> lookupByResourceId(type,
resourceId));
+ }
+
+ private <T> void registerDefaultBeanIfMissing(final AfterBeanDiscovery
afterBeanDiscovery,
+ final BeanManager
beanManager,
+ final List<ResourceInfo>
resources,
+ final Class<T> type,
+ final String jndiName,
+ final String
defaultResourceId) {
+ if (!beanManager.getBeans(type, Default.Literal.INSTANCE).isEmpty()) {
+ return;
+ }
+
+ final String resourceId = findResourceId(resources, type, jndiName,
defaultResourceId);
+ logger.debug("Registering default CDI bean for " +
type.getSimpleName() + " (resource '" + resourceId + "')");
+ afterBeanDiscovery.addBean()
+ .id("tomee.concurrency.default." + type.getName() + "#" +
resourceId)
+ .beanClass(type)
+ .types(Object.class, type)
+ .qualifiers(Default.Literal.INSTANCE, Any.Literal.INSTANCE)
+ .scope(ApplicationScoped.class)
+ .createWith(creationalContext -> lookupDefaultResource(type,
jndiName, resourceId));
+ }
+
+ private <T> String findResourceId(final List<ResourceInfo> resources,
+ final Class<T> type,
+ final String jndiName,
+ final String defaultResourceId) {
+ for (final ResourceInfo resource : resources) {
+ if (!isResourceType(resource, type)) {
+ continue;
+ }
+ final String normalized = normalizeJndiName(resource.jndiName);
+ if (Objects.equals(normalized, normalizeJndiName(jndiName))) {
+ return resource.id;
+ }
+ }
+ return defaultResourceId;
+ }
+
+ private <T> T lookupByResourceId(final Class<T> type, final String
resourceId) {
+ final ContainerSystem containerSystem =
SystemInstance.get().getComponent(ContainerSystem.class);
+ if (containerSystem == null) {
+ throw new IllegalStateException("ContainerSystem is not
available");
+ }
+
+ Object instance;
+ try {
+ instance =
containerSystem.getJNDIContext().lookup("openejb/Resource/" + resourceId);
+ } catch (final NamingException firstFailure) {
+ try {
+ instance =
containerSystem.getJNDIContext().lookup("openejb:Resource/" + resourceId);
+ } catch (final NamingException secondFailure) {
+ throw new IllegalStateException("Unable to lookup resource " +
resourceId, secondFailure);
+ }
+ }
+
+ if (!type.isInstance(instance)) {
+ throw new IllegalStateException("Resource " + resourceId + " is
not of type " + type.getName()
+ + ", found " + (instance == null ? "null" :
instance.getClass().getName()));
+ }
+ return type.cast(instance);
+ }
+
+ private <T> T lookupDefaultResource(final Class<T> type, final String
jndiName, final String resourceId) {
+ try {
+ return lookupByJndiName(type, jndiName);
+ } catch (final IllegalStateException firstFailure) {
+ try {
+ return lookupByResourceId(type, resourceId);
+ } catch (final IllegalStateException secondFailure) {
+ secondFailure.addSuppressed(firstFailure);
+ throw secondFailure;
+ }
+ }
+ }
+
+ private <T> T lookupByJndiName(final Class<T> type, final String jndiName)
{
+ final Object instance;
+ try {
+ instance = InitialContext.doLookup(jndiName);
+ } catch (final NamingException e) {
+ throw new IllegalStateException("Unable to lookup resource " +
jndiName, e);
+ }
+
+ if (!type.isInstance(instance)) {
+ throw new IllegalStateException("Resource " + jndiName + " is not
of type " + type.getName()
+ + ", found " + (instance == null ? "null" :
instance.getClass().getName()));
+ }
+ return type.cast(instance);
+ }
+
+ private List<String> parseQualifiers(final ResourceInfo resource) {
+ if (resource.properties == null) {
+ return List.of();
+ }
+ final String value =
resource.properties.getProperty(QUALIFIERS_PROPERTY);
+ if (value == null || value.isBlank()) {
+ return List.of();
+ }
+
+ final List<String> qualifiers = new ArrayList<>();
+ for (final String item : value.split(",")) {
+ final String qualifier = item.trim();
+ if (!qualifier.isEmpty()) {
+ qualifiers.add(qualifier);
+ }
+ }
+ return qualifiers;
+ }
+
+ private ResourceKind findResourceKind(final ResourceInfo resource) {
+ // Check MSES before MES since MSES extends MES
+ if (isResourceType(resource,
jakarta.enterprise.concurrent.ManagedScheduledExecutorService.class)) {
+ return ResourceKind.MANAGED_SCHEDULED_EXECUTOR;
+ }
+ if (isResourceType(resource,
jakarta.enterprise.concurrent.ManagedExecutorService.class)) {
+ return ResourceKind.MANAGED_EXECUTOR;
+ }
+ if (isResourceType(resource,
jakarta.enterprise.concurrent.ManagedThreadFactory.class)) {
+ return ResourceKind.MANAGED_THREAD_FACTORY;
+ }
+ if (isResourceType(resource,
jakarta.enterprise.concurrent.ContextService.class)) {
+ return ResourceKind.CONTEXT_SERVICE;
+ }
+ return null;
+ }
+
+ private boolean isResourceType(final ResourceInfo resource, final Class<?>
type) {
+ return resource.types != null
+ && (resource.types.contains(type.getName()) ||
resource.types.contains(type.getSimpleName()));
+ }
+
+ private boolean isJavaGlobalName(final String rawName) {
+ final String normalized = normalizeJndiName(rawName);
+ return normalized != null && normalized.startsWith("global/");
+ }
+
+ private String normalizeJndiName(final String rawName) {
+ if (rawName == null) {
+ return null;
+ }
+ return rawName.startsWith("java:") ?
rawName.substring("java:".length()) : rawName;
+ }
+
+ private boolean isVisibleInCurrentApp(final ResourceInfo resource, final
Set<String> currentAppIds) {
+ if (resource.originAppName == null ||
resource.originAppName.isEmpty()) {
+ return true;
+ }
+ return currentAppIds.contains(resource.originAppName);
+ }
+
+ private Set<String> findCurrentAppIds() {
+ final ContainerSystem containerSystem =
SystemInstance.get().getComponent(ContainerSystem.class);
+ if (containerSystem == null) {
+ return Set.of();
+ }
+
+ final Set<String> appIds = new LinkedHashSet<>();
+ final ClassLoader tccl =
Thread.currentThread().getContextClassLoader();
+ final WebBeansContext currentWbc;
+ try {
+ currentWbc = WebBeansContext.currentInstance();
+ } catch (final RuntimeException re) {
+ return Set.of();
+ }
+
+ for (final AppContext appContext : containerSystem.getAppContexts()) {
+ if (appContext.getWebBeansContext() == currentWbc ||
appContext.getClassLoader() == tccl) {
+ appIds.add(appContext.getId());
+ }
+ }
+ return appIds;
+ }
+}
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java
b/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java
index a81b9dd55e..16c4d69a38 100644
---
a/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java
+++
b/container/openejb-core/src/main/java/org/apache/openejb/config/AnnotationDeployer.java
@@ -4127,6 +4127,12 @@ public class AnnotationDeployer implements
DynamicDeployer {
contextService.getUnchanged().addAll(Arrays.asList(definition.unchanged()));
}
+ if (contextService.getQualifier().isEmpty() &&
definition.qualifiers().length > 0) {
+ for (final Class<?> qualifier : definition.qualifiers()) {
+ contextService.getQualifier().add(qualifier.getName());
+ }
+ }
+
consumer.getContextServiceMap().put(definition.name(),
contextService);
}
@@ -4142,6 +4148,12 @@ public class AnnotationDeployer implements
DynamicDeployer {
managedExecutor.setMaxAsync(definition.maxAsync() == -1 ? null :
definition.maxAsync());
managedExecutor.setVirtual(definition.virtual() ? Boolean.TRUE :
null);
+ if (managedExecutor.getQualifier().isEmpty() &&
definition.qualifiers().length > 0) {
+ for (final Class<?> qualifier : definition.qualifiers()) {
+ managedExecutor.getQualifier().add(qualifier.getName());
+ }
+ }
+
consumer.getManagedExecutorMap().put(definition.name(),
managedExecutor);
}
@@ -4157,6 +4169,12 @@ public class AnnotationDeployer implements
DynamicDeployer {
managedScheduledExecutor.setMaxAsync(definition.maxAsync() == -1 ?
null : definition.maxAsync());
managedScheduledExecutor.setVirtual(definition.virtual() ?
Boolean.TRUE : null);
+ if (managedScheduledExecutor.getQualifier().isEmpty() &&
definition.qualifiers().length > 0) {
+ for (final Class<?> qualifier : definition.qualifiers()) {
+
managedScheduledExecutor.getQualifier().add(qualifier.getName());
+ }
+ }
+
consumer.getManagedScheduledExecutorMap().put(definition.name(),
managedScheduledExecutor);
}
@@ -4171,6 +4189,12 @@ public class AnnotationDeployer implements
DynamicDeployer {
managedThreadFactory.setPriority(definition.priority());
managedThreadFactory.setVirtual(definition.virtual() ?
Boolean.TRUE : null);
+ if (managedThreadFactory.getQualifier().isEmpty() &&
definition.qualifiers().length > 0) {
+ for (final Class<?> qualifier : definition.qualifiers()) {
+
managedThreadFactory.getQualifier().add(qualifier.getName());
+ }
+ }
+
consumer.getManagedThreadFactoryMap().put(definition.name(),
managedThreadFactory);
}
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertContextServiceDefinitions.java
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertContextServiceDefinitions.java
index 6fc60d12d9..d037f7b308 100755
---
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertContextServiceDefinitions.java
+++
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertContextServiceDefinitions.java
@@ -88,6 +88,9 @@ public class ConvertContextServiceDefinitions extends
BaseConvertDefinitions {
put(p, "Propagated", Join.join(",", contextService.getPropagated()));
put(p, "Cleared", Join.join(",", contextService.getCleared()));
put(p, "Unchanged", Join.join(",", contextService.getUnchanged()));
+ if (contextService.getQualifier() != null &&
!contextService.getQualifier().isEmpty()) {
+ put(p, "Qualifiers", Join.join(",",
contextService.getQualifier()));
+ }
// to force it to be bound in JndiEncBuilder
put(p, "JndiName", def.getJndi());
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedExecutorServiceDefinitions.java
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedExecutorServiceDefinitions.java
index c55e2cd489..7e226c7b2d 100644
---
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedExecutorServiceDefinitions.java
+++
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedExecutorServiceDefinitions.java
@@ -23,6 +23,8 @@ import org.apache.openejb.jee.KeyedCollection;
import org.apache.openejb.jee.ManagedExecutor;
import org.apache.openejb.util.PropertyPlaceHolderHelper;
+import org.apache.openejb.util.Join;
+
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -91,6 +93,9 @@ public class ConvertManagedExecutorServiceDefinitions extends
BaseConvertDefinit
put(p, "HungTaskThreshold", managedExecutor.getHungTaskThreshold());
put(p, "Max", managedExecutor.getMaxAsync());
put(p, "Virtual", managedExecutor.getVirtual());
+ if (managedExecutor.getQualifier() != null &&
!managedExecutor.getQualifier().isEmpty()) {
+ put(p, "Qualifiers", Join.join(",",
managedExecutor.getQualifier()));
+ }
// to force it to be bound in JndiEncBuilder
put(p, "JndiName", def.getJndi());
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedScheduledExecutorServiceDefinitions.java
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedScheduledExecutorServiceDefinitions.java
index cb0e738c69..f9dee71be5 100644
---
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedScheduledExecutorServiceDefinitions.java
+++
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedScheduledExecutorServiceDefinitions.java
@@ -23,6 +23,8 @@ import org.apache.openejb.jee.KeyedCollection;
import org.apache.openejb.jee.ManagedScheduledExecutor;
import org.apache.openejb.util.PropertyPlaceHolderHelper;
+import org.apache.openejb.util.Join;
+
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -91,6 +93,9 @@ public class
ConvertManagedScheduledExecutorServiceDefinitions extends BaseConve
put(p, "HungTaskThreshold",
managedScheduledExecutor.getHungTaskThreshold());
put(p, "Core", managedScheduledExecutor.getMaxAsync());
put(p, "Virtual", managedScheduledExecutor.getVirtual());
+ if (managedScheduledExecutor.getQualifier() != null &&
!managedScheduledExecutor.getQualifier().isEmpty()) {
+ put(p, "Qualifiers", Join.join(",",
managedScheduledExecutor.getQualifier()));
+ }
// to force it to be bound in JndiEncBuilder
put(p, "JndiName", def.getJndi());
diff --git
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedThreadFactoryDefinitions.java
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedThreadFactoryDefinitions.java
index 3e1c936db4..69fb38c88c 100644
---
a/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedThreadFactoryDefinitions.java
+++
b/container/openejb-core/src/main/java/org/apache/openejb/config/ConvertManagedThreadFactoryDefinitions.java
@@ -24,6 +24,8 @@ import org.apache.openejb.jee.ManagedThreadFactory;
import org.apache.openejb.jee.ManagedThreadFactory;
import org.apache.openejb.util.PropertyPlaceHolderHelper;
+import org.apache.openejb.util.Join;
+
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -90,6 +92,9 @@ public class ConvertManagedThreadFactoryDefinitions extends
BaseConvertDefinitio
put(p, "Context", contextName);
put(p, "Priority", managedThreadFactory.getPriority());
put(p, "Virtual", managedThreadFactory.getVirtual());
+ if (managedThreadFactory.getQualifier() != null &&
!managedThreadFactory.getQualifier().isEmpty()) {
+ put(p, "Qualifiers", Join.join(",",
managedThreadFactory.getQualifier()));
+ }
// to force it to be bound in JndiEncBuilder
put(p, "JndiName", def.getJndi());
diff --git
a/container/openejb-core/src/test/java/org/apache/openejb/cdi/concurrency/ConcurrencyCDIExtensionTest.java
b/container/openejb-core/src/test/java/org/apache/openejb/cdi/concurrency/ConcurrencyCDIExtensionTest.java
new file mode 100644
index 0000000000..e3e561f11d
--- /dev/null
+++
b/container/openejb-core/src/test/java/org/apache/openejb/cdi/concurrency/ConcurrencyCDIExtensionTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.openejb.cdi.concurrency;
+
+import jakarta.enterprise.concurrent.ContextService;
+import jakarta.enterprise.concurrent.ManagedExecutorDefinition;
+import jakarta.enterprise.concurrent.ManagedExecutorService;
+import jakarta.enterprise.concurrent.ManagedScheduledExecutorDefinition;
+import jakarta.enterprise.concurrent.ManagedScheduledExecutorService;
+import jakarta.enterprise.concurrent.ManagedThreadFactory;
+import jakarta.enterprise.concurrent.ManagedThreadFactoryDefinition;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Default;
+import jakarta.enterprise.util.Nonbinding;
+import jakarta.inject.Inject;
+import jakarta.inject.Qualifier;
+import org.apache.openejb.jee.EnterpriseBean;
+import org.apache.openejb.jee.SingletonBean;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verifies that the {@link ConcurrencyCDIExtension} correctly registers
+ * concurrency resources as CDI beans, both with default and custom qualifiers.
+ */
+@RunWith(ApplicationComposer.class)
+public class ConcurrencyCDIExtensionTest {
+
+ @Inject
+ private DefaultInjectionBean defaultBean;
+
+ @Inject
+ private QualifiedInjectionBean qualifiedBean;
+
+ @Module
+ public EnterpriseBean ejb() {
+ return new SingletonBean(DummyEjb.class).localBean();
+ }
+
+ @Module
+ public Class<?>[] beans() {
+ return new Class<?>[]{
+ DefaultInjectionBean.class,
+ QualifiedInjectionBean.class,
+ AppConfig.class
+ };
+ }
+
+ @Test
+ public void defaultManagedExecutorServiceIsInjectable() {
+ assertNotNull("Default ManagedExecutorService should be injectable via
@Inject",
+ defaultBean.getMes());
+ }
+
+ @Test
+ public void defaultManagedScheduledExecutorServiceIsInjectable() {
+ assertNotNull("Default ManagedScheduledExecutorService should be
injectable via @Inject",
+ defaultBean.getMses());
+ }
+
+ @Test
+ public void defaultManagedThreadFactoryIsInjectable() {
+ assertNotNull("Default ManagedThreadFactory should be injectable via
@Inject",
+ defaultBean.getMtf());
+ }
+
+ @Test
+ public void defaultContextServiceIsInjectable() {
+ assertNotNull("Default ContextService should be injectable via
@Inject",
+ defaultBean.getCs());
+ }
+
+ @Test
+ public void qualifiedManagedExecutorServiceIsInjectable() {
+ assertNotNull("Qualified ManagedExecutorService should be injectable
via @Inject @TestQualifier",
+ qualifiedBean.getMes());
+ }
+
+ @Test
+ public void qualifiedManagedExecutorServiceExecutesTask() throws Exception
{
+ final CountDownLatch latch = new CountDownLatch(1);
+ qualifiedBean.getMes().execute(latch::countDown);
+ assertTrue("Task should complete on qualified MES",
+ latch.await(5, TimeUnit.SECONDS));
+ }
+
+ // --- Dummy EJB to trigger full resource deployment ---
+
+ @jakarta.ejb.Singleton
+ public static class DummyEjb {
+ }
+
+ // --- Qualifier ---
+
+ @Qualifier
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE})
+ public @interface TestQualifier {
+ }
+
+ // --- App config with qualifier-enabled definition ---
+
+ @ManagedExecutorDefinition(
+ name = "java:comp/env/concurrent/TestQualifiedExecutor",
+ qualifiers = {TestQualifier.class}
+ )
+ @ApplicationScoped
+ public static class AppConfig {
+ }
+
+ // --- Bean that injects default concurrency resources ---
+
+ @ApplicationScoped
+ public static class DefaultInjectionBean {
+
+ @Inject
+ private ManagedExecutorService mes;
+
+ @Inject
+ private ManagedScheduledExecutorService mses;
+
+ @Inject
+ private ManagedThreadFactory mtf;
+
+ @Inject
+ private ContextService cs;
+
+ public ManagedExecutorService getMes() {
+ return mes;
+ }
+
+ public ManagedScheduledExecutorService getMses() {
+ return mses;
+ }
+
+ public ManagedThreadFactory getMtf() {
+ return mtf;
+ }
+
+ public ContextService getCs() {
+ return cs;
+ }
+ }
+
+ // --- Bean that injects qualified concurrency resources ---
+
+ @ApplicationScoped
+ public static class QualifiedInjectionBean {
+
+ @Inject
+ @TestQualifier
+ private ManagedExecutorService mes;
+
+ public ManagedExecutorService getMes() {
+ return mes;
+ }
+ }
+}