Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions CDI_5_BETA1_SUPPORT_PLAN.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies {
annotationProcessor("org.eclipse.odi:micronaut-odi-processor-cdi:<version>")

implementation("org.eclipse.odi:micronaut-odi-cdi:<version>")
implementation("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0")
implementation("jakarta.cdi:jakarta.cdi-api:5.0.0.Beta1")
}
```

Expand Down
2 changes: 2 additions & 0 deletions cdi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -94,9 +105,95 @@ public boolean isCandidateBean(Argument<?> beanType, QualifiedBeanType<?> candid
}
return candidate.isCandidateBean(beanType);
}

@Override
public <T> Optional<BeanDefinition<T>> resolveNonUniqueBean(Argument<T> beanType,
Qualifier<T> qualifier,
Collection<BeanDefinition<T>> candidates) {
return resolveCdiBean(qualifier, candidates);
}
});
}

private static <T> Optional<BeanDefinition<T>> resolveCdiBean(Qualifier<T> qualifier,
Collection<BeanDefinition<T>> beanDefinitions) {
if (beanDefinitions.isEmpty() || beanDefinitions.size() == 1) {
return Optional.empty();
}
if (isDefaultQualifier(qualifier)) {
List<BeanDefinition<T>> 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<BeanDefinition<T>> alternatives = beanDefinitions
.stream()
.filter(bd -> bd.hasStereotype(Alternative.class))
.filter(bd -> getPriority(bd) > 0)
.collect(Collectors.toList());
if (!alternatives.isEmpty()) {
return highestUniquePriority(alternatives);
}
List<BeanDefinition<T>> 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 <T> boolean isDefaultQualifier(Qualifier<T> qualifier) {
return qualifier == null || DefaultQualifier.instance().contains((Qualifier) qualifier);
}

private static <T> Optional<BeanDefinition<T>> highestUniquePriority(Collection<BeanDefinition<T>> beanDefinitions) {
List<BeanDefinition<T>> sorted = beanDefinitions.stream()
.filter(beanDefinition -> getPriority(beanDefinition) > 0)
.sorted(Comparator.<BeanDefinition<T>>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;
Expand Down
Loading
Loading