Skip to content

Commit

Permalink
ArC: allow putting bean enablement annotations on stereotypes
Browse files Browse the repository at this point in the history
It would probably be best to write this algorithm in a lazy fashion
(driven by the annotation transformation demands), but that would
require breaking an extension API (specifically, it wouldn't be
possible to produce `BuildTimeConditionBuildItem`). Hence, this commit
enhances the eager algorithm for bean enablement scanning to also
scan stereotypes, relying on subclass information in the Jandex index
to support `@Inherited` stereotypes.
  • Loading branch information
Ladicek committed Nov 28, 2023
1 parent e4eb8bd commit c197f41
Show file tree
Hide file tree
Showing 13 changed files with 1,540 additions and 137 deletions.
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/cdi-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,8 @@ public class TracerConfiguration {

NOTE: The runtime profile has absolutely no effect on the bean resolution using `@IfBuildProfile` and `@UnlessBuildProfile`.

TIP: It is also possible to use `@IfBuildProfile` and `@UnlessBuildProfile` on stereotypes.

[[enable_build_properties]]
=== Enabling Beans for Quarkus Build Properties

Expand Down Expand Up @@ -654,6 +656,8 @@ public class TracerConfiguration {

NOTE: Properties set at runtime have absolutely no effect on the bean resolution using `@IfBuildProperty`.

Check warning on line 657 in docs/src/main/asciidoc/cdi-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/cdi-reference.adoc", "range": {"start": {"line": 657, "column": 81}}}, "severity": "INFO"}

TIP: It is also possible to use `@IfBuildProperty` and `@UnlessBuildProperty` on stereotypes.

=== Declaring Selected Alternatives

Check warning on line 661 in docs/src/main/asciidoc/cdi-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/cdi-reference.adoc", "range": {"start": {"line": 661, "column": 1}}}, "severity": "INFO"}

Check warning on line 661 in docs/src/main/asciidoc/cdi-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '4.9. Declaring Selected Alternatives'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '4.9. Declaring Selected Alternatives'.", "location": {"path": "docs/src/main/asciidoc/cdi-reference.adoc", "range": {"start": {"line": 661, "column": 1}}}, "severity": "INFO"}

In CDI, an alternative bean may be selected either globally for an application by means of `@Priority`, or for a bean archive using a `beans.xml` descriptor.

Check warning on line 663 in docs/src/main/asciidoc/cdi-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/cdi-reference.adoc", "range": {"start": {"line": 663, "column": 11}}}, "severity": "WARNING"}

Check warning on line 663 in docs/src/main/asciidoc/cdi-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/cdi-reference.adoc", "range": {"start": {"line": 663, "column": 108}}}, "severity": "INFO"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public BuildTimeConditionBuildItem(AnnotationTarget target, boolean enabled) {
this.target = target;
break;
default:
throw new IllegalArgumentException("'target' can only be a class, a field or a method");
throw new IllegalArgumentException("'target' can only be a class, a field or a method: " + target);
}
this.enabled = enabled;
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.arc.deployment;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;

import io.quarkus.builder.item.SimpleBuildItem;

final class BuildTimeEnabledStereotypesBuildItem extends SimpleBuildItem {
private final Map<DotName, BuildTimeEnabledStereotype> map;

BuildTimeEnabledStereotypesBuildItem(List<BuildTimeEnabledStereotype> buildTimeEnabledStereotypes) {
Map<DotName, BuildTimeEnabledStereotype> map = new HashMap<>();
for (BuildTimeEnabledStereotype buildTimeEnabledStereotype : buildTimeEnabledStereotypes) {
map.put(buildTimeEnabledStereotype.name, buildTimeEnabledStereotype);
}
this.map = map;
}

boolean isStereotype(DotName name) {
return map.containsKey(name);
}

BuildTimeEnabledStereotype getStereotype(DotName stereotypeName) {
return map.get(stereotypeName);
}

Collection<BuildTimeEnabledStereotype> all() {
return map.values();
}

static final class BuildTimeEnabledStereotype {
final DotName name;
final boolean inheritable; // meta-annotated `@Inherited`

// enablement annotations present directly _or transitively_ on this stereotype
final Map<DotName, List<AnnotationInstance>> annotations;

BuildTimeEnabledStereotype(DotName name, boolean inheritable, Map<DotName, List<AnnotationInstance>> annotations) {
this.name = name;
this.inheritable = inheritable;
this.annotations = annotations;
}

List<AnnotationInstance> getEnablementAnnotations(DotName enablementAnnotationName) {
return annotations.getOrDefault(enablementAnnotationName, List.of());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package io.quarkus.arc.test.profile;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Stereotype;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.profile.IfBuildProfile;
import io.quarkus.test.QuarkusUnitTest;

public class IfBuildProfileStereotypeTest {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(DevOnly.class, InheritableDevOnly.class, TransitiveDevOnly.class,
InheritableTransitiveDevOnly.class, MyService.class, DevOnlyMyService.class,
InheritableDevOnlyMyService.class, TransitiveDevOnlyMyService.class,
InheritableTransitiveDevOnlyMyService.class, MyServiceSimple.class,
MyServiceDevOnlyDirect.class, MyServiceDevOnlyTransitive.class,
MyServiceDevOnlyOnSuperclassNotInheritable.class,
MyServiceDevOnlyOnSuperclassInheritable.class,
MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class,
MyServiceDevOnlyTransitiveOnSuperclassInheritable.class, Producers.class));

@Inject
@Any
Instance<MyService> services;

@Test
public void test() {
Set<String> hello = services.stream().map(MyService::hello).collect(Collectors.toSet());
Set<Object> expected = Set.of(
MyServiceSimple.class.getSimpleName(),
MyServiceDevOnlyOnSuperclassNotInheritable.class.getSimpleName(),
MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class.getSimpleName(),
Producers.SIMPLE);
assertEquals(expected, hello);
}

@IfBuildProfile("dev")
@Stereotype
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DevOnly {
}

@IfBuildProfile("dev")
@Stereotype
@Inherited
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritableDevOnly {
}

@DevOnly
@Stereotype
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TransitiveDevOnly {
}

@DevOnly
@Stereotype
@Inherited
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritableTransitiveDevOnly {
}

interface MyService {
String hello();
}

@DevOnly
static abstract class DevOnlyMyService implements MyService {
}

@InheritableDevOnly
static abstract class InheritableDevOnlyMyService implements MyService {
}

@TransitiveDevOnly
static abstract class TransitiveDevOnlyMyService implements MyService {
}

@InheritableTransitiveDevOnly
static abstract class InheritableTransitiveDevOnlyMyService implements MyService {
}

@ApplicationScoped
static class MyServiceSimple implements MyService {
@Override
public String hello() {
return MyServiceSimple.class.getSimpleName();
}
}

@ApplicationScoped
@DevOnly
static class MyServiceDevOnlyDirect implements MyService {
@Override
public String hello() {
return MyServiceDevOnlyDirect.class.getSimpleName();
}
}

@ApplicationScoped
@TransitiveDevOnly
static class MyServiceDevOnlyTransitive implements MyService {
@Override
public String hello() {
return MyServiceDevOnlyTransitive.class.getSimpleName();
}
}

@ApplicationScoped
static class MyServiceDevOnlyOnSuperclassNotInheritable extends DevOnlyMyService {
@Override
public String hello() {
return MyServiceDevOnlyOnSuperclassNotInheritable.class.getSimpleName();
}
}

@ApplicationScoped
static class MyServiceDevOnlyOnSuperclassInheritable extends InheritableDevOnlyMyService {
@Override
public String hello() {
return MyServiceDevOnlyOnSuperclassInheritable.class.getSimpleName();
}
}

@ApplicationScoped
static class MyServiceDevOnlyTransitiveOnSuperclassNotInheritable extends TransitiveDevOnlyMyService {
@Override
public String hello() {
return MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class.getSimpleName();
}
}

@ApplicationScoped
static class MyServiceDevOnlyTransitiveOnSuperclassInheritable extends InheritableTransitiveDevOnlyMyService {
@Override
public String hello() {
return MyServiceDevOnlyTransitiveOnSuperclassInheritable.class.getSimpleName();
}
}

@ApplicationScoped
static class Producers {
static final String SIMPLE = "Producers.simple";
static final String DEV_ONLY_DIRECT = "Producers.devOnlyDirect";
static final String DEV_ONLY_TRANSITIVE = "Producers.devOnlyTransitive";

@Produces
MyService simple = new MyService() {
@Override
public String hello() {
return SIMPLE;
}
};

@Produces
@DevOnly
MyService devOnlyDirect = new MyService() {
@Override
public String hello() {
return DEV_ONLY_DIRECT;
}
};

@Produces
@TransitiveDevOnly
MyService devOnlyTransitive = new MyService() {
@Override
public String hello() {
return DEV_ONLY_TRANSITIVE;
}
};
}
}

0 comments on commit c197f41

Please sign in to comment.