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

Introduce AutoAddScopeBuildItem to remove boilerplate necessary when annotation transformers are used to add a scope annotation to a class #11350

Merged
merged 1 commit into from
Aug 13, 2020
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
@@ -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