-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce AutoAddScopeBuildItem to remove boilerplate necessary when...
...annotation transformers are used to add a scope annotation to a class - also fixes #11340
- Loading branch information
Showing
11 changed files
with
506 additions
and
83 deletions.
There are no files selected for viewing
243 changes: 243 additions & 0 deletions
243
extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
|
||
} |
129 changes: 129 additions & 0 deletions
129
extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.