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;
+        }
+    }
+}


Reply via email to