Skip to content

Commit

Permalink
Merge pull request #11350 from mkouba/issue-11340
Browse files Browse the repository at this point in the history
Introduce AutoAddScopeBuildItem to remove boilerplate necessary when annotation transformers are used to add a scope annotation to a class
  • Loading branch information
gsmet authored Aug 13, 2020
2 parents b697b25 + 0a80d0e commit a644d46
Show file tree
Hide file tree
Showing 11 changed files with 506 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package io.quarkus.arc.deployment;

import java.util.Collection;

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

import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.builder.item.MultiBuildItem;

/**
* This build item can be used to turn a class that is not annotated with a CDI scope annotation into a bean, i.e. the default
* scope annotation is added automatically if conditions are met.
*/
public final class AutoAddScopeBuildItem extends MultiBuildItem {

public static Builder builder() {
return new Builder();
}

private final MatchPredicate matchPredicate;
private final boolean containerServicesRequired;
private final DotName defaultScope;
private final boolean unremovable;
private final String reason;

private AutoAddScopeBuildItem(MatchPredicate matchPredicate, boolean containerServicesRequired,
DotName defaultScope, boolean unremovable, String reason) {
this.matchPredicate = matchPredicate;
this.containerServicesRequired = containerServicesRequired;
this.defaultScope = defaultScope;
this.unremovable = unremovable;
this.reason = reason;
}

public boolean isContainerServicesRequired() {
return containerServicesRequired;
}

public DotName getDefaultScope() {
return defaultScope;
}

public boolean isUnremovable() {
return unremovable;
}

public String getReason() {
return reason != null ? ": " + reason : "";
}

public boolean test(ClassInfo clazz, Collection<AnnotationInstance> annotations, IndexView index) {
return matchPredicate.test(clazz, annotations, index);
}

public interface MatchPredicate {

/**
* @param clazz
* @param annotations
* @param index
* @return {@code true} if the input arguments match the predicate,
* {@code false} otherwise
*/
boolean test(ClassInfo clazz, Collection<AnnotationInstance> annotations, IndexView index);

default MatchPredicate and(MatchPredicate other) {
return new MatchPredicate() {
@Override
public boolean test(ClassInfo clazz, Collection<AnnotationInstance> annotations, IndexView index) {
return test(clazz, annotations, index) && other.test(clazz, annotations, index);
}
};
}

}

public static class Builder {

private MatchPredicate matchPredicate;
private boolean requiresContainerServices;
private DotName defaultScope;
private boolean unremovable;
private String reason;

private Builder() {
this.defaultScope = BuiltinScope.DEPENDENT.getName();
this.unremovable = false;
this.requiresContainerServices = false;
}

/**
* At least one injection point or lifecycle callback must be declared in the class hierarchy. Otherwise, the scope
* annotation is not added.
* <p>
* Note that the detection algorithm is just the best effort. Some inheritance rules defined by the spec are not
* followed, e.g. per spec an initializer method is only inherited if not overriden. This method merely scans the
* annotations.
*
* @return self
*/
public Builder requiresContainerServices() {
this.requiresContainerServices = true;
return this;
}

/**
* The bean will be unremovable.
*
* @see ArcConfig#removeUnusedBeans
* @return self
*/
public Builder unremovable() {
this.unremovable = true;
return this;
}

/**
* Set a custom predicate.
*
* @param predicate
* @return self
*/
public Builder match(MatchPredicate predicate) {
this.matchPredicate = predicate;
return this;
}

/**
* The class must be annotated with the given annotation. Otherwise, the scope annotation is not added.
* <p>
* The final predicate is a short-circuiting logical AND of the previous predicate (if any) and this condition.
*
* @param annotationName
* @return self
*/
public Builder isAnnotatedWith(DotName annotationName) {
return and((clazz, annotations, index) -> Annotations.contains(annotations, annotationName));
}

/**
* The class or any of its element must be annotated with the given annotation. Otherwise, the scope annotation is not
* added.
* <p>
* The final predicate is a short-circuiting logical AND of the previous predicate (if any) and this condition.
*
*
* @param annotationNames
* @return self
*/
public Builder containsAnnotations(DotName... annotationNames) {
return and((clazz, annotations, index) -> {
for (DotName annotation : annotationNames) {
if (clazz.annotations().containsKey(annotation)) {
return true;
}
}
return false;
});
}

/**
* The class must directly or indirectly implement the given interface.
* <p>
* The final predicate is a short-circuiting logical AND of the previous predicate (if any) and this condition.
*
* @param interfaceName
* @param index
* @return self
*/
public Builder implementsInterface(DotName interfaceName) {
return and((clazz, annotations, index) -> {
if (clazz.interfaceNames().contains(interfaceName)) {
return true;
}
DotName superName = clazz.superName();
while (superName != null && !superName.equals(DotNames.OBJECT)) {
ClassInfo superClass = index.getClassByName(superName);
if (superClass != null) {
if (superClass.interfaceNames().contains(interfaceName)) {
return true;
}
superName = superClass.superName();
}
}
return false;
});
}

/**
* The scope annotation added to the class.
*
* @param scopeAnnotationName
* @return self
*/
public Builder defaultScope(DotName scopeAnnotationName) {
this.defaultScope = scopeAnnotationName;
return this;
}

/**
* The scope annotation added to the class.
*
* @param scope
* @return
*/
public Builder defaultScope(BuiltinScope scope) {
return defaultScope(scope.getName());
}

/**
* Specify an optional reason description that is used in log messages.
*
* @param reason
* @return the reason why the scope annotation was added
*/
public Builder reason(String reason) {
this.reason = reason;
return this;
}

private Builder and(MatchPredicate other) {
if (matchPredicate == null) {
matchPredicate = other;
} else {
matchPredicate = matchPredicate.and(other);
}
return this;
}

public AutoAddScopeBuildItem build() {
if (matchPredicate == null) {
throw new IllegalStateException("A matching predicate must be set!");
}
return new AutoAddScopeBuildItem(matchPredicate, requiresContainerServices, defaultScope, unremovable, reason);
}
}

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

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;

public class AutoAddScopeProcessor {

private static final Logger LOGGER = Logger.getLogger(AutoAddScopeProcessor.class);

@BuildStep
void annotationTransformer(List<AutoAddScopeBuildItem> autoScopes, CustomScopeAnnotationsBuildItem scopes,
List<AutoInjectAnnotationBuildItem> autoInjectAnnotations,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformers,
BuildProducer<UnremovableBeanBuildItem> unremovableBeans,
BeanArchiveIndexBuildItem beanArchiveIndex) throws Exception {
if (autoScopes.isEmpty()) {
return;
}
Set<DotName> containerAnnotationNames = autoInjectAnnotations.stream().flatMap(a -> a.getAnnotationNames().stream())
.collect(Collectors.toSet());
containerAnnotationNames.add(DotNames.POST_CONSTRUCT);
containerAnnotationNames.add(DotNames.PRE_DESTROY);
containerAnnotationNames.add(DotNames.INJECT);

Set<DotName> unremovables = new HashSet<>();

annotationsTransformers.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

@Override
public boolean appliesTo(Kind kind) {
return kind == Kind.CLASS;
}

@Override
public void transform(TransformationContext context) {
if (scopes.isScopeIn(context.getAnnotations())) {
// Skip classes annotated with a scope
return;
}
ClassInfo clazz = context.getTarget().asClass();
Boolean requiresContainerServices = null;

for (AutoAddScopeBuildItem autoScope : autoScopes) {
if (autoScope.isContainerServicesRequired()) {
if (requiresContainerServices == null) {
requiresContainerServices = requiresContainerServices(clazz, containerAnnotationNames,
beanArchiveIndex.getIndex());
}
if (!requiresContainerServices) {
// Skip - no injection point detected
continue;
}
}
if (autoScope.test(clazz, context.getAnnotations(), beanArchiveIndex.getIndex())) {
context.transform().add(autoScope.getDefaultScope()).done();
if (autoScope.isUnremovable()) {
unremovables.add(clazz.name());
}
LOGGER.debugf("Automatically added scope %s to class %s" + autoScope.getReason(),
autoScope.getDefaultScope(), clazz, autoScope.getReason());
break;
}
}
}
}));

if (!unremovables.isEmpty()) {
unremovableBeans.produce(new UnremovableBeanBuildItem(new Predicate<BeanInfo>() {

@Override
public boolean test(BeanInfo bean) {
return bean.isClassBean() && unremovables.contains(bean.getBeanClass());
}
}));
}
}

private boolean requiresContainerServices(ClassInfo clazz, Set<DotName> containerAnnotationNames, IndexView index) {
// Note that transformed methods/fields are not taken into account
if (hasContainerAnnotation(clazz, containerAnnotationNames)) {
return true;
}
if (index != null) {
DotName superName = clazz.superName();
while (superName != null && !superName.equals(DotNames.OBJECT)) {
ClassInfo superClass = index.getClassByName(superName);
if (superClass != null) {
if (hasContainerAnnotation(clazz, containerAnnotationNames)) {
return true;
}
superName = superClass.superName();
}
}
}
return false;
}

private boolean hasContainerAnnotation(ClassInfo clazz, Set<DotName> containerAnnotationNames) {
if (clazz.annotations().isEmpty() || containerAnnotationNames.isEmpty()) {
return false;
}
return containsAny(clazz, containerAnnotationNames);
}

private boolean containsAny(ClassInfo clazz, Set<DotName> annotationNames) {
for (DotName annotation : clazz.annotations().keySet()) {
if (annotationNames.contains(annotation)) {
return true;
}
}
return false;
}

}
9 changes: 7 additions & 2 deletions extensions/jsonb/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-jsonb-parent</artifactId>
<groupId>io.quarkus</groupId>
Expand Down Expand Up @@ -34,6 +34,11 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Loading

0 comments on commit a644d46

Please sign in to comment.