Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArC - introduce the convenient AnnotationsTransformer.Builder #19166

Merged
merged 1 commit into from
Aug 3, 2021
Merged
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
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;
}

}

}