Skip to content

Commit

Permalink
Merge pull request #19166 from mkouba/arc-annotation-transformer-builder
Browse files Browse the repository at this point in the history
ArC - introduce the convenient AnnotationsTransformer.Builder
  • Loading branch information
mkouba authored Aug 3, 2021
2 parents b8750f9 + 3e238a0 commit 5afdd3c
Show file tree
Hide file tree
Showing 2 changed files with 345 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package io.quarkus.arc.processor;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.DotName;

/**
* Allows a build-time extension to override the annotations that exist on bean classes.
* <p>
* The container should use {@link AnnotationStore} to obtain annotations of any {@link org.jboss.jandex.ClassInfo},
* {@link org.jboss.jandex.FieldInfo} and {@link org.jboss.jandex.MethodInfo}.
*
* @author Martin Kouba
* @see Builder
*/
public interface AnnotationsTransformer extends BuildExtension {

Expand All @@ -31,6 +39,17 @@ default boolean appliesTo(Kind kind) {
*/
void transform(TransformationContext transformationContext);

/**
*
* @return a new builder instance
*/
static Builder builder() {
return new Builder();
}

/**
* A transformation context.
*/
interface TransformationContext extends BuildContext {

AnnotationTarget getTarget();
Expand Down Expand Up @@ -64,4 +83,202 @@ default boolean isMethod() {

}

/**
* A convenient builder.
*/
static final class Builder {

private int priority;
private Predicate<Kind> appliesTo;
private Predicate<TransformationContext> predicate;

private Builder() {
this.priority = DEFAULT_PRIORITY;
}

/**
*
* @param appliesToKind
* @return self
* @see AnnotationsTransformer#appliesTo(Kind)
*/
public Builder appliesTo(Kind appliesToKind) {
return appliesTo(kind -> kind == appliesToKind);
}

/**
*
* @param appliesTo
* @return self
* @see AnnotationsTransformer#appliesTo(Kind)
*/
public Builder appliesTo(Predicate<Kind> appliesTo) {
this.appliesTo = appliesTo;
return this;
}

/**
*
* @param priority
* @return self
*/
public Builder priority(int priority) {
this.priority = priority;
return this;
}

/**
* {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations.
*
* @param annotationNames
* @return self
*/
public Builder whenContainsAll(List<DotName> annotationNames) {
return when(context -> {
for (DotName annotationName : annotationNames) {
if (!Annotations.contains(context.getAnnotations(), annotationName)) {
return false;
}
}
return true;
});
}

/**
* {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations.
*
* @param annotationNames
* @return self
*/
public Builder whenContainsAll(DotName... annotationNames) {
return whenContainsAll(List.of(annotationNames));
}

/**
* {@link TransformationContext#getAnnotations()} must contain ALL of the given annotations.
*
* @param annotationNames
* @return self
*/
@SafeVarargs
public final Builder whenContainsAll(Class<? extends Annotation>... annotationNames) {
return whenContainsAll(
Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList()));
}

/**
* {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations.
*
* @param annotationNames
* @return self
*/
public Builder whenContainsAny(List<DotName> annotationNames) {
return when(context -> Annotations.containsAny(context.getAnnotations(), annotationNames));
}

/**
* {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations.
*
* @param annotationNames
* @return self
*/
public Builder whenContainsAny(DotName... annotationNames) {
return whenContainsAny(List.of(annotationNames));
}

/**
* {@link TransformationContext#getAnnotations()} must contain ANY of the given annotations.
*
* @param annotationNames
* @return self
*/
@SafeVarargs
public final Builder whenContainsAny(Class<? extends Annotation>... annotationNames) {
return whenContainsAny(
Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList()));
}

/**
* {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations.
*
* @param annotationNames
* @return self
*/
public Builder whenContainsNone(List<DotName> annotationNames) {
return when(context -> !Annotations.containsAny(context.getAnnotations(), annotationNames));
}

/**
* {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations.
*
* @param annotationNames
* @return self
*/
public Builder whenContainsNone(DotName... annotationNames) {
return whenContainsNone(List.of(annotationNames));
}

/**
* {@link TransformationContext#getAnnotations()} must NOT contain any of the given annotations.
*
* @param annotationNames
* @return self
*/
@SafeVarargs
public final Builder whenContainsNone(Class<? extends Annotation>... annotationNames) {
return whenContainsNone(
Arrays.stream(annotationNames).map(a -> DotName.createSimple(a.getName())).collect(Collectors.toList()));
}

/**
* The transformation logic is only performed if the given predicate is evaluated to true. Multiple predicates are
* logically-ANDed.
*
* @param predicate
* @return self
*/
public Builder when(Predicate<TransformationContext> when) {
if (predicate == null) {
predicate = when;
} else {
predicate = predicate.and(when);
}
return this;
}

/**
* The given transformation logic is only performed if all conditions added via {@link #when(Predicate)} are met.
*
* @param consumer
* @return a new annotation transformer
*/
public AnnotationsTransformer transform(Consumer<TransformationContext> consumer) {
Predicate<Kind> appliesTo = this.appliesTo;
int priority = this.priority;
Consumer<TransformationContext> transform = Objects.requireNonNull(consumer);
Predicate<TransformationContext> predicate = this.predicate;
return new AnnotationsTransformer() {

@Override
public int getPriority() {
return priority;
}

@Override
public boolean appliesTo(Kind kind) {
return appliesTo != null ? appliesTo.test(kind) : true;
}

@Override
public void transform(TransformationContext context) {
if (predicate == null || predicate.test(context)) {
transform.accept(context);
}
}

};
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package io.quarkus.arc.test.buildextension.annotations;

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

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.test.ArcTestContainer;
import java.util.AbstractList;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.Vetoed;
import javax.inject.Inject;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.PrimitiveType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class AnnotationsTransformerBuilderTest {

@RegisterExtension
public ArcTestContainer container = ArcTestContainer.builder()
.beanClasses(Seven.class, One.class, IWantToBeABean.class)
.annotationsTransformers(
AnnotationsTransformer.builder()
.appliesTo(Kind.CLASS)
.whenContainsAny(Dependent.class)
.transform(context -> {
if (context.getTarget().asClass().name().toString().equals(One.class.getName())) {
// Veto bean class One
context.transform().add(Vetoed.class).done();
}
}),
AnnotationsTransformer.builder()
.appliesTo(Kind.CLASS)
.when(context -> context.getTarget().asClass().name().local()
.equals(IWantToBeABean.class.getSimpleName()))
.transform(context -> context.transform().add(Dependent.class).done()),
AnnotationsTransformer.builder()
.appliesTo(Kind.FIELD)
.transform(context -> {
if (context.getTarget().asField().name().equals("seven")) {
context.transform().add(Inject.class).done();
}
}),
// Add @Produces to a method that returns int and is not annoated with @Simple
AnnotationsTransformer.builder()
.appliesTo(Kind.METHOD)
.whenContainsNone(Simple.class)
.when(context -> context.getTarget().asMethod().returnType().name()
.equals(PrimitiveType.INT.name()))
.transform(context -> {
context.transform().add(Produces.class).done();
}))
.build();

@Test
public void testVetoed() {
ArcContainer arc = Arc.container();
assertTrue(arc.instance(Seven.class).isAvailable());
// One is vetoed
assertFalse(arc.instance(One.class).isAvailable());
assertEquals(Integer.valueOf(7), Integer.valueOf(arc.instance(Seven.class).get().size()));

// Scope annotation and @Inject are added by transformer
InstanceHandle<IWantToBeABean> iwant = arc.instance(IWantToBeABean.class);
assertTrue(iwant.isAvailable());
assertEquals(Integer.valueOf(7), Integer.valueOf(iwant.get().size()));

assertEquals(Integer.valueOf(7), arc.select(int.class).get());
}

// => add @Dependent here
static class IWantToBeABean {

// => add @Inject here
Seven seven;

// => add @Produces here
public int size() {
return seven.size();
}

// => do not add @Produces here
@Simple
public int anotherSize() {
return seven.size();
}

}

@Dependent
static class Seven extends AbstractList<Integer> {

@Override
public Integer get(int index) {
return Integer.valueOf(7);
}

@Simple
@Override
public int size() {
return 7;
}

}

// => add @Vetoed here
@Dependent
static class One extends AbstractList<Integer> {

@Override
public Integer get(int index) {
return Integer.valueOf(1);
}

@Override
public int size() {
return 1;
}

}

}

0 comments on commit 5afdd3c

Please sign in to comment.