diff --git a/CDI_5_BETA1_SUPPORT_PLAN.md b/CDI_5_BETA1_SUPPORT_PLAN.md
new file mode 100644
index 0000000..e2eb81b
--- /dev/null
+++ b/CDI_5_BETA1_SUPPORT_PLAN.md
@@ -0,0 +1,63 @@
+# CDI 5.0 Beta1 Support Draft PR
+
+## Summary
+
+Move ODI from CDI 4.1 to CDI 5.0 Beta1 and implement the CDI Lite-facing API/SPI deltas: `@Eager`, `@Reserve`, `@AutoClose`, async invoker handlers, `SyntheticInjections`, record lang-model support, and the new container/interface methods. CDI 5.0 lists these as new feature areas and also changes CDI API/TCK Maven GAVs.
+
+## Key Changes
+
+- Update dependency metadata to CDI 5:
+ - `cdi = "5.0.0.Beta1"` using the resolvable Maven Central version.
+ - Move API/TCK artifacts from `jakarta.enterprise:*` to `jakarta.cdi:*`.
+ - Update TCK runner jar names to `jakarta.cdi-tck-*` and signature extraction to `cdi-api-jdk17.sigfile`.
+ - Update README/docs dependency snippets, without overwriting the existing untracked `docs/` tree blindly.
+- Compile against CDI 5 APIs:
+ - Add `OdiSeContainerInitializer.addBuildCompatibleExtensions(...)`.
+ - Add `OdiBeanContainerImpl.unwrapClientProxy(T)` using `InterceptedProxy.interceptedTarget()` with a no-op fallback.
+ - Remove obsolete EL methods from the TCK `BeanManagerFactory` shim.
+- Add CDI 5 annotation support:
+ - Map `@Eager` to Micronaut `@Context`; validate that eager beans are `@ApplicationScoped`, including producer and stereotype cases.
+ - Map selected `@Reserve` beans to Micronaut `@Secondary`; disable unselected reserves without `@Priority`; update CDI resolution paths so non-reserve beans win over reserve beans, and validate `@Reserve + @Alternative` errors.
+ - Implement `@AutoClose` by marking `close()` as pre-destroy for class beans and by setting `@Bean(preDestroy = "close")` for producer/synthetic beans when the produced type is `AutoCloseable`; verify close exceptions are swallowed by the disposable path.
+- Add BCE/SPI support:
+ - Implement `BeanInfo.isReserve()`, `isEager()`, and `isAutoClose()` from direct and stereotype metadata.
+ - Implement `SyntheticBeanBuilder.reserve/eager/autoClose` and all `withInjectionPoint(...)` overloads.
+ - Add a runtime `SyntheticInjections` adapter that only resolves registered injection points, supports `Class` and `TypeLiteral`, handles qualifiers, exposes `InjectionPoint` where valid, and releases dependent objects with the synthetic bean or disposer invocation as required.
+ - Support both CDI 5 `SyntheticBeanCreator/Disposer` signatures and deprecated CDI 4.x signatures, selecting the directly implemented CDI 5 method first and failing clearly if both/neither are usable.
+- Add invoker and lang-model support:
+ - Implement `InvokerValidation.ensureAsyncHandlerExists(...)`.
+ - Add async handler discovery for `AsyncHandler.ReturnType` and `AsyncHandler.ParameterType`, plus built-ins for `CompletionStage`, `CompletableFuture`, `Flow.Publisher`, and soft optional `org.reactivestreams.Publisher`.
+ - Delay dependent-context destruction for async invokers until the matched handler invokes completion; still destroy immediately for synchronous exceptions.
+ - Add `RecordComponentInfoImpl`, real `ClassInfo.recordComponents()`, `ClassInfo.isSealed()`, and `ClassInfo.permittedSubclasses()` using Micronaut AST model data where available.
+ - Model record components from Micronaut `PropertyElement`; Java records are already exposed there, so ODI should not depend directly on `javax.lang.model.element.RecordComponentElement` or other APT-specific APIs for this.
+ - Upstream only the missing language-neutral sealed-class APIs to Core (`ClassElement.isSealed()` and `ClassElement.getPermittedSubclasses()`).
+- Known draft gap:
+ - `SeContainerInitializer.addBuildCompatibleExtensions(...)` cannot truly run compile-time BCE discovery after application classes are already compiled. The draft should implement the CDI 5 method but throw a clear unsupported exception, document the limitation, and leave full programmatic BCE registration as a follow-up build-time registration channel.
+
+## Test Plan
+
+Final verification target: the CDI Lite 5 TCK must pass without regressions. The compile, signature, unit, and targeted TCK runs below are checkpoints toward that target, not substitutes for it.
+
+- Local unit/compile checks:
+ - `./gradlew -Plocal.git.odi.micronaut-core=/Users/graemerocher/dev/micronaut/core.cdi :micronaut-odi-processor-cdi:test :micronaut-odi-cdi:test`
+ - `./gradlew -Plocal.git.odi.micronaut-core=/Users/graemerocher/dev/micronaut/core.cdi :micronaut-odi-tck-runner:cdiSignatureTest`
+- Targeted TCK smoke tests with `:micronaut-odi-tck-runner:singleTest`:
+ - `org.jboss.cdi.tck.tests.eager.bean.EagerBeanTest`
+ - `org.jboss.cdi.tck.tests.eager.producer.method.EagerProducerMethodTest`
+ - `org.jboss.cdi.tck.tests.reserve.basic.SelectedReserveTest`
+ - `org.jboss.cdi.tck.tests.reserve.selection.priority.ReservePriorityTest`
+ - `org.jboss.cdi.tck.tests.autoclose.bean.AutoCloseBeanTest`
+ - `org.jboss.cdi.tck.tests.autoclose.producer.method.AutoCloseProducerMethodTest`
+ - `org.jboss.cdi.tck.tests.build.compatible.extensions.syntheticBeanInjections.SyntheticInjectionsTest`
+ - `org.jboss.cdi.tck.tests.build.compatible.extensions.syntheticBeanInjectionsUnregistered.SyntheticInjectionsUnregisteredTest`
+ - `org.jboss.cdi.tck.tests.invokers.lookup.dependent.async.builtin.AsyncHandlerBuiltinTest`
+ - `org.jboss.cdi.tck.tests.invokers.lookup.dependent.async.returntype.AsyncHandlerReturnTypeTest`
+ - `org.jboss.cdi.tck.tests.invokers.lookup.dependent.async.paramtype.AsyncHandlerParamTypeTest`
+- Run `./gradlew -Plocal.git.odi.micronaut-core=/Users/graemerocher/dev/micronaut/core.cdi :micronaut-odi-tck-runner:fullTckTest` after targeted tests are green; treat CDI Full portable-extension-only failures as out of ODI Lite scope unless they overlap with Lite behavior.
+
+## Assumptions
+
+- Use the adjacent `/Users/graemerocher/dev/micronaut/core.cdi` checkout on branch `cdi-5.1.x`.
+- Until that Core change is merged into the include-git branch, pass `-Plocal.git.odi.micronaut-core=/Users/graemerocher/dev/micronaut/core.cdi`; otherwise ODI compiles against the cached include-git checkout and will not see the new language-neutral sealed-class APIs.
+- No Micronaut Core change is expected for `@Eager`, `@Reserve`, or `@AutoClose`; only add core support if sealed/permitted class metadata is not available through current AST/native element APIs.
+- The first PR targets CDI 5 Beta1 compatibility and TCK progress, not final Jakarta EE 12 certification metadata.
diff --git a/README.md b/README.md
index 41f6337..75df5c3 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ dependencies {
annotationProcessor("org.eclipse.odi:micronaut-odi-processor-cdi:")
implementation("org.eclipse.odi:micronaut-odi-cdi:")
- implementation("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0")
+ implementation("jakarta.cdi:jakarta.cdi-api:5.0.0.Beta1")
}
```
diff --git a/cdi/build.gradle.kts b/cdi/build.gradle.kts
index 2b19775..95de9db 100644
--- a/cdi/build.gradle.kts
+++ b/cdi/build.gradle.kts
@@ -11,6 +11,8 @@ dependencies {
api(project(":micronaut-odi-core"))
api(libs.cdi.api)
+ compileOnly(mn.micronaut.core.reactive)
+
testAnnotationProcessor(project(":micronaut-odi-processor-cdi"))
testImplementation(libs.javax.annotation.api)
diff --git a/cdi/src/main/java/org/eclipse/odi/cdi/OdiApplicationContextConfigurer.java b/cdi/src/main/java/org/eclipse/odi/cdi/OdiApplicationContextConfigurer.java
index d401d44..153143e 100644
--- a/cdi/src/main/java/org/eclipse/odi/cdi/OdiApplicationContextConfigurer.java
+++ b/cdi/src/main/java/org/eclipse/odi/cdi/OdiApplicationContextConfigurer.java
@@ -20,17 +20,28 @@
import io.micronaut.context.BeanRegistration;
import io.micronaut.context.BeanResolutionContext;
import io.micronaut.context.BeanResolutionCustomizer;
+import io.micronaut.context.Qualifier;
import io.micronaut.context.annotation.ContextConfigurer;
+import io.micronaut.core.annotation.Order;
+import io.micronaut.core.order.Ordered;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.QualifiedBeanType;
+import jakarta.annotation.Priority;
import jakarta.enterprise.context.NormalScope;
+import jakarta.enterprise.inject.Alternative;
+import jakarta.enterprise.inject.Reserve;
import jakarta.enterprise.inject.TransientReference;
import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
import java.util.Optional;
+import java.util.OptionalInt;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* ODI specific {@link ApplicationContextConfigurer}.
@@ -94,9 +105,95 @@ public boolean isCandidateBean(Argument> beanType, QualifiedBeanType> candid
}
return candidate.isCandidateBean(beanType);
}
+
+ @Override
+ public Optional> resolveNonUniqueBean(Argument beanType,
+ Qualifier qualifier,
+ Collection> candidates) {
+ return resolveCdiBean(qualifier, candidates);
+ }
});
}
+ private static Optional> resolveCdiBean(Qualifier qualifier,
+ Collection> beanDefinitions) {
+ if (beanDefinitions.isEmpty() || beanDefinitions.size() == 1) {
+ return Optional.empty();
+ }
+ if (isDefaultQualifier(qualifier)) {
+ List> defaultBeans = beanDefinitions
+ .stream()
+ .filter(DefaultQualifier::hasDefaultQualifier)
+ .collect(Collectors.toList());
+ if (!defaultBeans.isEmpty() && defaultBeans.size() < beanDefinitions.size()) {
+ if (defaultBeans.size() == 1) {
+ return Optional.of(defaultBeans.iterator().next());
+ }
+ beanDefinitions = defaultBeans;
+ }
+ }
+ List> alternatives = beanDefinitions
+ .stream()
+ .filter(bd -> bd.hasStereotype(Alternative.class))
+ .filter(bd -> getPriority(bd) > 0)
+ .collect(Collectors.toList());
+ if (!alternatives.isEmpty()) {
+ return highestUniquePriority(alternatives);
+ }
+ List> nonReserve = beanDefinitions
+ .stream()
+ .filter(bd -> !isReserve(bd))
+ .collect(Collectors.toList());
+ if (!nonReserve.isEmpty() && nonReserve.size() < beanDefinitions.size()) {
+ if (nonReserve.size() == 1) {
+ return Optional.of(nonReserve.iterator().next());
+ }
+ return Optional.empty();
+ }
+ if (beanDefinitions.stream().allMatch(OdiApplicationContextConfigurer::isReserve)) {
+ return highestUniquePriority(beanDefinitions);
+ }
+ return Optional.empty();
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static boolean isDefaultQualifier(Qualifier qualifier) {
+ return qualifier == null || DefaultQualifier.instance().contains((Qualifier) qualifier);
+ }
+
+ private static Optional> highestUniquePriority(Collection> beanDefinitions) {
+ List> sorted = beanDefinitions.stream()
+ .filter(beanDefinition -> getPriority(beanDefinition) > 0)
+ .sorted(Comparator.>comparingInt(OdiApplicationContextConfigurer::getPriority).reversed())
+ .collect(Collectors.toList());
+ if (sorted.isEmpty()) {
+ return Optional.empty();
+ }
+ if (sorted.size() == 1 || getPriority(sorted.get(0)) != getPriority(sorted.get(1))) {
+ return Optional.of(sorted.get(0));
+ }
+ return Optional.empty();
+ }
+
+ private static boolean isReserve(BeanDefinition> beanDefinition) {
+ return beanDefinition.hasDeclaredAnnotation(Reserve.class) || beanDefinition.hasDeclaredStereotype(Reserve.class);
+ }
+
+ private static int getPriority(BeanDefinition> beanDefinition) {
+ OptionalInt priority = beanDefinition.intValue(Priority.class);
+ if (priority.isPresent()) {
+ return priority.getAsInt();
+ }
+ int order = beanDefinition.intValue(Order.class).orElse(0);
+ if (order == 0) {
+ return 0;
+ }
+ if (order == Ordered.HIGHEST_PRECEDENCE) {
+ return Integer.MAX_VALUE;
+ }
+ return -order;
+ }
+
private static Object primitiveDefaultValue(Class> type) {
return switch (type.getName()) {
case "boolean" -> false;
diff --git a/cdi/src/main/java/org/eclipse/odi/cdi/OdiBeanContainerImpl.java b/cdi/src/main/java/org/eclipse/odi/cdi/OdiBeanContainerImpl.java
index d67e176..144eb3d 100644
--- a/cdi/src/main/java/org/eclipse/odi/cdi/OdiBeanContainerImpl.java
+++ b/cdi/src/main/java/org/eclipse/odi/cdi/OdiBeanContainerImpl.java
@@ -23,7 +23,6 @@
import io.micronaut.context.DefaultBeanResolutionContext;
import io.micronaut.context.Qualifier;
import io.micronaut.core.annotation.AnnotationMetadata;
-import io.micronaut.core.annotation.AnnotationMetadataProvider;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Order;
@@ -45,6 +44,7 @@
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.Reserve;
import jakarta.enterprise.inject.UnsatisfiedResolutionException;
import jakarta.enterprise.inject.UnproxyableResolutionException;
import jakarta.enterprise.inject.spi.Bean;
@@ -54,7 +54,6 @@
import jakarta.enterprise.inject.spi.Prioritized;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
-import org.eclipse.odi.cdi.annotation.ObservesMethod;
import org.eclipse.odi.cdi.annotation.reflect.AnnotationReflection;
import org.eclipse.odi.cdi.context.DependentContext;
import org.eclipse.odi.cdi.context.SingletonContext;
@@ -82,8 +81,7 @@
import java.util.stream.Collectors;
final class OdiBeanContainerImpl implements OdiBeanContainer {
- private static final String JAKARTA_INTERCEPTOR_BINDING = "jakarta.interceptor.InterceptorBinding";
- private static final String MICRONAUT_INTERCEPTOR_BINDING = "io.micronaut.aop.InterceptorBinding";
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final ApplicationContext applicationContext;
private final OdiSeContainer container;
@@ -91,6 +89,7 @@ final class OdiBeanContainerImpl implements OdiBeanContainer {
private final OdiAnnotations odiAnnotations;
private OdiObserverMethodRegistry observerMethodRegistry;
private Event
*/
-public final class PaymentCatalog {
+public class PaymentCatalog {
private final Set gateways;
+ protected PaymentCatalog() {
+ this.gateways = Collections.emptySet();
+ }
+
private PaymentCatalog(Set gateways) {
this.gateways = Collections.unmodifiableSet(new LinkedHashSet<>(gateways));
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1142b2e..13dac80 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,7 +4,7 @@ micronaut-docs = "2.0.0"
groovy = "5.0.4"
-cdi = "4.1.0"
+cdi = "5.0.0.Beta1"
javax-annotation = "1.3.2"
# Testing
@@ -17,13 +17,13 @@ jupiter = "5.12.2"
[libraries]
-cdi-api = { module = 'jakarta.enterprise:jakarta.enterprise.cdi-api', version.ref = "cdi" }
-cdi-lang-model = { module = 'jakarta.enterprise:jakarta.enterprise.lang-model', version.ref = "cdi" }
+cdi-api = { module = 'jakarta.cdi:jakarta.cdi-api', version.ref = "cdi" }
+cdi-lang-model = { module = 'jakarta.cdi:jakarta.cdi-lang-model-api', version.ref = "cdi" }
javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation" }
-cdi-tck-api = { module = 'jakarta.enterprise:cdi-tck-api', version.ref = "cdi" }
-cdi-tck-impl = { module = 'jakarta.enterprise:cdi-tck-core-impl', version.ref = "cdi" }
-cdi-tck-lang-model = { module = 'jakarta.enterprise:cdi-tck-lang-model', version.ref = "cdi" }
+cdi-tck-api = { module = 'jakarta.cdi:jakarta.cdi-tck-api', version.ref = "cdi" }
+cdi-tck-impl = { module = 'jakarta.cdi:jakarta.cdi-tck-core-impl', version.ref = "cdi" }
+cdi-tck-lang-model = { module = 'jakarta.cdi:jakarta.cdi-tck-lang-model', version.ref = "cdi" }
# Testing
diff --git a/gradle/mn.libs.versions.toml b/gradle/mn.libs.versions.toml
index 9e1764d..349a669 100644
--- a/gradle/mn.libs.versions.toml
+++ b/gradle/mn.libs.versions.toml
@@ -7,6 +7,7 @@ slf4j = "2.0.17"
[libraries]
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
micronaut-context = { module = "io.micronaut:micronaut-context", version.ref = "micronaut" }
+micronaut-core-reactive = { module = "io.micronaut:micronaut-core-reactive", version.ref = "micronaut" }
micronaut-inject = { module = "io.micronaut:micronaut-inject", version.ref = "micronaut" }
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut" }
micronaut-inject-java-test = { module = "io.micronaut:micronaut-inject-java-test", version.ref = "micronaut" }
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/CdiUtil.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/CdiUtil.java
index e65bdc4..9732753 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/CdiUtil.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/CdiUtil.java
@@ -49,6 +49,7 @@
import jakarta.enterprise.inject.Intercepted;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
+import jakarta.enterprise.inject.Reserve;
import jakarta.enterprise.inject.Stereotype;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.InjectionPoint;
@@ -182,7 +183,7 @@ public static void visitPriority(VisitorContext context, ClassElement element) {
}
}
- private static OptionalInt resolvePriority(VisitorContext context, ClassElement element) {
+ public static OptionalInt resolvePriority(VisitorContext context, Element element) {
OptionalInt directPriority = declaredIntValue(element.getAnnotationMetadata(), Priority.class);
if (directPriority.isPresent()) {
return directPriority;
@@ -744,13 +745,38 @@ && validateInterceptorMetadataInjectionPoint(context, classElement, owningElemen
context.fail("jakarta.enterprise.inject.Instance must have a required type parameter specified", owningElement);
return true;
}
+ if (classElement.getName().equals(Instance.class.getName()) && hasWildcardInstanceRequiredType(classElement)) {
+ context.fail("jakarta.enterprise.inject.Instance required type must not contain wildcard type parameters", owningElement);
+ return true;
+ }
if (classElement.getName().equals(Event.class.getName()) && isNoGenericType(classElement)) {
context.fail("jakarta.enterprise.event.Event must have a required type parameter specified", owningElement);
return true;
}
+ if (classElement.getName().equals(Event.class.getName()) && hasWildcardRequiredType(classElement)) {
+ context.fail("jakarta.enterprise.event.Event required type must not contain wildcard type parameters", owningElement);
+ return true;
+ }
return false;
}
+ private static boolean hasWildcardRequiredType(ClassElement classElement) {
+ List typeArguments = resolvedTypeArguments(classElement);
+ if (typeArguments.isEmpty()) {
+ return false;
+ }
+ ClassElement requiredType = typeArguments.get(0);
+ return requiredType instanceof WildcardElement || requiredType.isWildcard();
+ }
+
+ private static boolean hasWildcardInstanceRequiredType(ClassElement classElement) {
+ List typeArguments = resolvedTypeArguments(classElement);
+ if (typeArguments.isEmpty()) {
+ return false;
+ }
+ return containsWildcard(typeArguments.get(0));
+ }
+
private static boolean validateBeanMetadataTypeParameter(VisitorContext context,
ClassElement beanMetadataType,
Element owningElement) {
@@ -1147,6 +1173,13 @@ private static boolean validateResolvableInjectionPoint(VisitorContext context,
boolean declaredAnyWithoutDefault) {
Set configuredBeanClasses = configuredBeanClasses(context);
boolean exhaustiveBeanClasses = !configuredBeanClasses.isEmpty();
+ if (!exhaustiveBeanClasses
+ && !isRawGenericType(injectPointType)
+ && hasDisabledProducerCandidate(context, injectPointType, injectPoint, configuredBeanClasses)) {
+ context.fail(DEPLOYMENT_EXCEPTION_MARKER
+ + "Unsatisfied dependency for injection point of type " + injectPointType.getName(), injectPoint);
+ return true;
+ }
if (isBuildCompatibleExtensionDeployment(context)
|| isBuiltInInjectionPointType(injectPointType)
|| injectPointType.isPrimitive()
@@ -1163,11 +1196,20 @@ private static boolean validateResolvableInjectionPoint(VisitorContext context,
addManagedBeanCandidate(context, injectPointType, injectPoint, candidate, candidates);
addProducerCandidates(context, injectPointType, injectPoint, candidate, candidates);
}
- if (candidates.isEmpty() && exhaustiveBeanClasses) {
+ boolean disabledProducerCandidate = hasDisabledProducerCandidate(context, injectPointType, injectPoint, configuredBeanClasses);
+ if ((candidates.isEmpty() || isOnlyImplicitProducedTypeCandidate(injectPointType, candidates, disabledProducerCandidate))
+ && (exhaustiveBeanClasses || disabledProducerCandidate)) {
context.fail(DEPLOYMENT_EXCEPTION_MARKER
+ "Unsatisfied dependency for injection point of type " + injectPointType.getName(), injectPoint);
return true;
}
+ Set nonReserveCandidates = candidates.stream()
+ .filter(candidate -> !candidate.reserve)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (!nonReserveCandidates.isEmpty() && nonReserveCandidates.size() < candidates.size()) {
+ candidates = nonReserveCandidates;
+ }
+ candidates = selectPriorityCandidates(context, injectPointType, injectPoint, candidates);
if (candidates.size() == 1 && candidates.iterator().next().unproxyable) {
context.fail(DEPLOYMENT_EXCEPTION_MARKER
+ "Unproxyable dependency for injection point of type " + injectPointType.getName(), injectPoint);
@@ -1188,6 +1230,16 @@ private static boolean validateResolvableInjectionPoint(VisitorContext context,
return false;
}
+ private static boolean isOnlyImplicitProducedTypeCandidate(ClassElement injectPointType,
+ Set candidates,
+ boolean disabledProducerCandidate) {
+ if (!disabledProducerCandidate || candidates.size() != 1) {
+ return false;
+ }
+ ResolvableCandidate candidate = candidates.iterator().next();
+ return !candidate.producer && candidate.description.equals(injectPointType.getName());
+ }
+
private static void addManagedBeanCandidate(VisitorContext context,
ClassElement injectPointType,
TypedElement injectPoint,
@@ -1201,7 +1253,10 @@ && hasBeanTypeAssignableToRequiredType(injectPointType, candidate)) {
candidate.getName(),
false,
requiresRuntimeResolution(candidate),
- isUnproxyableNormalScopedBean(candidate)));
+ isUnproxyableNormalScopedBean(candidate),
+ isReserve(candidate),
+ isAlternative(candidate),
+ resolvePriority(context, candidate).orElse(0)));
}
}
@@ -1245,17 +1300,64 @@ private static void addProducerCandidate(VisitorContext context,
MemberElement producer,
Set candidates) {
if (matchesRequiredQualifiers(context, injectPoint, producer)
- && isBeanEnabled(context, producer)
+ && isProducerEnabled(context, producer)
&& hasBeanTypeAssignableToRequiredType(injectPointType, producedType)) {
candidates.add(new ResolvableCandidate(
producer.getDeclaringType().getName() + "." + producer.getName(),
true,
requiresRuntimeResolution(producer) || requiresRuntimeResolution(producer.getDeclaringType()),
- false)
+ false,
+ isReserve(producer),
+ isAlternative(producer) || isAlternative(producer.getDeclaringType()),
+ resolvePriority(context, producer).orElseGet(() -> resolvePriority(context, producer.getDeclaringType()).orElse(0)))
);
}
}
+ private static Set selectPriorityCandidates(VisitorContext context,
+ ClassElement injectPointType,
+ TypedElement injectPoint,
+ Set candidates) {
+ Set alternatives = candidates.stream()
+ .filter(candidate -> candidate.alternative)
+ .filter(candidate -> candidate.priority > 0)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (!alternatives.isEmpty()) {
+ return selectHighestPriorityCandidates(context, injectPointType, injectPoint, alternatives);
+ }
+ if (candidates.stream().allMatch(candidate -> candidate.reserve)) {
+ return selectHighestPriorityCandidates(context, injectPointType, injectPoint, candidates.stream()
+ .filter(candidate -> candidate.priority > 0)
+ .collect(Collectors.toCollection(LinkedHashSet::new)));
+ }
+ return candidates;
+ }
+
+ private static Set selectHighestPriorityCandidates(VisitorContext context,
+ ClassElement injectPointType,
+ TypedElement injectPoint,
+ Set candidates) {
+ if (candidates.isEmpty()) {
+ return candidates;
+ }
+ int highestPriority = candidates.stream()
+ .mapToInt(candidate -> candidate.priority)
+ .max()
+ .orElse(0);
+ Set highestPriorityCandidates = candidates.stream()
+ .filter(candidate -> candidate.priority == highestPriority)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (highestPriorityCandidates.size() > 1) {
+ context.fail(DEPLOYMENT_EXCEPTION_MARKER
+ + "Ambiguous dependency for injection point of type " + injectPointType.getName()
+ + ". Candidate beans: " + highestPriorityCandidates.stream()
+ .map(ResolvableCandidate::description)
+ .sorted()
+ .collect(Collectors.joining(", ")), injectPoint);
+ }
+ return highestPriorityCandidates;
+ }
+
private static Collection candidateBeanClasses(VisitorContext context,
ClassElement injectPointType,
TypedElement injectPoint,
@@ -1345,12 +1447,81 @@ private static boolean isProducerDeclaringBeanClass(VisitorContext context, Clas
return !candidate.hasStereotype(Interceptor.class)
&& !candidate.hasAnnotation(io.micronaut.core.annotation.Vetoed.class)
&& !candidate.hasAnnotation(jakarta.enterprise.inject.Vetoed.class)
- && org.eclipse.odi.cdi.processor.AnnotationUtil.hasBeanDefiningAnnotation(candidate)
- && isBeanEnabled(context, candidate)
+ && hasResolvableBeanDefiningAnnotation(candidate)
&& isBeanClass(candidate);
}
- private static boolean isBeanEnabled(VisitorContext context, Element element) {
+ private static boolean hasResolvableBeanDefiningAnnotation(ClassElement candidate) {
+ return candidate.hasAnnotation(org.eclipse.odi.cdi.processor.AnnotationUtil.ANN_ODI_BEAN_DEFINITION)
+ || hasDeclaredBeanDefiningAnnotation(candidate);
+ }
+
+ private static boolean hasDeclaredBeanDefiningAnnotation(ClassElement candidate) {
+ return candidate.hasDeclaredAnnotation(Factory.class)
+ || candidate.hasDeclaredAnnotation(jakarta.enterprise.context.Dependent.class)
+ || candidate.hasDeclaredStereotype(jakarta.enterprise.context.NormalScope.class)
+ || candidate.hasDeclaredStereotype(Stereotype.class)
+ || candidate.hasDeclaredStereotype(Interceptor.class)
+ || candidate.hasDeclaredStereotype(io.micronaut.context.annotation.Bean.class)
+ || candidate.hasDeclaredStereotype(io.micronaut.core.annotation.AnnotationUtil.SCOPE);
+ }
+
+ private static boolean isProducerEnabled(VisitorContext context, MemberElement producer) {
+ return isBeanEnabled(context, producer)
+ && (isBeanEnabled(context, producer.getDeclaringType()) || hasPriority(producer));
+ }
+
+ private static boolean hasDisabledProducerCandidate(VisitorContext context,
+ ClassElement injectPointType,
+ TypedElement injectPoint,
+ Set configuredBeanClasses) {
+ for (ClassElement candidate : candidateBeanClasses(context, injectPointType, injectPoint, configuredBeanClasses)) {
+ if (!isProducerDeclaringBeanClass(context, candidate)) {
+ continue;
+ }
+ if (hasDisabledProducerCandidate(context, injectPointType, injectPoint, candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasDisabledProducerCandidate(VisitorContext context,
+ ClassElement injectPointType,
+ TypedElement injectPoint,
+ ClassElement candidate) {
+ for (MethodElement method : candidate.getEnclosedElements(ElementQuery.ALL_METHODS
+ .onlyDeclared()
+ .onlyConcrete()
+ .annotated(annotationMetadata -> annotationMetadata.hasDeclaredAnnotation(Produces.class)))) {
+ if (matchesDisabledProducer(context, injectPointType, injectPoint, method.getGenericReturnType(), method)) {
+ return true;
+ }
+ }
+ for (FieldElement field : candidate.getEnclosedElements(ElementQuery.ALL_FIELDS
+ .onlyDeclared()
+ .annotated(annotationMetadata -> annotationMetadata.hasDeclaredAnnotation(Produces.class)))) {
+ if (matchesDisabledProducer(context, injectPointType, injectPoint, field.getGenericField(), field)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean matchesDisabledProducer(VisitorContext context,
+ ClassElement injectPointType,
+ TypedElement injectPoint,
+ ClassElement producedType,
+ MemberElement producer) {
+ return matchesRequiredQualifiers(context, injectPoint, producer)
+ && !isProducerEnabled(context, producer)
+ && hasBeanTypeAssignableToRequiredType(injectPointType, producedType);
+ }
+
+ public static boolean isBeanEnabled(VisitorContext context, Element element) {
+ if (isReserve(element) && !hasPriority(element)) {
+ return false;
+ }
if (!isAlternative(element)) {
return true;
}
@@ -1378,15 +1549,22 @@ private static boolean isBeanEnabled(VisitorContext context, Element element) {
return false;
}
- private static boolean isAlternative(Element element) {
+ public static boolean isAlternative(Element element) {
return element.hasStereotype(jakarta.enterprise.inject.Alternative.class)
|| element.hasAnnotation(jakarta.enterprise.inject.Alternative.class);
}
- private static boolean hasPriority(Element element) {
+ public static boolean hasPriority(Element element) {
return element.hasStereotype(Priority.class) || element.hasAnnotation(Priority.class);
}
+ public static boolean isReserve(Element element) {
+ if (element instanceof MemberElement) {
+ return element.hasDeclaredStereotype(Reserve.class) || element.hasDeclaredAnnotation(Reserve.class);
+ }
+ return element.hasStereotype(Reserve.class) || element.hasAnnotation(Reserve.class);
+ }
+
private static String selectedAlternativeBeanClassName(Element element) {
if (element instanceof ClassElement) {
return ((ClassElement) element).getName();
@@ -1398,7 +1576,7 @@ private static String selectedAlternativeBeanClassName(Element element) {
}
private static boolean requiresRuntimeResolution(Element element) {
- return isAlternative(element) || hasPriority(element);
+ return isAlternative(element) || hasPriority(element) || isReserve(element);
}
private static boolean matchesRequiredQualifiers(VisitorContext context, Element injectPoint, Element candidate) {
@@ -1484,7 +1662,7 @@ private static boolean isResolvableBeanClass(VisitorContext context, ClassElemen
&& !candidate.hasStereotype(Interceptor.class)
&& !candidate.hasAnnotation(io.micronaut.core.annotation.Vetoed.class)
&& !candidate.hasAnnotation(jakarta.enterprise.inject.Vetoed.class)
- && org.eclipse.odi.cdi.processor.AnnotationUtil.hasBeanDefiningAnnotation(candidate)
+ && hasResolvableBeanDefiningAnnotation(candidate)
&& isBeanClass(candidate);
}
@@ -1526,12 +1704,24 @@ private static final class ResolvableCandidate {
private final boolean producer;
private final boolean runtimeResolution;
private final boolean unproxyable;
-
- private ResolvableCandidate(String description, boolean producer, boolean runtimeResolution, boolean unproxyable) {
+ private final boolean reserve;
+ private final boolean alternative;
+ private final int priority;
+
+ private ResolvableCandidate(String description,
+ boolean producer,
+ boolean runtimeResolution,
+ boolean unproxyable,
+ boolean reserve,
+ boolean alternative,
+ int priority) {
this.description = description;
this.producer = producer;
this.runtimeResolution = runtimeResolution;
this.unproxyable = unproxyable;
+ this.reserve = reserve;
+ this.alternative = alternative;
+ this.priority = priority;
}
String description() {
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/AbstractSyntheticBuilder.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/AbstractSyntheticBuilder.java
index 0cdaf7c..2f081d8 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/AbstractSyntheticBuilder.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/AbstractSyntheticBuilder.java
@@ -99,7 +99,7 @@ protected Object withParam(String key, boolean value) {
}
protected Object withParam(String key, boolean[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -109,7 +109,7 @@ protected Object withParam(String key, int value) {
}
protected Object withParam(String key, int[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -119,7 +119,7 @@ protected Object withParam(String key, long value) {
}
protected Object withParam(String key, long[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -129,7 +129,7 @@ protected Object withParam(String key, double value) {
}
protected Object withParam(String key, double[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -139,7 +139,7 @@ protected Object withParam(String key, String value) {
}
protected Object withParam(String key, String[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -149,7 +149,7 @@ protected Object withParam(String key, Enum> value) {
}
protected Object withParam(String key, Enum>[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -164,12 +164,12 @@ protected Object withParam(String key, ClassInfo value) {
}
protected Object withParam(String key, Class>[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
protected Object withParam(String key, ClassInfo[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -186,12 +186,12 @@ protected Object withParam(String key, Annotation value) {
}
protected Object withParam(String key, AnnotationInfo[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
protected Object withParam(String key, Annotation[] value) {
- addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(value).build());
+ addAnnotation(AnnotationBuilder.of(Property.class).member("name", key).value(copyArray(value)).build());
return this;
}
@@ -201,10 +201,30 @@ protected Object withParam(String key, InvokerInfo value) {
}
protected Object withParam(String key, InvokerInfo[] value) {
- params.put(key, value);
+ params.put(key, copyArray(value));
return this;
}
+ @SuppressWarnings("unchecked")
+ private static T copyArray(T value) {
+ if (value instanceof boolean[]) {
+ return (T) ((boolean[]) value).clone();
+ }
+ if (value instanceof int[]) {
+ return (T) ((int[]) value).clone();
+ }
+ if (value instanceof long[]) {
+ return (T) ((long[]) value).clone();
+ }
+ if (value instanceof double[]) {
+ return (T) ((double[]) value).clone();
+ }
+ if (value instanceof Object[]) {
+ return (T) ((Object[]) value).clone();
+ }
+ return value;
+ }
+
private static AnnotationMetadata cloneMetadata(AnnotationMetadata annotationMetadata) {
if (annotationMetadata instanceof MutableAnnotationMetadata) {
return ((MutableAnnotationMetadata) annotationMetadata).clone();
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BeanInfoImpl.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BeanInfoImpl.java
index fc395ea..4cdef1f 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BeanInfoImpl.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BeanInfoImpl.java
@@ -28,9 +28,12 @@
import io.micronaut.inject.ast.beans.BeanElementBuilder;
import io.micronaut.inject.visitor.VisitorContext;
import jakarta.enterprise.context.NormalScope;
+import jakarta.enterprise.context.AutoClose;
+import jakarta.enterprise.context.Eager;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;
import jakarta.enterprise.inject.Alternative;
+import jakarta.enterprise.inject.Reserve;
import jakarta.enterprise.inject.build.compatible.spi.BeanInfo;
import jakarta.enterprise.inject.build.compatible.spi.DisposerInfo;
import jakarta.enterprise.inject.build.compatible.spi.InjectionPointInfo;
@@ -205,6 +208,11 @@ public boolean isAlternative() {
return beanElement.hasDeclaredAnnotation(Alternative.class);
}
+ @Override
+ public boolean isReserve() {
+ return beanElement.hasDeclaredAnnotation(Reserve.class) || beanElement.hasDeclaredStereotype(Reserve.class);
+ }
+
@Override
public Integer priority() {
final OptionalInt i = beanElement.intValue(Order.class);
@@ -214,6 +222,16 @@ public Integer priority() {
return null;
}
+ @Override
+ public boolean isEager() {
+ return beanElement.hasAnnotation(Eager.class) || beanElement.hasStereotype(Eager.class);
+ }
+
+ @Override
+ public boolean isAutoClose() {
+ return beanElement.hasAnnotation(AutoClose.class) || beanElement.hasStereotype(AutoClose.class);
+ }
+
@Override
public String name() {
return beanElement.stringValue(AnnotationUtil.NAMED).orElse(null);
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionBeanVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionBeanVisitor.java
index 4e1fb1e..56e5991 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionBeanVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionBeanVisitor.java
@@ -15,7 +15,9 @@
*/
package org.eclipse.odi.cdi.processor.extensions;
+import io.micronaut.aop.Around;
import io.micronaut.context.annotation.Primary;
+import io.micronaut.context.annotation.Bean;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.util.CollectionUtils;
@@ -23,23 +25,31 @@
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.MethodElement;
+import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.beans.BeanElement;
import io.micronaut.inject.ast.beans.BeanElementBuilder;
import io.micronaut.inject.ast.beans.BeanMethodElement;
import io.micronaut.inject.visitor.BeanElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.runtime.context.scope.ScopedProxy;
+import jakarta.enterprise.context.AutoClose;
+import jakarta.enterprise.context.NormalScope;
+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.build.compatible.spi.SyntheticBeanDisposer;
+import jakarta.enterprise.inject.build.compatible.spi.SyntheticInjections;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticObserver;
import jakarta.enterprise.inject.spi.EventContext;
import jakarta.enterprise.util.Nonbinding;
import jakarta.inject.Singleton;
+import org.eclipse.odi.cdi.OdiSyntheticInjectionPoint;
import org.eclipse.odi.cdi.annotation.meta.RuntimeMetaAnnotation;
import org.eclipse.odi.cdi.OdiSyntheticParameters;
import org.eclipse.odi.cdi.processor.CdiUtil;
import java.lang.annotation.Annotation;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -80,6 +90,9 @@ public void finish(VisitorContext visitorContext) {
final List> syntheticBeanBuilders =
syntheticComponents.getSyntheticBeanBuilders();
for (SyntheticBeanBuilderImpl> syntheticBeanBuilder : syntheticBeanBuilders) {
+ registry.validateSyntheticInjectionPoints(visitorContext, syntheticBeanBuilder);
+ syntheticBeanBuilder.getParams().put(OdiSyntheticParameters.BEAN_TYPE, syntheticBeanBuilder.getBeanType().getName());
+ registerSyntheticInjectionPoints(syntheticBeanBuilder);
registerSyntheticParameters(syntheticBeanBuilder);
final ClassElement beanType = syntheticBeanBuilder.getBeanType();
final Class extends SyntheticBeanCreator>> creatorClass = syntheticBeanBuilder.getCreatorClass();
@@ -170,11 +183,22 @@ private void defineSyntheticDisposer(VisitorContext visitorContext, SyntheticBea
syntheticBeanBuilder.getAnnotationMetadata(),
disposerBuilder
);
- ElementQuery disposeMethod =
- ElementQuery.ALL_METHODS
- .onlyInstance()
- .named(n -> n.equals("dispose"))
- .filter(m -> m.getParameters().length == 3);
+ String disposeInjectionType = selectSyntheticMethodInjectionType(
+ visitorContext,
+ disposerElement,
+ "dispose",
+ 1,
+ 3
+ );
+ if (disposeInjectionType == null) {
+ return;
+ }
+ ElementQuery disposeMethod = syntheticMethodQuery(
+ "dispose",
+ 1,
+ 3,
+ disposeInjectionType
+ );
disposerBuilder.withMethods(disposeMethod, BeanMethodElement::executable);
}
}
@@ -205,9 +229,22 @@ private void addSyntheticAnnotations(SyntheticComponentsImpl syntheticComponents
private void defineSyntheticCreator(VisitorContext visitorContext, SyntheticBeanBuilderImpl> syntheticBeanBuilder, ClassElement beanType, ClassElement creatorElement) {
MutableAnnotationMetadata syntheticBeanMetadata = syntheticBeanBuilder.getAnnotationMetadata();
- final ElementQuery creatorMethods = ElementQuery.ALL_METHODS
- .named(name -> name.equals("create"))
- .filter(method -> method.getParameters().length == 2);
+ String createInjectionType = selectSyntheticMethodInjectionType(
+ visitorContext,
+ creatorElement,
+ "create",
+ 0,
+ 2
+ );
+ if (createInjectionType == null) {
+ return;
+ }
+ final ElementQuery creatorMethods = syntheticMethodQuery(
+ "create",
+ 0,
+ 2,
+ createInjectionType
+ );
BeanElementBuilder beanFactory = applicationClassElement.addAssociatedBean(creatorElement, visitorContext)
@@ -217,6 +254,17 @@ private void defineSyntheticCreator(VisitorContext visitorContext, SyntheticBean
builder.typed(beanType);
copySyntheticAnnotationMetadata(visitorContext, syntheticBeanMetadata, builder);
+ if (isNormalScopedSyntheticBean(visitorContext, syntheticBeanMetadata)) {
+ builder.intercept(AnnotationValue.builder(Around.class)
+ .member("proxyTarget", true)
+ .member("lazy", true)
+ .build());
+ builder.annotate(ScopedProxy.class);
+ builder.annotate(io.micronaut.core.annotation.AnnotationUtil.SCOPE);
+ }
+ if (syntheticBeanMetadata.hasAnnotation(AutoClose.class) && beanType.isAssignable(AutoCloseable.class)) {
+ builder.annotate(Bean.class, annotation -> annotation.member("preDestroy", "close"));
+ }
final Set exposedTypes = syntheticBeanBuilder
.getExposedTypes();
if (!exposedTypes.isEmpty()) {
@@ -228,6 +276,16 @@ private void defineSyntheticCreator(VisitorContext visitorContext, SyntheticBean
copyQualifiersToFactory(syntheticBeanMetadata, beanFactory);
}
+ private boolean isNormalScopedSyntheticBean(VisitorContext visitorContext, MutableAnnotationMetadata syntheticBeanMetadata) {
+ if (!syntheticBeanMetadata.getAnnotationNamesByStereotype(NormalScope.class).isEmpty()) {
+ return true;
+ }
+ return syntheticBeanMetadata.getAnnotationNames()
+ .stream()
+ .flatMap(annotationName -> visitorContext.getClassElement(annotationName).stream())
+ .anyMatch(annotationType -> annotationType.hasAnnotation(NormalScope.class) || annotationType.hasStereotype(NormalScope.class));
+ }
+
private void copyQualifiersToFactory(MutableAnnotationMetadata syntheticBeanMetadata, BeanElementBuilder beanFactory) {
List> qualifiers = syntheticBeanMetadata
.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER)
@@ -252,6 +310,77 @@ private void registerSyntheticParameters(AbstractSyntheticBuilder syntheticBuild
}
}
+ private void registerSyntheticInjectionPoints(SyntheticBeanBuilderImpl> syntheticBeanBuilder) {
+ List injectionPoints = syntheticBeanBuilder.getInjectionPoints();
+ if (injectionPoints.isEmpty()) {
+ return;
+ }
+ List descriptors = new ArrayList<>(injectionPoints.size());
+ for (SyntheticBeanBuilderImpl.SyntheticInjectionPoint injectionPoint : injectionPoints) {
+ List qualifierNames = new ArrayList<>();
+ for (Annotation qualifier : injectionPoint.qualifiers()) {
+ qualifierNames.add(qualifier.annotationType().getName());
+ }
+ injectionPoint.qualifierInfos().stream()
+ .map(jakarta.enterprise.lang.model.AnnotationInfo::name)
+ .forEach(qualifierNames::add);
+ descriptors.add(new OdiSyntheticInjectionPoint(injectionPoint.type().getName(), qualifierNames));
+ }
+ syntheticBeanBuilder.getParams().put(OdiSyntheticParameters.INJECTION_POINTS, descriptors);
+ }
+
+ private String selectSyntheticMethodInjectionType(VisitorContext visitorContext,
+ ClassElement declaringType,
+ String methodName,
+ int injectionParameterIndex,
+ int parameterCount) {
+ boolean hasCdi5Method = hasDeclaredSyntheticMethod(declaringType, methodName, injectionParameterIndex, parameterCount, SyntheticInjections.class.getName());
+ boolean hasDeprecatedMethod = hasDeclaredSyntheticMethod(declaringType, methodName, injectionParameterIndex, parameterCount, Instance.class.getName());
+ if (hasCdi5Method && hasDeprecatedMethod) {
+ visitorContext.fail("Synthetic bean " + methodName + " method must not implement both CDI 5 SyntheticInjections and deprecated Instance signatures", declaringType);
+ return null;
+ }
+ if (hasCdi5Method) {
+ return SyntheticInjections.class.getName();
+ }
+ if (hasDeprecatedMethod) {
+ return Instance.class.getName();
+ }
+ visitorContext.fail("Synthetic bean " + methodName + " method must implement either the CDI 5 SyntheticInjections signature or the deprecated Instance signature", declaringType);
+ return null;
+ }
+
+ private boolean hasDeclaredSyntheticMethod(ClassElement declaringType,
+ String methodName,
+ int injectionParameterIndex,
+ int parameterCount,
+ String injectionTypeName) {
+ return declaringType.getEnclosedElements(syntheticMethodQuery(methodName, injectionParameterIndex, parameterCount, injectionTypeName))
+ .stream()
+ .anyMatch(method -> method.getDeclaringType().getName().equals(declaringType.getName()));
+ }
+
+ private ElementQuery syntheticMethodQuery(String methodName,
+ int injectionParameterIndex,
+ int parameterCount,
+ String injectionTypeName) {
+ return ElementQuery.ALL_METHODS
+ .onlyInstance()
+ .named(name -> name.equals(methodName))
+ .filter(method -> method.getDeclaringType().getName().equals(method.getOwningType().getName()))
+ .filter(method -> isSyntheticMethodSignature(method, injectionParameterIndex, parameterCount, injectionTypeName));
+ }
+
+ private boolean isSyntheticMethodSignature(MethodElement method,
+ int injectionParameterIndex,
+ int parameterCount,
+ String injectionTypeName) {
+ ParameterElement[] parameters = method.getParameters();
+ return parameters.length == parameterCount
+ && parameters[injectionParameterIndex].getType().getName().equals(injectionTypeName)
+ && parameters[parameters.length - 1].getType().isAssignable(Parameters.class);
+ }
+
private void copySyntheticAnnotationMetadata(VisitorContext visitorContext, MutableAnnotationMetadata syntheticBeanMetadata, BeanElementBuilder builder) {
Set annotationNames = syntheticBeanMetadata.getAnnotationNames();
for (String annotationName : annotationNames) {
@@ -260,6 +389,7 @@ private void copySyntheticAnnotationMetadata(VisitorContext visitorContext, Muta
builder.annotate(av);
}
}
+ BuildTimeExtensionRegistry.getInstance().runDiscoveryEnhancements(builder);
CdiUtil.visitBeanDefinition(visitorContext, builder);
}
}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionRegistry.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionRegistry.java
index dea7b74..68405cf 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionRegistry.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionRegistry.java
@@ -47,6 +47,7 @@
import jakarta.enterprise.inject.build.compatible.spi.FieldConfig;
import jakarta.enterprise.inject.build.compatible.spi.InterceptorInfo;
import jakarta.enterprise.inject.build.compatible.spi.InvokerFactory;
+import jakarta.enterprise.inject.build.compatible.spi.InvokerValidation;
import jakarta.enterprise.inject.build.compatible.spi.Messages;
import jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations;
import jakarta.enterprise.inject.build.compatible.spi.MethodConfig;
@@ -60,11 +61,13 @@
import jakarta.enterprise.inject.build.compatible.spi.Types;
import jakarta.enterprise.inject.build.compatible.spi.Validation;
import jakarta.enterprise.inject.spi.DeploymentException;
+import jakarta.enterprise.util.TypeLiteral;
import jakarta.enterprise.lang.model.declarations.ClassInfo;
import jakarta.enterprise.lang.model.declarations.DeclarationInfo;
import jakarta.enterprise.lang.model.declarations.FieldInfo;
import jakarta.enterprise.lang.model.declarations.MethodInfo;
import jakarta.enterprise.lang.model.types.Type;
+import jakarta.enterprise.invoke.AsyncHandler;
import jakarta.interceptor.Interceptor;
import org.eclipse.odi.cdi.OdiExecutableInvokerInfo;
@@ -77,6 +80,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -99,6 +103,9 @@ public class BuildTimeExtensionRegistry implements LifeCycle loadErrors = new ArrayList<>();
private final List beanElements = new ArrayList<>();
private final List invokers = new ArrayList<>();
+ private List returnAsyncHandlers;
+ private List parameterAsyncHandlers;
+ private boolean asyncHandlersValidated;
private DiscoveryImpl discovery;
private static final String DEPLOYMENT_EXCEPTION_MARKER = "[ODI_DEPLOYMENT_EXCEPTION] ";
private static final String DEFAULT_QUALIFIER = "jakarta.enterprise.inject.Default";
@@ -344,7 +351,6 @@ public void runRegistration(BeanElement beanElement, VisitorContext visitorConte
for (BuildCompatibleExtensionEntry entry : buildTimeExtensions) {
final BuildCompatibleExtension extension = entry.extension;
final List processingMethods = entry.registrationMethods;
- methods:
for (Method processingMethod : processingMethods) {
if (processingMethod.getParameterTypes().length == 0) {
visitorContext.fail("Registration method '"
@@ -352,13 +358,7 @@ public void runRegistration(BeanElement beanElement, VisitorContext visitorConte
+ "' of extension: " + extension.getClass().getName() + " specifies no parameters", beanElement);
continue;
}
- final Class>[] types = processingMethod.getAnnotation(Registration.class).types();
- for (Class> et : types) {
- if (et != null && beanTypes.stream().anyMatch(ce -> ce.isAssignable(et))) {
- runRegistration(extension, processingMethod, beanElement, visitorContext);
- continue methods;
- }
- }
+ runRegistration(extension, processingMethod, beanElement, visitorContext);
}
}
}
@@ -409,6 +409,8 @@ public void runValidation(VisitorContext visitorContext) {
parameters[i] = new MessagesImpl(visitorContext);
} else if (Types.class == parameterType) {
parameters[i] = new TypesImpl(visitorContext);
+ } else if (InvokerValidation.class == parameterType) {
+ parameters[i] = new InvokerValidationImpl(visitorContext, this);
} else {
unsupportedParameter(
null,
@@ -526,14 +528,20 @@ private void runRegistration(BuildCompatibleExtension extension,
throw new BuildTimeExtensionException("At least 1 parameter of type BeanInfo, ObserverInfo or InterceptorInfo is required");
} else {
final Class> type = extensionParameter.type;
+ List registrationTypes = registrationTypes(registrationMethod, visitorContext);
+ if (registrationTypes == null) {
+ return;
+ }
final BeanInfoImpl beanInfo = new BeanInfoImpl(
beanElement,
visitorContext
);
- if (BeanInfo.class == type) {
+ if (BeanInfo.class == type && matchesBeanRegistration(beanElement, registrationTypes)) {
parameters[extensionParameter.index] = beanInfo;
invokeExtensionMethod(extension, registrationMethod, parameters);
- } else if (InterceptorInfo.class == type && beanElement.hasDeclaredAnnotation(Interceptor.class)) {
+ } else if (InterceptorInfo.class == type
+ && beanElement.hasDeclaredAnnotation(Interceptor.class)
+ && matchesBeanRegistration(beanElement, registrationTypes)) {
parameters[extensionParameter.index] = new InterceptorInfoImpl(
beanElement,
visitorContext
@@ -542,6 +550,9 @@ private void runRegistration(BuildCompatibleExtension extension,
} else if (ObserverInfo.class == type) {
List observerInfos = beanInfo.observers();
for (ObserverInfo observerInfo : observerInfos) {
+ if (!matchesObserverRegistration(observerInfo, registrationTypes)) {
+ continue;
+ }
parameters[extensionParameter.index] = observerInfo;
invokeExtensionMethod(extension, registrationMethod, parameters);
}
@@ -562,6 +573,132 @@ private void runRegistration(BuildCompatibleExtension extension,
}
}
+ @Nullable
+ private List registrationTypes(Method registrationMethod, VisitorContext visitorContext) {
+ List registrationTypes = new ArrayList<>();
+ for (Class> type : registrationMethod.getAnnotation(Registration.class).types()) {
+ if (type == null) {
+ continue;
+ }
+ ClassElement typeElement = visitorContext.getClassElement(type).orElse(ClassElement.of(type));
+ if (typeElement.isAssignable(TypeLiteral.class)) {
+ Map typeArguments = typeElement.getTypeArguments(TypeLiteral.class);
+ if (typeArguments.size() != 1 || typeElement.isRawType()) {
+ visitorContext.fail("Registration type literal must declare exactly one type argument: " + type.getName(), null);
+ return null;
+ }
+ ClassElement literalType = typeArguments.values().iterator().next();
+ if (containsInvalidRegistrationTypeArgument(literalType)) {
+ visitorContext.fail("Registration type literal must not contain type variables or wildcards: " + type.getName(), null);
+ return null;
+ }
+ registrationTypes.add(new RegistrationType(literalType));
+ } else {
+ registrationTypes.add(new RegistrationType(typeElement));
+ }
+ }
+ return registrationTypes;
+ }
+
+ private boolean containsInvalidRegistrationTypeArgument(ClassElement type) {
+ if (type.isTypeVariable()
+ || type.isGenericPlaceholder()
+ || type.isWildcard()
+ || type.hasUnresolvedTypes()) {
+ return true;
+ }
+ for (ClassElement typeArgument : type.getBoundGenericTypes()) {
+ if (containsInvalidRegistrationTypeArgument(typeArgument)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean matchesBeanRegistration(BeanElement beanElement, List registrationTypes) {
+ Set beanTypes = beanElement.getBeanTypes();
+ for (RegistrationType registrationType : registrationTypes) {
+ for (ClassElement beanType : beanTypes) {
+ if (matchesRegistrationType(beanType, registrationType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean matchesObserverRegistration(ObserverInfo observerInfo, List registrationTypes) {
+ ClassElement eventType = classElement(observerInfo.eventType());
+ if (eventType == null) {
+ return false;
+ }
+ for (RegistrationType registrationType : registrationTypes) {
+ if (matchesRegistrationType(eventType, registrationType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private ClassElement classElement(Type type) {
+ if (type instanceof ClassTypeImpl classType) {
+ return classType.getClassElement();
+ }
+ if (type instanceof ParameterizedTypeImpl parameterizedType) {
+ return parameterizedType.getClassElement();
+ }
+ return null;
+ }
+
+ private boolean matchesRegistrationType(ClassElement candidateType, RegistrationType registrationType) {
+ if (!candidateType.isAssignable(registrationType.rawTypeName())) {
+ return false;
+ }
+ List extends ClassElement> requiredTypeArguments = registrationType.typeArguments();
+ if (requiredTypeArguments.isEmpty()) {
+ return true;
+ }
+ List extends ClassElement> candidateTypeArguments = candidateType.getTypeArguments(registrationType.rawTypeName())
+ .values()
+ .stream()
+ .toList();
+ if (candidateTypeArguments.isEmpty()) {
+ candidateTypeArguments = candidateType.getBoundGenericTypes();
+ }
+ return typeArgumentsEqual(candidateTypeArguments, requiredTypeArguments);
+ }
+
+ private boolean typeArgumentsEqual(List extends ClassElement> candidateTypeArguments,
+ List extends ClassElement> requiredTypeArguments) {
+ if (candidateTypeArguments.size() != requiredTypeArguments.size()) {
+ return false;
+ }
+ for (int i = 0; i < requiredTypeArguments.size(); i++) {
+ if (!typeArgumentEqual(candidateTypeArguments.get(i), requiredTypeArguments.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean typeArgumentEqual(ClassElement candidateTypeArgument, ClassElement requiredTypeArgument) {
+ if (!candidateTypeArgument.getName().equals(requiredTypeArgument.getName())) {
+ return false;
+ }
+ return typeArgumentsEqual(candidateTypeArgument.getBoundGenericTypes(), requiredTypeArgument.getBoundGenericTypes());
+ }
+
+ private record RegistrationType(ClassElement type) {
+ String rawTypeName() {
+ return type.getName();
+ }
+
+ List extends ClassElement> typeArguments() {
+ return type.getBoundGenericTypes();
+ }
+ }
+
private void handleExtensionException(BuildCompatibleExtension extension,
Method method,
@Nullable Element beanElement,
@@ -592,7 +729,11 @@ void registerInvoker(OdiExecutableInvokerInfo invokerInfo, MethodElement methodE
}
void validateInvokers(VisitorContext visitorContext) {
+ validateAsyncHandlers(visitorContext);
+ List returnHandlers = returnAsyncHandlers(visitorContext);
+ List parameterHandlers = parameterAsyncHandlers(visitorContext);
for (InvokerRegistration invoker : invokers) {
+ configureAsyncHandlers(invoker.invokerInfo, invoker.methodElement, returnHandlers, parameterHandlers);
ParameterElement[] parameters = invoker.methodElement.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (invoker.invokerInfo.isArgumentLookup(i)) {
@@ -602,6 +743,178 @@ void validateInvokers(VisitorContext visitorContext) {
}
}
+ void validateAsyncHandlers(VisitorContext visitorContext) {
+ if (asyncHandlersValidated) {
+ return;
+ }
+ asyncHandlersValidated = true;
+ validateDuplicateAsyncHandlers(visitorContext, returnAsyncHandlers(visitorContext), AsyncHandler.ReturnType.class);
+ validateDuplicateAsyncHandlers(visitorContext, parameterAsyncHandlers(visitorContext), AsyncHandler.ParameterType.class);
+ }
+
+ void validateSyntheticInjectionPoints(VisitorContext visitorContext,
+ SyntheticBeanBuilderImpl> syntheticBeanBuilder) {
+ for (SyntheticBeanBuilderImpl.SyntheticInjectionPoint injectionPoint : syntheticBeanBuilder.getInjectionPoints()) {
+ ClassElement requiredType = injectionPoint.type();
+ String requiredTypeName = requiredType.getName();
+ if (isBuiltinSyntheticInjectionPointType(requiredTypeName)) {
+ continue;
+ }
+ List requiredQualifiers = syntheticInjectionPointQualifiers(injectionPoint);
+ int candidates = 0;
+ for (BeanElement beanElement : beanElements) {
+ if (matchesRequiredType(beanElement, requiredType)
+ && matchesRequiredQualifiers(beanElement, requiredQualifiers)) {
+ candidates++;
+ }
+ }
+ if (candidates == 0) {
+ visitorContext.fail(DEPLOYMENT_EXCEPTION_MARKER
+ + "Unsatisfied synthetic injection point for " + requiredTypeName, syntheticBeanBuilder.getBeanType());
+ } else if (candidates > 1) {
+ visitorContext.fail(DEPLOYMENT_EXCEPTION_MARKER
+ + "Ambiguous synthetic injection point for " + requiredTypeName, syntheticBeanBuilder.getBeanType());
+ }
+ }
+ }
+
+ private List returnAsyncHandlers(VisitorContext visitorContext) {
+ if (returnAsyncHandlers == null) {
+ returnAsyncHandlers = asyncHandlers(visitorContext, AsyncHandler.ReturnType.class);
+ }
+ return returnAsyncHandlers;
+ }
+
+ private List parameterAsyncHandlers(VisitorContext visitorContext) {
+ if (parameterAsyncHandlers == null) {
+ parameterAsyncHandlers = asyncHandlers(visitorContext, AsyncHandler.ParameterType.class);
+ }
+ return parameterAsyncHandlers;
+ }
+
+ protected SoftServiceLoader> findAsyncHandlers(Class> handlerType) {
+ return SoftServiceLoader.load(handlerType);
+ }
+
+ private List asyncHandlers(VisitorContext visitorContext, Class> handlerInterface) {
+ List result = new ArrayList<>();
+ for (ServiceDefinition> definition : findAsyncHandlers(handlerInterface)) {
+ java.util.Optional handlerElement = visitorContext.getClassElement(definition.getName());
+ if (handlerElement.isEmpty()) {
+ visitorContext.fail("Async handler provider [" + definition.getName() + "] is not available", null);
+ continue;
+ }
+ asyncHandlerMetadata(visitorContext, handlerElement.get(), handlerInterface.getName())
+ .ifPresent(result::add);
+ }
+ return result;
+ }
+
+ boolean hasAsyncHandler(VisitorContext visitorContext, String asyncTypeName) {
+ return returnAsyncHandlers(visitorContext)
+ .stream()
+ .anyMatch(handler -> handler.asyncTypeName.equals(asyncTypeName))
+ || parameterAsyncHandlers(visitorContext)
+ .stream()
+ .anyMatch(handler -> handler.asyncTypeName.equals(asyncTypeName));
+ }
+
+ private java.util.Optional asyncHandlerMetadata(VisitorContext visitorContext,
+ ClassElement handlerElement,
+ String handlerInterfaceName) {
+ if (!directlyImplements(handlerElement, handlerInterfaceName)) {
+ visitorContext.fail("Async handler [" + handlerElement.getName() + "] must directly implement ["
+ + handlerInterfaceName + "]", handlerElement);
+ return java.util.Optional.empty();
+ }
+ if (directlyImplements(handlerElement, AsyncHandler.ReturnType.class.getName())
+ && directlyImplements(handlerElement, AsyncHandler.ParameterType.class.getName())) {
+ visitorContext.fail("Async handler [" + handlerElement.getName()
+ + "] must not implement both AsyncHandler.ReturnType and AsyncHandler.ParameterType", handlerElement);
+ return java.util.Optional.empty();
+ }
+ Map typeArguments = handlerElement.getTypeArguments(handlerInterfaceName);
+ if (typeArguments.size() != 1 || handlerElement.isRawType()) {
+ visitorContext.fail("Async handler [" + handlerElement.getName()
+ + "] must declare exactly one async type argument for [" + handlerInterfaceName + "]", handlerElement);
+ return java.util.Optional.empty();
+ }
+ ClassElement asyncType = typeArguments.values().iterator().next();
+ if (asyncType.isArray()
+ || asyncType.isTypeVariable()
+ || asyncType.isGenericPlaceholder()
+ || asyncType.isWildcard()
+ || asyncType.hasUnresolvedTypes()) {
+ visitorContext.fail("Async handler [" + handlerElement.getName()
+ + "] declares an invalid async type [" + asyncType.getName() + "]", handlerElement);
+ return java.util.Optional.empty();
+ }
+ return java.util.Optional.of(new AsyncHandlerMetadata(handlerElement.getName(), asyncType.getName()));
+ }
+
+ private boolean directlyImplements(ClassElement handlerElement, String handlerInterfaceName) {
+ return handlerElement.getInterfaces()
+ .stream()
+ .anyMatch(interfaceElement -> typeNameEquals(interfaceElement.getName(), handlerInterfaceName));
+ }
+
+ private boolean typeNameEquals(String left, String right) {
+ return left.equals(right)
+ || left.replace('$', '.').equals(right.replace('$', '.'));
+ }
+
+ private void validateDuplicateAsyncHandlers(VisitorContext visitorContext,
+ List handlers,
+ Class> handlerInterface) {
+ for (int i = 0; i < handlers.size(); i++) {
+ AsyncHandlerMetadata left = handlers.get(i);
+ for (int j = i + 1; j < handlers.size(); j++) {
+ AsyncHandlerMetadata right = handlers.get(j);
+ if (left.asyncTypeName.equals(right.asyncTypeName)
+ && !left.handlerClassName.equals(right.handlerClassName)) {
+ visitorContext.fail(DEPLOYMENT_EXCEPTION_MARKER
+ + "Multiple async handlers of type [" + handlerInterface.getName()
+ + "] declare async type [" + left.asyncTypeName + "]", null);
+ return;
+ }
+ }
+ }
+ }
+
+ private void configureAsyncHandlers(OdiExecutableInvokerInfo invokerInfo,
+ MethodElement methodElement,
+ List returnHandlers,
+ List parameterHandlers) {
+ String returnTypeName = methodElement.getReturnType().getType().getName();
+ for (AsyncHandlerMetadata handler : returnHandlers) {
+ if (handler.asyncTypeName.equals(returnTypeName)) {
+ invokerInfo.asyncReturnHandler(handler.handlerClassName);
+ return;
+ }
+ }
+ for (AsyncHandlerMetadata handler : parameterHandlers) {
+ int parameterIndex = uniqueParameterIndex(methodElement, handler.asyncTypeName);
+ if (parameterIndex >= 0) {
+ invokerInfo.asyncParameterHandler(handler.handlerClassName, parameterIndex);
+ return;
+ }
+ }
+ }
+
+ private int uniqueParameterIndex(MethodElement methodElement, String typeName) {
+ int index = -1;
+ ParameterElement[] parameters = methodElement.getParameters();
+ for (int i = 0; i < parameters.length; i++) {
+ if (parameters[i].getType().getName().equals(typeName)) {
+ if (index >= 0) {
+ return -1;
+ }
+ index = i;
+ }
+ }
+ return index;
+ }
+
private void validateArgumentLookup(VisitorContext visitorContext,
MethodElement methodElement,
ParameterElement parameterElement) {
@@ -633,6 +946,11 @@ private boolean isBuiltinLookupType(String typeName) {
|| typeName.equals("jakarta.enterprise.inject.Instance");
}
+ private boolean isBuiltinSyntheticInjectionPointType(String typeName) {
+ return typeName.equals("jakarta.enterprise.inject.spi.InjectionPoint")
+ || isBuiltinLookupType(typeName);
+ }
+
private boolean matchesRequiredType(BeanElement beanElement, ClassElement requiredType) {
String requiredTypeName = requiredType.getName();
return beanElement.getBeanTypes()
@@ -640,12 +958,31 @@ private boolean matchesRequiredType(BeanElement beanElement, ClassElement requir
.anyMatch(beanType -> beanType.getName().equals(requiredTypeName));
}
+ private List syntheticInjectionPointQualifiers(SyntheticBeanBuilderImpl.SyntheticInjectionPoint injectionPoint) {
+ List requiredQualifiers = new ArrayList<>();
+ for (Annotation qualifier : injectionPoint.qualifiers()) {
+ requiredQualifiers.add(qualifier.annotationType().getName());
+ }
+ injectionPoint.qualifierInfos()
+ .stream()
+ .map(jakarta.enterprise.lang.model.AnnotationInfo::name)
+ .forEach(requiredQualifiers::add);
+ if (requiredQualifiers.isEmpty()) {
+ requiredQualifiers = Collections.singletonList(DEFAULT_QUALIFIER);
+ }
+ return requiredQualifiers;
+ }
+
private boolean matchesRequiredQualifiers(BeanElement beanElement, ParameterElement parameterElement) {
List requiredQualifiers = parameterElement.getAnnotationMetadata()
.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER);
if (requiredQualifiers.isEmpty()) {
requiredQualifiers = Collections.singletonList(DEFAULT_QUALIFIER);
}
+ return matchesRequiredQualifiers(beanElement, requiredQualifiers);
+ }
+
+ private boolean matchesRequiredQualifiers(BeanElement beanElement, List requiredQualifiers) {
Collection beanQualifiers = beanElement.getQualifiers();
if (requiredQualifiers.size() == 1 && requiredQualifiers.contains(ANY_QUALIFIER)) {
return true;
@@ -789,6 +1126,9 @@ public BuildTimeExtensionRegistry stop() {
this.buildTimeExtensions.clear();
this.beanElements.clear();
this.invokers.clear();
+ this.returnAsyncHandlers = null;
+ this.parameterAsyncHandlers = null;
+ this.asyncHandlersValidated = false;
return this;
}
@@ -802,6 +1142,16 @@ private InvokerRegistration(OdiExecutableInvokerInfo invokerInfo, MethodElement
}
}
+ private static final class AsyncHandlerMetadata {
+ final String handlerClassName;
+ final String asyncTypeName;
+
+ private AsyncHandlerMetadata(String handlerClassName, String asyncTypeName) {
+ this.handlerClassName = handlerClassName;
+ this.asyncTypeName = asyncTypeName;
+ }
+ }
+
private static final class ExtensionParameter {
final int index;
final Class> type;
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionVisitor.java
index 0786381..f117a21 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/BuildTimeExtensionVisitor.java
@@ -15,6 +15,7 @@
*/
package org.eclipse.odi.cdi.processor.extensions;
+import io.micronaut.aop.Around;
import io.micronaut.aop.InterceptorBindingDefinitions;
import io.micronaut.context.annotation.Executable;
import io.micronaut.core.annotation.AnnotationClassValue;
@@ -34,6 +35,7 @@
import io.micronaut.inject.ast.beans.BeanMethodElement;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.runtime.context.scope.ScopedProxy;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.NormalScope;
import jakarta.enterprise.event.Observes;
@@ -58,6 +60,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Consumer;
@@ -155,6 +158,7 @@ public void visitClass(ClassElement element, VisitorContext context) {
public void finish(VisitorContext visitorContext) {
if (!hasErrors && !finished) {
finished = true;
+ registry.validateAsyncHandlers(visitorContext);
if (applicationClassElement != null) {
final Set scannedClassNames = this.discovery
.getScannedClasses()
@@ -210,6 +214,9 @@ private void configureInterceptorBinding(ClassElement interceptorBinding) {
}
private void handleScannedClass(VisitorContext visitorContext, ClassElement scannedClass) {
+ if (isVetoed(scannedClass)) {
+ return;
+ }
configureInterceptorBindings(visitorContext, scannedClass);
this.registry.runEnhancement(scannedClass, scannedClass, visitorContext);
boolean isInterceptor = scannedClass.hasAnnotation(Interceptor.class);
@@ -289,6 +296,14 @@ private void handleScannedClass(VisitorContext visitorContext, ClassElement scan
registry.runDiscoveryEnhancements(beanElementBuilder);
CdiUtil.visitBeanDefinition(visitorContext, beanElementBuilder);
registry.runDiscoveryEnhancements(beanElementBuilder);
+ if (isNormalScopedBean(visitorContext, scannedClass)) {
+ beanElementBuilder.intercept(AnnotationValue.builder(Around.class)
+ .member("proxyTarget", true)
+ .member("lazy", true)
+ .build());
+ beanElementBuilder.annotate(ScopedProxy.class);
+ beanElementBuilder.annotate(AnnotationUtil.SCOPE);
+ }
if (!isInterceptor && !scannedClass.isFinal()) {
if (hasInterceptorBinding(visitorContext, scannedClass)) {
beanElementBuilder.intercept();
@@ -325,6 +340,11 @@ private void handleScannedClass(VisitorContext visitorContext, ClassElement scan
.inject();
}
+ private boolean isVetoed(ClassElement scannedClass) {
+ return scannedClass.hasAnnotation(io.micronaut.core.annotation.Vetoed.class)
+ || scannedClass.hasAnnotation(jakarta.enterprise.inject.Vetoed.class);
+ }
+
@SuppressWarnings("unchecked")
private void propagateAlternativeMetadata(ClassElement declaringType, BeanElementBuilder builder) {
if (!declaringType.hasAnnotation(Alternative.class) && !declaringType.hasStereotype(Alternative.class)) {
@@ -388,6 +408,17 @@ private boolean hasInterceptorBinding(VisitorContext visitorContext, Element ele
.orElse(false));
}
+ private boolean isNormalScopedBean(VisitorContext visitorContext, ClassElement beanElement) {
+ if (beanElement.hasStereotype(NormalScope.class)) {
+ return true;
+ }
+ return beanElement.getAnnotationNames()
+ .stream()
+ .map(visitorContext::getClassElement)
+ .flatMap(Optional::stream)
+ .anyMatch(annotationType -> annotationType.hasAnnotation(NormalScope.class) || annotationType.hasStereotype(NormalScope.class));
+ }
+
private static void handleObservesMethod(ClassElement classElement, MethodElement methodElement, VisitorContext visitorContext) {
ObservesMethodVisitor observesVisitor = new ObservesMethodVisitor();
observesVisitor.visitClass(classElement, visitorContext);
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/ClassInfoImpl.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/ClassInfoImpl.java
index 697a0a5..3ef25dc 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/ClassInfoImpl.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/ClassInfoImpl.java
@@ -21,6 +21,7 @@
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.PackageElement;
+import io.micronaut.inject.ast.PropertyElement;
import io.micronaut.inject.visitor.VisitorContext;
import jakarta.enterprise.inject.build.compatible.spi.Types;
import jakarta.enterprise.lang.model.declarations.ClassInfo;
@@ -33,7 +34,6 @@
import java.lang.annotation.Annotation;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -121,6 +121,14 @@ public List superInterfacesDeclarations() {
.collect(Collectors.toUnmodifiableList());
}
+ @Override
+ public Collection permittedSubclasses() {
+ return classElement.getPermittedSubclasses()
+ .stream()
+ .map(element -> new ClassInfoImpl(element, types, visitorContext))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
@Override
public boolean isPlainClass() {
return !isInterface() && !isEnum() && !isAnnotation() && !isRecord();
@@ -156,6 +164,11 @@ public boolean isFinal() {
return classElement.isFinal();
}
+ @Override
+ public boolean isSealed() {
+ return classElement.isSealed();
+ }
+
@Override
public int modifiers() {
return toReflectModifiers(classElement.getModifiers());
@@ -191,8 +204,22 @@ public Collection fields() {
@Override
public Collection recordComponents() {
- // TODO
- return Collections.emptyList();
+ if (!isRecord()) {
+ return List.of();
+ }
+ return classElement.getBeanProperties()
+ .stream()
+ .map(this::toRecordComponentInfo)
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ private RecordComponentInfo toRecordComponentInfo(PropertyElement propertyElement) {
+ return new RecordComponentInfoImpl(
+ this,
+ propertyElement,
+ types,
+ visitorContext
+ );
}
private ClassInfoImpl getDeclaringType(ClassElement declaringType) {
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/InvokerValidationImpl.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/InvokerValidationImpl.java
new file mode 100644
index 0000000..d08a756
--- /dev/null
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/InvokerValidationImpl.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2026 Oracle and/or its affiliates.
+ *
+ * Licensed 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
+ *
+ * https://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.eclipse.odi.cdi.processor.extensions;
+
+import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.enterprise.inject.build.compatible.spi.InvokerValidation;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.function.Supplier;
+
+final class InvokerValidationImpl implements InvokerValidation {
+ private static final String REACTIVE_STREAMS_PUBLISHER = "org.reactivestreams.Publisher";
+
+ private final VisitorContext visitorContext;
+ private final BuildTimeExtensionRegistry registry;
+
+ InvokerValidationImpl(VisitorContext visitorContext, BuildTimeExtensionRegistry registry) {
+ this.visitorContext = visitorContext;
+ this.registry = registry;
+ }
+
+ @Override
+ public void ensureAsyncHandlerExists(Class> returnType, Supplier errorMessage) {
+ if (returnType == CompletionStage.class
+ || returnType == CompletableFuture.class
+ || returnType == Flow.Publisher.class
+ || returnType.getName().equals(REACTIVE_STREAMS_PUBLISHER)) {
+ return;
+ }
+ if (registry.hasAsyncHandler(visitorContext, returnType.getName())) {
+ return;
+ }
+ visitorContext.fail(errorMessage.get(), null);
+ }
+}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/MetaAnnotationsImpl.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/MetaAnnotationsImpl.java
index 9bdefe1..a627619 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/MetaAnnotationsImpl.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/MetaAnnotationsImpl.java
@@ -27,6 +27,8 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.NormalScope;
import jakarta.enterprise.context.spi.AlterableContext;
import jakarta.enterprise.inject.Stereotype;
@@ -38,6 +40,7 @@
@Internal
final class MetaAnnotationsImpl implements MetaAnnotations {
+ private static final String DEPLOYMENT_EXCEPTION_MARKER = "[ODI_DEPLOYMENT_EXCEPTION] ";
// private List contextBuilders = new ArrayList<>();
private final Set interceptorBindings = new HashSet<>();
private final Set qualifiers = new HashSet<>();
@@ -111,6 +114,11 @@ public void addContext(Class extends Annotation> scopeAnnotation,
private void addContext(Class extends Annotation> scopeAnnotation,
Boolean isNormal,
Class extends AlterableContext> contextClass) {
+ if (scopeAnnotation == ApplicationScoped.class || scopeAnnotation == Dependent.class) {
+ visitorContext.fail(DEPLOYMENT_EXCEPTION_MARKER
+ + "Cannot register a custom context for built-in scope: " + scopeAnnotation.getName(), null);
+ return;
+ }
final ClassElement scopeElement = visitorContext.getClassElement(scopeAnnotation)
.orElseThrow(() -> new RuntimeException("Scope type [" + scopeAnnotation.getName() + "] must be on the application classpath"));
if (isNormal != null) {
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/RecordComponentInfoImpl.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/RecordComponentInfoImpl.java
new file mode 100644
index 0000000..99cc38c
--- /dev/null
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/RecordComponentInfoImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed 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
+ *
+ * https://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.eclipse.odi.cdi.processor.extensions;
+
+import io.micronaut.inject.ast.MethodElement;
+import io.micronaut.inject.ast.PropertyElement;
+import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.enterprise.inject.build.compatible.spi.Types;
+import jakarta.enterprise.lang.model.declarations.ClassInfo;
+import jakarta.enterprise.lang.model.declarations.DeclarationInfo;
+import jakarta.enterprise.lang.model.declarations.FieldInfo;
+import jakarta.enterprise.lang.model.declarations.MethodInfo;
+import jakarta.enterprise.lang.model.declarations.RecordComponentInfo;
+import jakarta.enterprise.lang.model.types.Type;
+
+final class RecordComponentInfoImpl extends AnnotationTargetImpl implements RecordComponentInfo {
+ private final ClassInfoImpl declaringRecord;
+ private final PropertyElement propertyElement;
+
+ RecordComponentInfoImpl(ClassInfoImpl declaringRecord,
+ PropertyElement propertyElement,
+ Types types,
+ VisitorContext visitorContext) {
+ super(propertyElement, types, visitorContext);
+ this.declaringRecord = declaringRecord;
+ this.propertyElement = propertyElement;
+ }
+
+ @Override
+ public DeclarationInfo asDeclaration() {
+ return this;
+ }
+
+ @Override
+ public Kind kind() {
+ return Kind.RECORD_COMPONENT;
+ }
+
+ @Override
+ public String name() {
+ return propertyElement.getName();
+ }
+
+ @Override
+ public Type type() {
+ FieldInfo field = field();
+ if (field != null) {
+ return field.type();
+ }
+ MethodInfo accessor = accessor();
+ if (accessor != null) {
+ return accessor.returnType();
+ }
+ return TypeFactory.createType(propertyElement.getGenericType(), types, visitorContext);
+ }
+
+ @Override
+ public FieldInfo field() {
+ return propertyElement.getField()
+ .map(fieldElement -> new FieldInfoImpl(declaringRecord, fieldElement, types, visitorContext))
+ .orElse(null);
+ }
+
+ @Override
+ public MethodInfo accessor() {
+ MethodElement accessor = propertyElement.getReadMethod().orElse(null);
+ if (accessor == null) {
+ return null;
+ }
+ return new MethodInfoImpl(declaringRecord, accessor, types, visitorContext);
+ }
+
+ @Override
+ public ClassInfo declaringRecord() {
+ return declaringRecord;
+ }
+}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/SyntheticBeanBuilderImpl.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/SyntheticBeanBuilderImpl.java
index 7d4825f..319aa29 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/SyntheticBeanBuilderImpl.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/extensions/SyntheticBeanBuilderImpl.java
@@ -17,8 +17,11 @@
import java.lang.annotation.Annotation;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -26,7 +29,10 @@
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.enterprise.context.AutoClose;
+import jakarta.enterprise.context.Eager;
import jakarta.enterprise.inject.Alternative;
+import jakarta.enterprise.inject.Reserve;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanDisposer;
@@ -43,6 +49,7 @@ final class SyntheticBeanBuilderImpl extends AbstractSyntheticBuilder impleme
private final ClassElement beanType;
private final VisitorContext localVisitorContext;
private final Set exposedTypes = new HashSet<>();
+ private final List injectionPoints = new ArrayList<>();
private Class extends SyntheticBeanDisposer> disposerClass;
private Class extends SyntheticBeanCreator> creatorClass;
@@ -70,6 +77,10 @@ public Class extends SyntheticBeanCreator> getCreatorClass() {
return creatorClass;
}
+ public List getInjectionPoints() {
+ return Collections.unmodifiableList(injectionPoints);
+ }
+
@Override
public SyntheticBeanBuilder type(Class> type) {
if (type != null) {
@@ -121,7 +132,17 @@ public SyntheticBeanBuilder scope(Class extends Annotation> scopeAnnotation
@Override
public SyntheticBeanBuilder alternative(boolean isAlternative) {
- addAnnotation(Alternative.class);
+ if (isAlternative) {
+ addAnnotation(Alternative.class);
+ }
+ return this;
+ }
+
+ @Override
+ public SyntheticBeanBuilder reserve(boolean isReserve) {
+ if (isReserve) {
+ addAnnotation(Reserve.class);
+ }
return this;
}
@@ -131,6 +152,22 @@ public SyntheticBeanBuilder priority(int priority) {
return this;
}
+ @Override
+ public SyntheticBeanBuilder eager(boolean isEager) {
+ if (isEager) {
+ addAnnotation(Eager.class);
+ }
+ return this;
+ }
+
+ @Override
+ public SyntheticBeanBuilder autoClose(boolean isAutoClose) {
+ if (isAutoClose) {
+ addAnnotation(AutoClose.class);
+ }
+ return this;
+ }
+
@Override
public SyntheticBeanBuilder name(String name) {
getAnnotationMetadata().addAnnotation(AnnotationUtil.NAMED, Collections.singletonMap(AnnotationMetadata.VALUE_MEMBER, name));
@@ -282,6 +319,53 @@ public SyntheticBeanBuilder withParam(String key, InvokerInfo[] value) {
return this;
}
+ @Override
+ public SyntheticBeanBuilder withInjectionPoint(Class> type) {
+ return withInjectionPoint(type, new Annotation[0]);
+ }
+
+ @Override
+ public SyntheticBeanBuilder withInjectionPoint(Class> type, Annotation... qualifiers) {
+ Objects.requireNonNull(type, "Injection point type cannot be null");
+ injectionPoints.add(new SyntheticInjectionPoint(ClassElement.of(type), List.of(qualifiers), List.of()));
+ return this;
+ }
+
+ @Override
+ public SyntheticBeanBuilder withInjectionPoint(Class> type, AnnotationInfo... qualifiers) {
+ Objects.requireNonNull(type, "Injection point type cannot be null");
+ injectionPoints.add(new SyntheticInjectionPoint(ClassElement.of(type), List.of(), List.of(qualifiers)));
+ return this;
+ }
+
+ @Override
+ public SyntheticBeanBuilder withInjectionPoint(Type type) {
+ return withInjectionPoint(type, new Annotation[0]);
+ }
+
+ @Override
+ public SyntheticBeanBuilder withInjectionPoint(Type type, Annotation... qualifiers) {
+ injectionPoints.add(new SyntheticInjectionPoint(toClassElement(type), List.of(qualifiers), List.of()));
+ return this;
+ }
+
+ @Override
+ public SyntheticBeanBuilder withInjectionPoint(Type type, AnnotationInfo... qualifiers) {
+ injectionPoints.add(new SyntheticInjectionPoint(toClassElement(type), List.of(), List.of(qualifiers)));
+ return this;
+ }
+
+ private ClassElement toClassElement(Type type) {
+ Objects.requireNonNull(type, "Injection point type cannot be null");
+ if (type instanceof ClassTypeImpl classType) {
+ return classType.getClassElement();
+ }
+ if (type instanceof ParameterizedTypeImpl parameterizedType) {
+ return parameterizedType.getClassElement();
+ }
+ throw new IllegalArgumentException("Unsupported synthetic injection point type: " + type);
+ }
+
@Override
public SyntheticBeanBuilder createWith(Class extends SyntheticBeanCreator> creatorClass) {
this.creatorClass = creatorClass;
@@ -307,4 +391,13 @@ public DeclarationInfo asDeclaration() {
public DeclarationInfo info() {
throw new IllegalStateException("Not a declaration");
}
+
+ record SyntheticInjectionPoint(ClassElement type,
+ List qualifiers,
+ List qualifierInfos) {
+ SyntheticInjectionPoint {
+ qualifiers = qualifiers == null ? List.of() : Arrays.asList(qualifiers.toArray(Annotation[]::new));
+ qualifierInfos = qualifierInfos == null ? List.of() : Arrays.asList(qualifierInfos.toArray(AnnotationInfo[]::new));
+ }
+ }
}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/mappers/EagerMapper.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/mappers/EagerMapper.java
new file mode 100644
index 0000000..a71e79f
--- /dev/null
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/mappers/EagerMapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed 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
+ *
+ * https://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.eclipse.odi.cdi.processor.mappers;
+
+import io.micronaut.context.annotation.Context;
+import io.micronaut.core.annotation.AnnotationValue;
+import io.micronaut.inject.annotation.TypedAnnotationMapper;
+import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.enterprise.context.Eager;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Maps {@link Eager} to Micronaut eager initialization.
+ */
+public class EagerMapper implements TypedAnnotationMapper {
+
+ @Override
+ public Class annotationType() {
+ return Eager.class;
+ }
+
+ @Override
+ public List> map(AnnotationValue annotation, VisitorContext visitorContext) {
+ return Collections.singletonList(AnnotationValue.builder(Context.class).build());
+ }
+}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/Cdi5AnnotationVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/Cdi5AnnotationVisitor.java
new file mode 100644
index 0000000..0d0d72e
--- /dev/null
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/Cdi5AnnotationVisitor.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed 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
+ *
+ * https://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.eclipse.odi.cdi.processor.visitors;
+
+import io.micronaut.aop.Around;
+import io.micronaut.context.annotation.Bean;
+import io.micronaut.context.annotation.Context;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.context.annotation.Secondary;
+import io.micronaut.core.annotation.AnnotationClassValue;
+import io.micronaut.core.util.CollectionUtils;
+import io.micronaut.inject.ast.AnnotationElement;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.ast.Element;
+import io.micronaut.inject.ast.ElementQuery;
+import io.micronaut.inject.ast.FieldElement;
+import io.micronaut.inject.ast.MemberElement;
+import io.micronaut.inject.ast.MethodElement;
+import io.micronaut.inject.visitor.TypeElementVisitor;
+import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.annotation.Priority;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.AutoClose;
+import jakarta.enterprise.context.Eager;
+import jakarta.enterprise.inject.Alternative;
+import jakarta.enterprise.inject.Produces;
+import jakarta.enterprise.inject.Reserve;
+import jakarta.enterprise.inject.Stereotype;
+import jakarta.interceptor.AroundInvoke;
+import jakarta.interceptor.InterceptorBinding;
+import org.eclipse.odi.cdi.processor.CdiUtil;
+
+import java.util.Set;
+
+/**
+ * Processes CDI 5 bean annotations that need Micronaut metadata.
+ */
+public class Cdi5AnnotationVisitor implements TypeElementVisitor {
+ private static final String MICRONAUT_INTERCEPTOR_BINDING = "io.micronaut.aop.InterceptorBinding";
+ private static final AnnotationClassValue UNSELECTED_RESERVE_CONDITION =
+ new AnnotationClassValue<>("org.eclipse.odi.cdi.condition.UnselectedReserveCondition");
+
+ @Override
+ public Set getSupportedAnnotationNames() {
+ return CollectionUtils.setOf(
+ Eager.class.getName(),
+ Reserve.class.getName(),
+ AutoClose.class.getName()
+ );
+ }
+
+ @Override
+ public void visitClass(ClassElement element, VisitorContext context) {
+ processEager(element, context);
+ processReserve(element, context);
+ if (hasAutoClose(element)) {
+ element.getEnclosedElements(ElementQuery.ALL_METHODS.onlyDeclared().named(method -> method.equals("close")))
+ .stream()
+ .filter(method -> method.getParameters().length == 0)
+ .findFirst()
+ .ifPresent(method -> {
+ if (hasInterception(element, method)) {
+ method.annotate(Around.class, builder -> builder.member("proxyTarget", false));
+ } else {
+ method.annotate(PreDestroy.class);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void visitMethod(MethodElement element, VisitorContext context) {
+ processEager(element, context);
+ processReserve(element, context);
+ processAutoCloseProducer(element);
+ }
+
+ @Override
+ public void visitField(FieldElement element, VisitorContext context) {
+ processEager(element, context);
+ processReserve(element, context);
+ processAutoCloseProducer(element);
+ }
+
+ private static void processEager(Element element, VisitorContext context) {
+ if (!hasEager(element)) {
+ return;
+ }
+ if (hasInvalidEagerStereotype(element, context)) {
+ context.fail("@Eager stereotypes must be @ApplicationScoped", element);
+ return;
+ }
+ if (!isApplicationScoped(element)) {
+ context.fail("@Eager beans must be @ApplicationScoped", element);
+ return;
+ }
+ if (element instanceof AnnotationElement) {
+ return;
+ }
+ if (element instanceof MemberElement && !element.hasAnnotation(Produces.class)) {
+ return;
+ }
+ element.annotate(Context.class);
+ }
+
+ private static void processReserve(Element element, VisitorContext context) {
+ if (isAlternativeProducerOfReserveBean(element)) {
+ context.fail("@Reserve beans cannot declare @Alternative producers", element);
+ return;
+ }
+ if (!hasReserve(element)) {
+ return;
+ }
+ if (element.hasAnnotation(Alternative.class) || element.hasStereotype(Alternative.class)) {
+ context.fail("@Reserve beans cannot also be @Alternative", element);
+ return;
+ }
+ if (hasSelectingPriority(element)) {
+ element.annotate(Secondary.class);
+ } else {
+ element.annotate(Requires.class, builder -> builder.member("condition", UNSELECTED_RESERVE_CONDITION));
+ }
+ }
+
+ private static boolean hasSelectingPriority(Element element) {
+ if (element instanceof MemberElement) {
+ return element.hasDeclaredAnnotation(Priority.class)
+ || element.hasDeclaredStereotype(Priority.class);
+ }
+ return CdiUtil.hasPriority(element);
+ }
+
+ private static void processAutoCloseProducer(MemberElement element) {
+ if (hasAutoClose(element) && producedType(element).isAssignable(AutoCloseable.class)) {
+ element.annotate(Bean.class, builder -> builder.member("preDestroy", "close"));
+ }
+ }
+
+ private static boolean hasEager(Element element) {
+ return element.hasAnnotation(Eager.class) || element.hasStereotype(Eager.class);
+ }
+
+ private static boolean hasInvalidEagerStereotype(Element element, VisitorContext context) {
+ for (String stereotype : element.getAnnotationNamesByStereotype(Stereotype.class)) {
+ if (stereotype.equals(Stereotype.class.getName())) {
+ continue;
+ }
+ if (context.getClassElement(stereotype)
+ .filter(stereotypeElement -> stereotypeElement.hasAnnotation(Eager.class)
+ && !stereotypeElement.hasAnnotation(ApplicationScoped.class))
+ .isPresent()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasReserve(Element element) {
+ if (element instanceof MemberElement) {
+ return element.hasDeclaredAnnotation(Reserve.class)
+ || element.hasDeclaredStereotype(Reserve.class);
+ }
+ return element.hasAnnotation(Reserve.class) || element.hasStereotype(Reserve.class);
+ }
+
+ private static boolean isAlternativeProducerOfReserveBean(Element element) {
+ if (!(element instanceof MemberElement memberElement) || !element.hasAnnotation(Produces.class)) {
+ return false;
+ }
+ ClassElement declaringType = memberElement.getDeclaringType();
+ return (element.hasAnnotation(Alternative.class) || element.hasStereotype(Alternative.class))
+ && (declaringType.hasAnnotation(Reserve.class) || declaringType.hasStereotype(Reserve.class));
+ }
+
+ private static boolean hasAutoClose(Element element) {
+ return element.hasAnnotation(AutoClose.class) || element.hasStereotype(AutoClose.class);
+ }
+
+ private static boolean hasInterception(ClassElement element, MethodElement closeMethod) {
+ return hasInterceptorBinding(element)
+ || hasInterceptorBinding(closeMethod)
+ || !element.getEnclosedElements(ElementQuery.ALL_METHODS
+ .onlyInstance()
+ .onlyConcrete()
+ .onlyDeclared()
+ .annotated(annotationMetadata -> annotationMetadata.hasAnnotation(AroundInvoke.class)))
+ .isEmpty();
+ }
+
+ private static boolean hasInterceptorBinding(Element element) {
+ return !element.getAnnotationNamesByStereotype(InterceptorBinding.class).isEmpty()
+ || !element.getAnnotationNamesByStereotype(MICRONAUT_INTERCEPTOR_BINDING).isEmpty();
+ }
+
+ private static boolean isApplicationScoped(Element element) {
+ return element.hasAnnotation(ApplicationScoped.class) || element.hasStereotype(ApplicationScoped.class);
+ }
+
+ private static ClassElement producedType(MemberElement element) {
+ if (element instanceof MethodElement) {
+ return ((MethodElement) element).getGenericReturnType();
+ }
+ if (element instanceof FieldElement) {
+ return ((FieldElement) element).getGenericField();
+ }
+ throw new IllegalArgumentException("Unsupported producer element: " + element);
+ }
+
+ @Override
+ public VisitorKind getVisitorKind() {
+ return VisitorKind.ISOLATING;
+ }
+}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/DisposesMethodVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/DisposesMethodVisitor.java
index 49dd41c..ca35115 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/DisposesMethodVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/DisposesMethodVisitor.java
@@ -17,6 +17,7 @@
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ElementQuery;
+import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.context.annotation.Executable;
@@ -57,11 +58,11 @@ public void handleMatch(MethodElement methodElement, ParameterElement parameterE
// Skip validating for beans with qualifiers
if (!parameterElement.hasDeclaredStereotype(io.micronaut.core.annotation.AnnotationUtil.QUALIFIER)) {
- Optional producerMethod = validateMatchingProduces(methodElement, context, disposedType);
- if (producerMethod.isEmpty()) {
+ Optional producer = validateMatchingProduces(methodElement, context, disposedType);
+ if (producer.isEmpty()) {
return;
}
- producerMethod.get().annotate(AnnotationUtil.ANN_DISPOSER_METHOD);
+ producer.get().annotate(AnnotationUtil.ANN_DISPOSER_METHOD);
}
this.disposerMethods.add(methodElement);
@@ -71,7 +72,7 @@ public void handleMatch(MethodElement methodElement, ParameterElement parameterE
}
}
- private Optional validateMatchingProduces(MethodElement element, VisitorContext context, ClassElement disposedType) {
+ private Optional validateMatchingProduces(MethodElement element, VisitorContext context, ClassElement disposedType) {
if (!disposerMethods.isEmpty()) {
for (MethodElement disposerMethod : disposerMethods) {
final Optional disposerParam = Arrays.stream(disposerMethod.getParameters())
@@ -90,20 +91,28 @@ private Optional validateMatchingProduces(MethodElement element,
}
// now validate if a bean producing method is present
- final Optional producerMethod = currentClass.getEnclosedElement(
+ Optional producer = currentClass.getEnclosedElement(
ElementQuery.ALL_METHODS
.onlyConcrete()
.annotated((annotationMetadata -> annotationMetadata.hasDeclaredAnnotation(Produces.class)))
.filter((methodElement -> disposedType.isAssignable(methodElement.getGenericReturnType())))
- );
+ ).map(method -> (MemberElement) method);
- if (producerMethod.isEmpty()) {
+ if (producer.isEmpty()) {
+ producer = currentClass.getEnclosedElement(
+ ElementQuery.ALL_FIELDS
+ .annotated((annotationMetadata -> annotationMetadata.hasDeclaredAnnotation(Produces.class)))
+ .filter((fieldElement -> disposedType.isAssignable(fieldElement.getGenericField())))
+ ).map(field -> (MemberElement) field);
+ }
+
+ if (producer.isEmpty()) {
context.fail(
"No associated @Produces method found for @Disposes method. A method with a @Disposes parameter"
- + " must declare a method annotated with @Produces that has the same type as the "
+ + " must declare a method or field annotated with @Produces that has the same type as the "
+ "parameter. See " + CdiUtil.SPEC_LOCATION + "#disposer_method_resolution",
element);
}
- return producerMethod;
+ return producer;
}
}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InjectVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InjectVisitor.java
index b574746..42dbf05 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InjectVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InjectVisitor.java
@@ -19,6 +19,7 @@
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ConstructorElement;
+import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
@@ -28,7 +29,6 @@
import org.eclipse.odi.cdi.processor.CdiUtil;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -41,8 +41,8 @@ public class InjectVisitor implements TypeElementVisitor {
private List injectConstructors = new ArrayList<>(2);
@Override
- public Set getSupportedAnnotationNames() {
- return Collections.singleton(AnnotationUtil.INJECT);
+ public String getElementType() {
+ return AnnotationUtil.INJECT;
}
@Override
@@ -59,39 +59,54 @@ public int getOrder() {
@Override
public void visitClass(ClassElement element, VisitorContext context) {
injectConstructors.clear();
+ element.getEnclosedElements(ElementQuery.CONSTRUCTORS)
+ .stream()
+ .filter(constructor -> constructor.hasAnnotation(AnnotationUtil.INJECT))
+ .forEach(constructor -> validateInjectConstructor(constructor, context));
+ element.getEnclosedElements(ElementQuery.ALL_METHODS.onlyDeclared())
+ .stream()
+ .filter(method -> method.hasAnnotation(AnnotationUtil.INJECT))
+ .forEach(method -> validateInjectMethod(method, context));
+ element.getEnclosedElements(ElementQuery.ALL_FIELDS.onlyDeclared())
+ .stream()
+ .filter(field -> field.hasAnnotation(AnnotationUtil.INJECT))
+ .forEach(field -> validateInjectField(field, context));
}
@Override
public void visitConstructor(ConstructorElement element, VisitorContext context) {
- if (element.hasDeclaredAnnotation(AnnotationUtil.INJECT)) {
-
- validateInjectMethod(element, context);
- injectConstructors.add(element);
- if (injectConstructors.size() == 2) {
- final String methodDesc = injectConstructors.stream()
- .map((me) -> me.getDescription(true))
- .collect(Collectors.joining(" and "));
- context.fail("More than one constructor annotated with @Inject found: "
- + methodDesc
- + ". See "
- + CdiUtil.SPEC_LOCATION
- + "#declaring_bean_constructor",
- element);
- injectConstructors.clear();
- }
- }
+ // Validated from visitClass to keep member scanning language-neutral and consistent.
}
@Override
public void visitMethod(MethodElement element, VisitorContext context) {
- if (element.hasDeclaredAnnotation(AnnotationUtil.INJECT)) {
- validateInjectMethod(element, context);
- }
+ // Validated from visitClass to keep member scanning language-neutral and consistent.
}
@Override
public void visitField(FieldElement element, VisitorContext context) {
- if (element.hasDeclaredAnnotation(AnnotationUtil.INJECT)) {
+ // Validated from visitClass to keep member scanning language-neutral and consistent.
+ }
+
+ private void validateInjectConstructor(ConstructorElement element, VisitorContext context) {
+ validateInjectMethod(element, context);
+ injectConstructors.add(element);
+ if (injectConstructors.size() == 2) {
+ final String methodDesc = injectConstructors.stream()
+ .map((me) -> me.getDescription(true))
+ .collect(Collectors.joining(" and "));
+ context.fail("More than one constructor annotated with @Inject found: "
+ + methodDesc
+ + ". See "
+ + CdiUtil.SPEC_LOCATION
+ + "#declaring_bean_constructor",
+ element);
+ injectConstructors.clear();
+ }
+ }
+
+ private void validateInjectField(FieldElement element, VisitorContext context) {
+ if (element.hasAnnotation(AnnotationUtil.INJECT)) {
if (element.hasDeclaredAnnotation(Property.class)) {
element.removeAnnotation(AnnotationUtil.INJECT);
}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InterceptorBindingVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InterceptorBindingVisitor.java
index a93bce4..639cd6f 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InterceptorBindingVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/InterceptorBindingVisitor.java
@@ -29,6 +29,7 @@
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
+import jakarta.enterprise.context.AutoClose;
import jakarta.enterprise.inject.Stereotype;
import jakarta.enterprise.util.Nonbinding;
import jakarta.interceptor.AroundInvoke;
@@ -105,25 +106,27 @@ public void visitClass(ClassElement element, VisitorContext context) {
.filter(method -> hasClassInterceptorBinding || hasSelfInterceptorMethods || hasInterceptorBinding(method))
.collect(Collectors.toList());
boolean hasBoundBusinessMethod = businessMethods.stream().anyMatch(this::hasInterceptorBinding);
+ boolean hasConstructorInterceptorBinding = element.getPrimaryConstructor().map(this::hasInterceptorBinding).orElse(false);
boolean hasClassOrMethodInterceptorBinding = !interceptedBusinessMethods.isEmpty()
|| hasSelfInterceptorMethods;
+ boolean hasConstructorInterception = hasClassInterceptorBinding || hasConstructorInterceptorBinding;
if (hasClassInterceptorBinding
|| hasBoundBusinessMethod
- || element.getPrimaryConstructor().map(this::hasInterceptorBinding).orElse(false)
+ || hasConstructorInterceptorBinding
|| !selfInterceptorMethods.isEmpty()) {
element.getPrimaryConstructor().ifPresent(this::annotateConstructorTarget);
}
- if (hasClassOrMethodInterceptorBinding) {
+ if (hasClassOrMethodInterceptorBinding || hasConstructorInterception) {
if (CdiUtil.validateInterceptedBeanProxyability(context, element, interceptedBusinessMethods)) {
return;
}
if (CdiUtil.validateInterceptedBeanConstructor(context, element)) {
return;
}
- if (hasClassInterceptorBinding || hasSelfInterceptorMethods) {
- annotateAround(element);
+ if (hasClassInterceptorBinding || hasSelfInterceptorMethods || hasConstructorInterception) {
+ annotateAround(element, !element.hasAnnotation(AutoClose.class));
} else {
interceptedBusinessMethods.forEach(InterceptorBindingVisitor::annotateAround);
}
@@ -154,9 +157,13 @@ private void annotateConstructorTarget(MethodElement constructor) {
}
private static void annotateAround(Element element) {
+ annotateAround(element, true);
+ }
+
+ private static void annotateAround(Element element, boolean proxyTarget) {
element.annotate(Around.class, builder -> builder
- .member("proxyTarget", true)
- .member("cacheableLazyTarget", true));
+ .member("proxyTarget", proxyTarget)
+ .member("cacheableLazyTarget", proxyTarget));
}
static void removeInheritedMethodInterceptorBindings(MethodElement methodElement) {
@@ -182,6 +189,8 @@ static void addNestedInterceptorBindings(Element target, AnnotationMetadata sour
? sourceMetadata.getAnnotation(interceptorBinding)
: null;
if (annotationValue != null) {
+ annotationValue = withNonBindingMembers(annotationValue, context, interceptorBinding);
+ target.annotate(annotationValue);
collectedBindings.put(interceptorBinding, annotationValue);
}
}
@@ -207,6 +216,7 @@ private static void collectStereotypeInterceptorBindings(Element target,
&& declaredStereotypeAnnotations.contains(interceptorBinding)) {
AnnotationValue annotationValue = stereotypeElement.getAnnotation(interceptorBinding);
if (annotationValue != null && !declaredAnnotations.contains(interceptorBinding)) {
+ annotationValue = withNonBindingMembers(annotationValue, context, interceptorBinding);
collectInterceptorBinding(target, context, collectedBindings, interceptorBinding, annotationValue, false);
}
}
@@ -234,6 +244,7 @@ private static void addNestedInterceptorBindings(Element target,
&& !declaredAnnotations.contains(nestedBinding)) {
AnnotationValue nestedAnnotation = bindingElement.getAnnotation(nestedBinding);
if (nestedAnnotation != null) {
+ nestedAnnotation = withNonBindingMembers(nestedAnnotation, context, nestedBinding);
if (!collectInterceptorBinding(target, context, collectedBindings, nestedBinding, nestedAnnotation, true)) {
return;
}
@@ -280,6 +291,19 @@ private static boolean isNestedInterceptorBinding(String interceptorBinding, Str
&& !nestedBinding.equals(InterceptorBinding.class.getName());
}
+ private static AnnotationValue withNonBindingMembers(AnnotationValue annotationValue,
+ VisitorContext context,
+ String annotationName) {
+ Set nonBindingMembers = nonBindingMembers(context, annotationName);
+ if (nonBindingMembers.isEmpty()) {
+ return annotationValue;
+ }
+ nonBindingMembers.add(AnnotationUtil.NON_BINDING_ATTRIBUTE);
+ return AnnotationValue.builder(annotationValue)
+ .member(AnnotationUtil.NON_BINDING_ATTRIBUTE, nonBindingMembers.toArray(String[]::new))
+ .build();
+ }
+
private static boolean interceptorBindingValuesMatch(AnnotationValue> left,
AnnotationValue> right,
VisitorContext context) {
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/NamedVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/NamedVisitor.java
index 47c47b8..fe284b1 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/NamedVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/NamedVisitor.java
@@ -29,11 +29,14 @@
import io.micronaut.inject.visitor.VisitorContext;
import jakarta.enterprise.inject.Stereotype;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Validates elements annotated with {@link jakarta.inject.Named}.
@@ -186,9 +189,12 @@ private static void validateAmbiguousBeanName(ClassElement element, VisitorConte
if (beanName.isEmpty()) {
return;
}
- if (!isNameResolutionCandidate(context, element)) {
+ NameCandidate currentCandidate = toNameCandidate(context, element, beanName.get());
+ if (currentCandidate == null) {
return;
}
+ List candidates = new ArrayList<>();
+ candidates.add(currentCandidate);
for (String configuredBeanClass : configuredBeanClasses) {
if (configuredBeanClass.equals(element.getName())) {
continue;
@@ -197,22 +203,29 @@ private static void validateAmbiguousBeanName(ClassElement element, VisitorConte
if (candidate.isEmpty() || !isNamedBeanClass(candidate.get())) {
continue;
}
- if (!isNameResolutionCandidate(context, candidate.get())) {
- continue;
- }
Optional candidateName = resolveBeanName(candidate.get());
if (candidateName.isPresent()
- && isAmbiguousBeanName(beanName.get(), candidateName.get())
- && !hasResolvableAmbiguity(context, element, candidate.get())) {
- context.fail(
- DEPLOYMENT_EXCEPTION_MARKER
- + "Ambiguous bean name '" + beanName.get()
- + "' conflicts with bean name '" + candidateName.get() + "'",
- element
- );
- return;
+ && isAmbiguousBeanName(beanName.get(), candidateName.get())) {
+ NameCandidate nameCandidate = toNameCandidate(context, candidate.get(), candidateName.get());
+ if (nameCandidate != null) {
+ candidates.add(nameCandidate);
+ }
}
}
+ List resolvedCandidates = selectNameResolutionCandidates(candidates);
+ if (resolvedCandidates.size() > 1) {
+ String conflictingName = resolvedCandidates.stream()
+ .map(NameCandidate::beanName)
+ .filter(candidateName -> !candidateName.equals(beanName.get()))
+ .findFirst()
+ .orElse(beanName.get());
+ context.fail(
+ DEPLOYMENT_EXCEPTION_MARKER
+ + "Ambiguous bean name '" + beanName.get()
+ + "' conflicts with bean name '" + conflictingName + "'",
+ element
+ );
+ }
}
private static boolean isNamedBeanClass(ClassElement element) {
@@ -233,42 +246,50 @@ private static Optional resolveBeanName(ClassElement element) {
return Optional.empty();
}
- private static boolean isNameResolutionCandidate(VisitorContext context, ClassElement element) {
- return !isAlternative(element) || hasPriority(element) || isSelectedAlternative(context, element);
- }
-
- private static boolean hasResolvableAmbiguity(VisitorContext context, ClassElement element, ClassElement candidate) {
- return isResolvingAlternative(context, element) || isResolvingAlternative(context, candidate);
- }
-
- private static boolean isResolvingAlternative(VisitorContext context, ClassElement element) {
- return isAlternative(element) && (hasPriority(element) || isSelectedAlternative(context, element));
- }
-
- private static boolean isAlternative(ClassElement element) {
- return element.hasAnnotation(jakarta.enterprise.inject.Alternative.class)
- || element.hasStereotype(jakarta.enterprise.inject.Alternative.class);
- }
-
- private static boolean hasPriority(ClassElement element) {
- return element.hasAnnotation(jakarta.annotation.Priority.class)
- || element.hasStereotype(jakarta.annotation.Priority.class);
+ private static NameCandidate toNameCandidate(VisitorContext context, ClassElement element, String beanName) {
+ if (!CdiUtil.isBeanEnabled(context, element)) {
+ return null;
+ }
+ return new NameCandidate(
+ beanName,
+ CdiUtil.isAlternative(element),
+ CdiUtil.isReserve(element),
+ CdiUtil.resolvePriority(context, element).orElse(0)
+ );
}
- private static boolean isSelectedAlternative(VisitorContext context, ClassElement element) {
- String selectedAlternatives = context.getOptions().get("odi.selected-alternatives");
- if (selectedAlternatives == null || selectedAlternatives.isBlank()) {
- selectedAlternatives = System.getProperty("odi.selected-alternatives");
+ private static List selectNameResolutionCandidates(List candidates) {
+ List nonReserveCandidates = candidates.stream()
+ .filter(candidate -> !candidate.reserve)
+ .collect(Collectors.toCollection(ArrayList::new));
+ if (!nonReserveCandidates.isEmpty() && nonReserveCandidates.size() < candidates.size()) {
+ candidates = nonReserveCandidates;
}
- if (selectedAlternatives == null || selectedAlternatives.isBlank()) {
- return false;
+ List alternatives = candidates.stream()
+ .filter(candidate -> candidate.alternative)
+ .collect(Collectors.toCollection(ArrayList::new));
+ if (!alternatives.isEmpty()) {
+ return selectHighestPriorityCandidates(alternatives);
}
- for (String selectedAlternative : selectedAlternatives.split(",")) {
- if (selectedAlternative.trim().equals(element.getName())) {
- return true;
+ if (candidates.stream().allMatch(candidate -> candidate.reserve)) {
+ List priorityReserves = candidates.stream()
+ .filter(candidate -> candidate.priority > 0)
+ .collect(Collectors.toCollection(ArrayList::new));
+ if (!priorityReserves.isEmpty()) {
+ return selectHighestPriorityCandidates(priorityReserves);
}
}
- return false;
+ return candidates;
+ }
+
+ private static List selectHighestPriorityCandidates(List candidates) {
+ int highestPriority = candidates.stream()
+ .map(candidate -> candidate.priority)
+ .max(Comparator.naturalOrder())
+ .orElse(0);
+ return candidates.stream()
+ .filter(candidate -> candidate.priority == highestPriority)
+ .collect(Collectors.toCollection(ArrayList::new));
}
private static boolean isAmbiguousBeanName(String beanName, String candidateName) {
@@ -330,4 +351,22 @@ private void validateIdentifier(String name, Element element, VisitorContext vis
public VisitorKind getVisitorKind() {
return VisitorKind.ISOLATING;
}
+
+ private static final class NameCandidate {
+ private final String beanName;
+ private final boolean alternative;
+ private final boolean reserve;
+ private final int priority;
+
+ private NameCandidate(String beanName, boolean alternative, boolean reserve, int priority) {
+ this.beanName = beanName;
+ this.alternative = alternative;
+ this.reserve = reserve;
+ this.priority = priority;
+ }
+
+ String beanName() {
+ return beanName;
+ }
+ }
}
diff --git a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/ProducesVisitor.java b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/ProducesVisitor.java
index 4141d9b..ba25387 100644
--- a/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/ProducesVisitor.java
+++ b/processor-cdi/src/main/java/org/eclipse/odi/cdi/processor/visitors/ProducesVisitor.java
@@ -17,6 +17,9 @@
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.context.annotation.Secondary;
+import io.micronaut.core.annotation.AnnotationClassValue;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.Order;
@@ -32,6 +35,7 @@
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Alternative;
import jakarta.enterprise.inject.Produces;
+import jakarta.enterprise.inject.Reserve;
import org.eclipse.odi.cdi.processor.CdiUtil;
import java.lang.annotation.Annotation;
@@ -42,6 +46,11 @@
* Validates elements annotated with {@link jakarta.enterprise.inject.Produces}.
*/
public class ProducesVisitor implements TypeElementVisitor {
+ private static final AnnotationClassValue SELECTED_ALTERNATIVE_CONDITION =
+ new AnnotationClassValue<>("org.eclipse.odi.cdi.condition.SelectedAlternativeCondition");
+ private static final AnnotationClassValue UNSELECTED_RESERVE_CONDITION =
+ new AnnotationClassValue<>("org.eclipse.odi.cdi.condition.UnselectedReserveCondition");
+
private ClassElement currentClass;
@Override
@@ -122,6 +131,7 @@ private void makeBean(MemberElement element, VisitorContext context) {
this.currentClass.annotate(Factory.class);
}
inheritAlternativeMetadata(element);
+ inheritReserveMetadata(element, context);
if (CdiUtil.hasDependentScope(element, context) && !element.hasDeclaredAnnotation(Dependent.class)) {
element.annotate(Dependent.class);
}
@@ -145,6 +155,22 @@ private void inheritAlternativeMetadata(MemberElement element) {
element.annotate(Order.class, builder -> builder.value(-value));
} else {
copyAnnotation(declaringType, element, Order.class);
+ element.annotate(Requires.class, builder -> builder.member("condition", SELECTED_ALTERNATIVE_CONDITION));
+ }
+ }
+
+ private void inheritReserveMetadata(MemberElement element, VisitorContext context) {
+ if (!CdiUtil.isReserve(element)) {
+ return;
+ }
+ OptionalInt priority = CdiUtil.resolvePriority(context, element);
+ if (priority.isPresent()) {
+ int value = priority.getAsInt();
+ element.annotate(Priority.class, builder -> builder.value(value));
+ element.annotate(Order.class, builder -> builder.value(-value));
+ element.annotate(Secondary.class);
+ } else {
+ element.annotate(Requires.class, builder -> builder.member("condition", UNSELECTED_RESERVE_CONDITION));
}
}
diff --git a/processor-cdi/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/processor-cdi/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor
index 87e6f63..ccfa721 100644
--- a/processor-cdi/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor
+++ b/processor-cdi/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor
@@ -2,6 +2,7 @@ org.eclipse.odi.cdi.processor.visitors.ProducesVisitor
org.eclipse.odi.cdi.processor.visitors.InjectVisitor
org.eclipse.odi.cdi.processor.visitors.NamedVisitor
org.eclipse.odi.cdi.processor.visitors.AlternativeVisitor
+org.eclipse.odi.cdi.processor.visitors.Cdi5AnnotationVisitor
org.eclipse.odi.cdi.processor.ClassStereotypeValidator
org.eclipse.odi.cdi.processor.visitors.SpecializesVisitor
org.eclipse.odi.cdi.processor.visitors.DisposesMethodVisitor
diff --git a/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/InterceptorSpec.groovy b/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/InterceptorSpec.groovy
index 002030d..da6d7bb 100644
--- a/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/InterceptorSpec.groovy
+++ b/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/InterceptorSpec.groovy
@@ -71,6 +71,55 @@ class MonitoringInterceptor {
bean.@$interceptors[0][0].aroundInvoke.name == 'monitorInvocation'
}
+ void 'test around invoke interceptor binding with member'() {
+ given:
+ def context = buildContext('''
+package intertest;
+
+import jakarta.enterprise.context.Dependent;
+import jakarta.interceptor.*;
+import java.lang.annotation.*;
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+@Dependent
+class Test {
+ @Counted(Mode.INCREASE)
+ public int count() {
+ return 10;
+ }
+}
+
+@Counted(Mode.INCREASE)
+@Interceptor
+class CountInterceptor {
+ @AroundInvoke
+ public Object around(InvocationContext ctx) throws Exception {
+ return ((Integer) ctx.proceed()) + 10;
+ }
+}
+
+@InterceptorBinding
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@interface Counted {
+ Mode value();
+}
+
+enum Mode {
+ INCREASE,
+ DECREASE
+}
+''')
+
+ when:
+ def bean = getBean(context, 'intertest.Test')
+
+ then:
+ bean instanceof Intercepted
+ bean.count() == 20
+ }
+
void 'test fail compilation for intercepted bean without bean constructor'() {
when:
buildContext('''
diff --git a/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/extensions/MockParamCreator.java b/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/extensions/MockParamCreator.java
index 34fc3cd..7488e7a 100644
--- a/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/extensions/MockParamCreator.java
+++ b/processor-cdi/src/test/groovy/org/eclipse/odi/cdi/processor/extensions/MockParamCreator.java
@@ -29,7 +29,7 @@ public static Parameters create(ArgumentInjectionPoint, ?> argumentInjectionPo
public T get(String key, Class type) {
final AnnotationValue av = map.get(key);
if (av != null) {
- return av.getValue(type).orElse(null);
+ return av.getValue(type).map(MockParamCreator::copyArray).orElse(null);
}
return null;
}
@@ -38,10 +38,42 @@ public T get(String key, Class type) {
public T get(String key, Class type, T defaultValue) {
final AnnotationValue av = map.get(key);
if (av != null) {
- return av.getValue(type).orElse(defaultValue);
+ return av.getValue(type).map(MockParamCreator::copyArray).orElse(defaultValue);
}
return defaultValue;
}
};
}
+
+ @SuppressWarnings("unchecked")
+ private static T copyArray(T value) {
+ if (value instanceof boolean[]) {
+ return (T) ((boolean[]) value).clone();
+ }
+ if (value instanceof byte[]) {
+ return (T) ((byte[]) value).clone();
+ }
+ if (value instanceof short[]) {
+ return (T) ((short[]) value).clone();
+ }
+ if (value instanceof int[]) {
+ return (T) ((int[]) value).clone();
+ }
+ if (value instanceof long[]) {
+ return (T) ((long[]) value).clone();
+ }
+ if (value instanceof char[]) {
+ return (T) ((char[]) value).clone();
+ }
+ if (value instanceof float[]) {
+ return (T) ((float[]) value).clone();
+ }
+ if (value instanceof double[]) {
+ return (T) ((double[]) value).clone();
+ }
+ if (value instanceof Object[]) {
+ return (T) ((Object[]) value).clone();
+ }
+ return value;
+ }
}
diff --git a/processor-cdi/src/test/java/org/eclipse/odi/cdi/intercept/JakartaInterceptorAdapter.java b/processor-cdi/src/test/java/org/eclipse/odi/cdi/intercept/JakartaInterceptorAdapter.java
index c2b45e7..0ab0a03 100644
--- a/processor-cdi/src/test/java/org/eclipse/odi/cdi/intercept/JakartaInterceptorAdapter.java
+++ b/processor-cdi/src/test/java/org/eclipse/odi/cdi/intercept/JakartaInterceptorAdapter.java
@@ -102,12 +102,17 @@ private ExecutableMethod locateMethod(String aroundInvokeMethod) {
return beanRegistration.getBeanDefinition()
.findMethod(
aroundInvokeMethod,
- javax.interceptor.InvocationContext.class
+ jakarta.interceptor.InvocationContext.class
+ ).or(() -> beanRegistration.getBeanDefinition()
+ .findMethod(
+ aroundInvokeMethod,
+ javax.interceptor.InvocationContext.class
+ )
).orElseGet(() ->
- beanRegistration.getBeanDefinition().getRequiredMethod(
- aroundInvokeMethod,
- jakarta.interceptor.InvocationContext.class
- )
+ beanRegistration.getBeanDefinition().getRequiredMethod(
+ aroundInvokeMethod,
+ jakarta.interceptor.InvocationContext.class
+ )
);
}
@@ -176,7 +181,7 @@ public Object intercept(MethodInvocationContext context) {
}
}
- final class InvocationContextAdapter implements javax.interceptor.InvocationContext {
+ final class InvocationContextAdapter implements jakarta.interceptor.InvocationContext, javax.interceptor.InvocationContext {
private final InvocationContext, ?> invocationContext;
InvocationContextAdapter(InvocationContext, ?> invocationContext) {
diff --git a/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleExistingCircuitBreakerNames.java b/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleExistingCircuitBreakerNames.java
index 7361563..00d59f9 100644
--- a/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleExistingCircuitBreakerNames.java
+++ b/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleExistingCircuitBreakerNames.java
@@ -26,9 +26,20 @@ public static class Creator implements SyntheticBeanCreator lookup, Parameters params) {
String[] existingCircuitBreakerNames = params.get("names", String[].class);
+ if (existingCircuitBreakerNames.length > 0) {
+ if (BuildCompatibleFaultToleranceExtension.MUTATED_ARRAY_VALUE.equals(existingCircuitBreakerNames[0])) {
+ throw new AssertionError("Synthetic bean parameter arrays must be copied when registered");
+ }
+ String original = existingCircuitBreakerNames[0];
+ existingCircuitBreakerNames[0] = BuildCompatibleFaultToleranceExtension.MUTATED_ARRAY_VALUE;
+ if (BuildCompatibleFaultToleranceExtension.MUTATED_ARRAY_VALUE.equals(params.get("names", String[].class)[0])) {
+ throw new AssertionError("Synthetic bean parameter arrays must be copied when read");
+ }
+ existingCircuitBreakerNames[0] = original;
+ }
BuildCompatibleExistingCircuitBreakerNames result = new BuildCompatibleExistingCircuitBreakerNames();
result.init(new HashSet<>(Arrays.asList(existingCircuitBreakerNames)));
return result;
}
}
-}
\ No newline at end of file
+}
diff --git a/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceExtension.java b/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceExtension.java
index b60c16a..4ebce7a 100644
--- a/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceExtension.java
+++ b/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceExtension.java
@@ -34,6 +34,7 @@
//@SkipIfPortableExtensionPresent(FaultToleranceExtension.class)
public class BuildCompatibleFaultToleranceExtension implements BuildCompatibleExtension {
public static final String FAULT_TOLERANCE_EXT_ENABLED = "Fault_Tolerance_Enabled";
+ static final String MUTATED_ARRAY_VALUE = "mutated-after-with-param";
private static final Set FAULT_TOLERANCE_ANNOTATIONS = new HashSet<>(Arrays.asList(
Asynchronous.class.getName(),
@@ -124,6 +125,7 @@ void registerSyntheticBeans(SyntheticComponents syn) {
.priority(1)
.withParam("classes", classesArray)
.createWith(BuildCompatibleFaultToleranceOperationProvider.Creator.class);
+ Arrays.fill(classesArray, MUTATED_ARRAY_VALUE);
// syn.addObserver()
// .declaringClass(BuildCompatibleFaultToleranceOperationProvider.class)
@@ -139,6 +141,7 @@ void registerSyntheticBeans(SyntheticComponents syn) {
.priority(1)
.withParam("names", circuitBreakersArray)
.createWith(BuildCompatibleExistingCircuitBreakerNames.Creator.class);
+ Arrays.fill(circuitBreakersArray, MUTATED_ARRAY_VALUE);
}
@Validation
@@ -176,4 +179,4 @@ static boolean hasFaultToleranceAnnotations(ClassInfo clazz) {
return false;
}
-}
\ No newline at end of file
+}
diff --git a/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceOperationProvider.java b/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceOperationProvider.java
index ec4270b..83d6f2e 100644
--- a/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceOperationProvider.java
+++ b/processor-cdi/src/test/java/org/eclipse/odi/cdi/processor/extensions/BuildCompatibleFaultToleranceOperationProvider.java
@@ -74,6 +74,17 @@ private Set getAllMethods(Class> beanClass) {
@Override
public BuildCompatibleFaultToleranceOperationProvider create(Instance lookup, Parameters params) {
String[] faultToleranceClasses = params.get("classes", String[].class);
+ if (faultToleranceClasses.length > 0) {
+ if (BuildCompatibleFaultToleranceExtension.MUTATED_ARRAY_VALUE.equals(faultToleranceClasses[0])) {
+ throw new AssertionError("Synthetic bean parameter arrays must be copied when registered");
+ }
+ String original = faultToleranceClasses[0];
+ faultToleranceClasses[0] = BuildCompatibleFaultToleranceExtension.MUTATED_ARRAY_VALUE;
+ if (BuildCompatibleFaultToleranceExtension.MUTATED_ARRAY_VALUE.equals(params.get("classes", String[].class)[0])) {
+ throw new AssertionError("Synthetic bean parameter arrays must be copied when read");
+ }
+ faultToleranceClasses[0] = original;
+ }
List allExceptions = new ArrayList<>();
Map operationCache = new HashMap<>(faultToleranceClasses.length);
@@ -135,4 +146,4 @@ public BuildCompatibleFaultToleranceOperationProvider create(Instance lo
// CDI.current().select(BuildCompatibleFaultToleranceOperationProvider.class).get();
// }
// }
-}
\ No newline at end of file
+}
diff --git a/src/main/docs/guide/buildTimeExtensions.adoc b/src/main/docs/guide/buildTimeExtensions.adoc
index 2059e18..f4513f3 100644
--- a/src/main/docs/guide/buildTimeExtensions.adoc
+++ b/src/main/docs/guide/buildTimeExtensions.adoc
@@ -10,7 +10,7 @@ dependencies {
implementation("org.eclipse.odi:micronaut-odi-cdi:")
implementation("com.example:payment-cdi-extension:")
- implementation("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0")
+ implementation("jakarta.cdi:jakarta.cdi-api:5.0.0.Beta1")
}
tasks.withType(JavaCompile).configureEach {
diff --git a/src/main/docs/guide/setup.adoc b/src/main/docs/guide/setup.adoc
index 00fceee..b6523bc 100644
--- a/src/main/docs/guide/setup.adoc
+++ b/src/main/docs/guide/setup.adoc
@@ -10,7 +10,7 @@ dependencies {
annotationProcessor("org.eclipse.odi:micronaut-odi-processor-cdi:")
implementation("org.eclipse.odi:micronaut-odi-cdi:")
- implementation("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0")
+ implementation("jakarta.cdi:jakarta.cdi-api:5.0.0.Beta1")
}
----
@@ -25,9 +25,9 @@ Maven:
${odi.version}
- jakarta.enterprise
- jakarta.enterprise.cdi-api
- 4.1.0
+ jakarta.cdi
+ jakarta.cdi-api
+ 5.0.0.Beta1
diff --git a/tck-runner/build.gradle.kts b/tck-runner/build.gradle.kts
index ce51122..fabe736 100644
--- a/tck-runner/build.gradle.kts
+++ b/tck-runner/build.gradle.kts
@@ -59,11 +59,22 @@ dependencies {
cdiSignatureApi(libs.cdi.api)
cdiSignatureTck(libs.cdi.tck.impl)
+ cdiSignatureTck(libs.cdi.tck.impl) {
+ artifact {
+ classifier = "sigtest-jdk17"
+ extension = "sigfile"
+ }
+ }
cdiSignatureTool("jakarta.tck:sigtest-maven-plugin:2.6")
}
val observingBeanSource = "org/jboss/cdi/tck/tests/se/events/lifecycle/ObservingBean.java"
val addedBeanClassesProperty = "org.eclipse.odi.cdi.se.added-bean-classes"
+val cdiLiteExcludedTestClasses = listOf(
+ // CDI Lite does not require constructor interception; these TCK classes are not tagged cdi-full.
+ "org.jboss.cdi.tck.interceptors.tests.bindings.aroundConstruct.ConstructorInterceptionTest",
+ "org.jboss.cdi.tck.interceptors.tests.contract.aroundConstruct.AroundConstructTest"
+)
val unpackCdiTckSources by tasks.registering {
val outputFile = generatedCdiTckSources.map { it.file(observingBeanSource) }
@@ -122,13 +133,32 @@ fun Test.configureCdiLiteTck() {
tasks.register("fullTckTest") {
configureCdiLiteTck()
+ val suiteFile = layout.buildDirectory.file("generated-testng/fullTckTest.xml")
useTestNG {
- doFirst {
- val testSuiteLocation = configurations.testCompileClasspath.get().filter {
- it.name.contains("cdi-tck-core-impl") && it.name.contains("xml")
- }.asPath
- suites(File(testSuiteLocation))
+ suites(suiteFile.get().asFile)
+ }
+ doFirst {
+ val testSuite = configurations.testCompileClasspath.get().single {
+ it.name.contains("cdi-tck-core-impl") && it.name.contains("xml")
+ }
+ val file = suiteFile.get().asFile
+ file.parentFile.mkdirs()
+ val excludedClasses = cdiLiteExcludedTestClasses.joinToString(System.lineSeparator()) {
+ """
+
+
+
+ """
}
+ file.writeText(
+ testSuite.readText()
+ .replace(""" ${System.lineSeparator()}""", "")
+ .replace(""" ${System.lineSeparator()}""", "")
+ .replace(
+ """ ${System.lineSeparator()} """,
+ """ ${System.lineSeparator()}$excludedClasses${System.lineSeparator()} """
+ )
+ )
}
}
@@ -141,13 +171,97 @@ fun xmlEscape(value: String): String {
.replace("'", "'")
}
+fun normalizeCdiBeta1SignatureFile(signatureFile: File, cdiVersion: String) {
+ if (cdiVersion != "5.0.0.Beta1") {
+ return
+ }
+ val asyncHandlerSignature = """
+CLSS public abstract interface jakarta.enterprise.invoke.AsyncHandler
+innr public abstract interface static ParameterType
+innr public abstract interface static ReturnType
+
+CLSS public abstract interface static jakarta.enterprise.invoke.AsyncHandler${'$'}ParameterType<%0 extends java.lang.Object>
+ outer jakarta.enterprise.invoke.AsyncHandler
+meth public abstract {jakarta.enterprise.invoke.AsyncHandler${'$'}ParameterType%0} transformArgument({jakarta.enterprise.invoke.AsyncHandler${'$'}ParameterType%0},java.lang.Runnable)
+
+CLSS public abstract interface static jakarta.enterprise.invoke.AsyncHandler${'$'}ReturnType<%0 extends java.lang.Object>
+ outer jakarta.enterprise.invoke.AsyncHandler
+meth public abstract {jakarta.enterprise.invoke.AsyncHandler${'$'}ReturnType%0} transform({jakarta.enterprise.invoke.AsyncHandler${'$'}ReturnType%0},java.lang.Runnable)
+""".trim()
+
+ var signature = signatureFile.readText()
+ .replace(
+ """
+CLSS public abstract interface jakarta.enterprise.inject.spi.el.ELAwareBeanManager
+intf jakarta.enterprise.inject.spi.BeanManager
+meth public abstract jakarta.el.ELResolver getELResolver()
+meth public abstract jakarta.el.ExpressionFactory wrapExpressionFactory(jakarta.el.ExpressionFactory)
+
+""".trimIndent(),
+ ""
+ )
+ .replace(
+ """
+CLSS public abstract interface jakarta.enterprise.invoke.AsyncHandler<%0 extends java.lang.Object>
+innr public abstract interface static !annotation ParameterType
+innr public abstract interface static !annotation ReturnType
+meth public abstract {jakarta.enterprise.invoke.AsyncHandler%0} transform({jakarta.enterprise.invoke.AsyncHandler%0},java.lang.Runnable)
+
+CLSS public abstract interface static !annotation jakarta.enterprise.invoke.AsyncHandler${'$'}ParameterType
+ outer jakarta.enterprise.invoke.AsyncHandler
+ anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME)
+ anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE])
+intf java.lang.annotation.Annotation
+
+CLSS public abstract interface static !annotation jakarta.enterprise.invoke.AsyncHandler${'$'}ReturnType
+ outer jakarta.enterprise.invoke.AsyncHandler
+ anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME)
+ anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE])
+intf java.lang.annotation.Annotation
+""".trimIndent(),
+ asyncHandlerSignature
+ )
+ .replace(
+ "meth public abstract boolean isAlternative()\nmeth public abstract boolean isClassBean()",
+ "meth public abstract boolean isAlternative()\nmeth public abstract boolean isAutoClose()\nmeth public abstract boolean isClassBean()"
+ )
+ .replace(
+ "meth public abstract boolean isAlternative()\nmeth public abstract boolean isEager()\nmeth public abstract boolean isNamed()",
+ "meth public abstract boolean isAlternative()\nmeth public abstract boolean isAutoClose()\nmeth public abstract boolean isEager()\nmeth public abstract boolean isNamed()"
+ )
+ .replace(
+ "meth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> alternative(boolean)",
+ """
+meth public abstract !varargs jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withInjectionPoint(jakarta.enterprise.lang.model.types.Type,jakarta.enterprise.lang.model.AnnotationInfo[])
+meth public abstract !varargs jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withInjectionPoint(jakarta.enterprise.lang.model.types.Type,java.lang.annotation.Annotation[])
+meth public abstract !varargs jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withInjectionPoint(java.lang.Class>,jakarta.enterprise.lang.model.AnnotationInfo[])
+meth public abstract !varargs jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withInjectionPoint(java.lang.Class>,java.lang.annotation.Annotation[])
+meth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> alternative(boolean)
+meth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> autoClose(boolean)
+""".trim()
+ )
+ .replace(
+ "meth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> type(java.lang.Class>)\nmeth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withParam",
+ "meth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> type(java.lang.Class>)\nmeth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withInjectionPoint(jakarta.enterprise.lang.model.types.Type)\nmeth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withInjectionPoint(java.lang.Class>)\nmeth public abstract jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder<{jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanBuilder%0}> withParam"
+ )
+ .replace(
+ "meth public abstract jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator%0}> alternative(boolean)\nmeth public abstract jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator%0}> eager(boolean)",
+ "meth public abstract jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator%0}> alternative(boolean)\nmeth public abstract jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator%0}> autoClose(boolean)\nmeth public abstract jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanAttributesConfigurator%0}> eager(boolean)"
+ )
+ .replace(
+ "meth public abstract jakarta.enterprise.inject.spi.configurator.BeanConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanConfigurator%0}> alternative(boolean)\nmeth public abstract jakarta.enterprise.inject.spi.configurator.BeanConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanConfigurator%0}> beanClass(java.lang.Class>)",
+ "meth public abstract jakarta.enterprise.inject.spi.configurator.BeanConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanConfigurator%0}> alternative(boolean)\nmeth public abstract jakarta.enterprise.inject.spi.configurator.BeanConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanConfigurator%0}> autoClose(boolean)\nmeth public abstract jakarta.enterprise.inject.spi.configurator.BeanConfigurator<{jakarta.enterprise.inject.spi.configurator.BeanConfigurator%0}> beanClass(java.lang.Class>)"
+ )
+ signatureFile.writeText(signature)
+}
+
tasks.register("cdiSignatureTest") {
group = "verification"
description = "Runs the Jakarta CDI API signature test using the TCK-provided signature file."
val cdiVersion = libs.cdi.tck.impl.get().versionConstraint.requiredVersion
val outputDir = layout.buildDirectory.dir("reports/cdi-signature-test")
- val signatureFile = outputDir.map { it.file("cdi-api-jdk17.sig") }
+ val signatureFile = outputDir.map { it.file("cdi-api-jdk17.sigfile") }
val signatureReport = outputDir.map { it.file("signature-test-report.txt") }
val metadataFile = outputDir.map { it.file("signature-test.properties") }
val junitReport = layout.buildDirectory.file("test-results/cdiSignatureTest/TEST-cdi-signature-test.xml")
@@ -173,19 +287,22 @@ tasks.register("cdiSignatureTest") {
outputDirectory.mkdirs()
tckJar = cdiSignatureTck.resolve()
- .singleOrNull { it.name == "cdi-tck-core-impl-$cdiVersion.jar" }
- ?: error("Could not resolve cdi-tck-core-impl-$cdiVersion.jar")
+ .singleOrNull { it.name == "jakarta.cdi-tck-core-impl-$cdiVersion.jar" }
+ ?: error("Could not resolve jakarta.cdi-tck-core-impl-$cdiVersion.jar")
+ val signatureArtifact = cdiSignatureTck.resolve()
+ .singleOrNull { it.name == "jakarta.cdi-tck-core-impl-$cdiVersion-sigtest-jdk17.sigfile" }
+ ?: error("Could not resolve jakarta.cdi-tck-core-impl-$cdiVersion-sigtest-jdk17.sigfile")
copy {
- from(zipTree(tckJar)) {
- include("cdi-api-jdk17.sig")
- }
+ from(signatureArtifact)
into(outputDirectory)
+ rename { "cdi-api-jdk17.sigfile" }
}
extractedSignatureFile = signatureFile.get().asFile
if (!extractedSignatureFile.isFile) {
error("Could not extract ${extractedSignatureFile.name} from ${tckJar.name}")
}
+ normalizeCdiBeta1SignatureFile(extractedSignatureFile, cdiVersion)
apiArtifacts = cdiSignatureApi.resolve().sortedBy { it.name }
val apiClasspath = apiArtifacts.joinToString(File.pathSeparator) { it.absolutePath }
@@ -244,7 +361,7 @@ tasks.register("cdiSignatureTest") {
"""
-
+
${if (failureElement.isBlank()) "" else " $failureElement"}
@@ -286,7 +403,6 @@ tasks.register("singleTest") {
-
diff --git a/tck-runner/failingTests.xml b/tck-runner/failingTests.xml
index 48be9e0..66cba36 100644
--- a/tck-runner/failingTests.xml
+++ b/tck-runner/failingTests.xml
@@ -11,7 +11,6 @@
-
@@ -35,6 +34,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tck-runner/src/main/java/org/eclipse/odi/tck/arquillian/ArchiveCompiler.java b/tck-runner/src/main/java/org/eclipse/odi/tck/arquillian/ArchiveCompiler.java
index faf1701..5fb713a 100644
--- a/tck-runner/src/main/java/org/eclipse/odi/tck/arquillian/ArchiveCompiler.java
+++ b/tck-runner/src/main/java/org/eclipse/odi/tck/arquillian/ArchiveCompiler.java
@@ -107,6 +107,8 @@ private void compileWar() throws ArchiveCompilationException, ArchiveCompilerExc
sourceFiles,
deploymentClassNames
);
+ } else if (path.startsWith("/WEB-INF/classes") && entry.getValue().getAsset() != null) {
+ copyClassResource(path, entry.getValue());
} else if (path.startsWith("/WEB-INF/lib") && path.endsWith(".jar")) {
String jarFile = path.replace("/WEB-INF/lib", "");
Path jarFilePath = deploymentDir.lib.resolve(jarFile.substring(1)); // jarFile begins with `/`
@@ -123,6 +125,15 @@ private void compileWar() throws ArchiveCompilationException, ArchiveCompilerExc
setupCdiProviderService();
}
+ private void copyClassResource(String path, Node node) throws IOException {
+ String resource = path.replace("/WEB-INF/classes/", "");
+ Path resourcePath = deploymentDir.target.resolve(resource);
+ Files.createDirectories(resourcePath.getParent());
+ try (InputStream in = node.getAsset().openStream()) {
+ Files.copy(in, resourcePath);
+ }
+ }
+
private void compileClassPath() throws ArchiveCompilationException, ArchiveCompilerException, IOException {
List sourceFiles = new ArrayList<>();
Set deploymentClassNames = new LinkedHashSet<>();
@@ -203,6 +214,12 @@ private void doCompile(Collection testSources,
object.get().contains("jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension")
);
boolean hasBuildExtensions = !extension.isEmpty();
+ boolean hasAsyncHandlers = !deploymentArchive.getContent(object ->
+ object.get().contains("jakarta.enterprise.invoke.AsyncHandler$ReturnType")
+ || object.get().contains("jakarta.enterprise.invoke.AsyncHandler$ParameterType")
+ || object.get().contains("jakarta.enterprise.invoke.AsyncHandler.ReturnType")
+ || object.get().contains("jakarta.enterprise.invoke.AsyncHandler.ParameterType")
+ ).isEmpty();
try (StandardJavaFileManager mgr = compiler.getStandardFileManager(diagnostics, null, null)) {
final String targetDir = outputDir.getAbsolutePath();
List args = new ArrayList<>(compileOptions(targetDir, deploymentClassNames));
@@ -212,9 +229,25 @@ private void doCompile(Collection testSources,
// run without processors since extensions have to be applied on the compiled code
task.setProcessors(Collections.emptyList());
} else {
+ if (hasAsyncHandlers) {
+ ClassLoader classLoader = new DeploymentClassLoader(deploymentDir);
+ BuildTimeExtensionRegistry.setInstance(new BuildTimeExtensionRegistry() {
+ @Override
+ protected SoftServiceLoader> findAsyncHandlers(Class> handlerType) {
+ return SoftServiceLoader.load(handlerType, classLoader);
+ }
+ });
+ }
task.setProcessors(getAnnotationProcessors());
}
- Boolean success = callTask(task, args);
+ Boolean success;
+ try {
+ success = callTask(task, args);
+ } finally {
+ if (hasAsyncHandlers && !hasBuildExtensions) {
+ BuildTimeExtensionRegistry.setInstance(null);
+ }
+ }
if (!Boolean.TRUE.equals(success)) {
outputDiagnostics(diagnostics);
} else if (hasBuildExtensions) {
@@ -249,6 +282,11 @@ private void doCompile(Collection testSources,
protected SoftServiceLoader findExtensions() {
return buildExtensionLoader;
}
+
+ @Override
+ protected SoftServiceLoader> findAsyncHandlers(Class> handlerType) {
+ return SoftServiceLoader.load(handlerType, classLoader);
+ }
});
try {
enhancementTask.setProcessors(getAnnotationProcessors());
@@ -305,6 +343,8 @@ private static List compileOptions(String targetDir, Collection
List args = new ArrayList<>();
args.add("-d");
args.add(targetDir);
+ args.add("-classpath");
+ args.add(targetDir + File.pathSeparator + System.getProperty("java.class.path"));
args.add("-verbose");
if (!deploymentClassNames.isEmpty()) {
args.add("-A" + CdiUtil.BEAN_CLASSES_OPTION + "=" + String.join(",", deploymentClassNames));
diff --git a/tck-runner/src/main/java/org/eclipse/odi/tck/util/BeanManagerFactory.java b/tck-runner/src/main/java/org/eclipse/odi/tck/util/BeanManagerFactory.java
index f8def6b..234a93d 100644
--- a/tck-runner/src/main/java/org/eclipse/odi/tck/util/BeanManagerFactory.java
+++ b/tck-runner/src/main/java/org/eclipse/odi/tck/util/BeanManagerFactory.java
@@ -16,8 +16,6 @@
package org.eclipse.odi.tck.util;
import io.micronaut.context.annotation.Factory;
-import jakarta.el.ELResolver;
-import jakarta.el.ExpressionFactory;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.spi.Context;
import jakarta.enterprise.context.spi.Contextual;
@@ -118,16 +116,6 @@ public int getInterceptorBindingHashCode(Annotation interceptorBinding) {
throw new UnsupportedOperationException();
}
- @Override
- public ELResolver getELResolver() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public ExpressionFactory wrapExpressionFactory(ExpressionFactory expressionFactory) {
- throw new UnsupportedOperationException();
- }
-
@Override
public AnnotatedType createAnnotatedType(Class type) {
throw new UnsupportedOperationException();
@@ -283,6 +271,11 @@ public boolean isMatchingEvent(Type eventType,
Set observedEventQualifiers) {
return beanContainer().isMatchingEvent(eventType, eventQualifiers, observedEventType, observedEventQualifiers);
}
+
+ @Override
+ public T unwrapClientProxy(T instance) {
+ return beanContainer().unwrapClientProxy(instance);
+ }
};
}