diff --git a/core/src/main/java/org/jboss/jandex/AnnotationInstance.java b/core/src/main/java/org/jboss/jandex/AnnotationInstance.java index 941ab8e6..7813b7fa 100644 --- a/core/src/main/java/org/jboss/jandex/AnnotationInstance.java +++ b/core/src/main/java/org/jboss/jandex/AnnotationInstance.java @@ -44,7 +44,6 @@ public final class AnnotationInstance { static final NameComparator NAME_COMPARATOR = new NameComparator(); static final AnnotationInstance[] EMPTY_ARRAY = new AnnotationInstance[0]; - static final DotName RETENTION = new DotName(DotName.JAVA_LANG_ANNOTATION_NAME, "Retention", true, false); private final DotName name; private final AnnotationTarget target; @@ -97,8 +96,8 @@ public static AnnotationInstanceBuilder builder(ClassInfo annotationType) { throw new IllegalArgumentException("Annotation type can't be null"); } DotName name = annotationType.name(); - boolean visible = annotationType.hasDeclaredAnnotation(RETENTION) - && annotationType.declaredAnnotation(RETENTION).value().asString().equals("RUNTIME"); + boolean visible = annotationType.hasDeclaredAnnotation(DotName.RETENTION_NAME) + && annotationType.declaredAnnotation(DotName.RETENTION_NAME).value().asString().equals("RUNTIME"); return new AnnotationInstanceBuilder(name, visible); } diff --git a/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java b/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java new file mode 100644 index 00000000..a8887ff9 --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java @@ -0,0 +1,265 @@ +package org.jboss.jandex; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Annotation overlay allows overriding annotation information from an index. This is useful when + * Jandex is used as a language model and annotations are directly used as framework metadata. + * Transforming metadata is a frequent requirement in such situations, but core Jandex is immutable. + * This interface is layered on top of core Jandex and provides the necessary indirection between + * the user and the core Jandex that is required to apply the transformations. + * + * @since 3.2.0 + */ +public interface AnnotationOverlay { + /** + * Returns a new builder for an annotation overlay for given {@code index} and a given collection + * of {@code transformations}. + * + *

+ * Thread safety + * + *

+ * The object returned by the builder is immutable and can be shared between threads without safe publication. + * + * @param index the Jandex index, must not be {@code null} + * @param annotationTransformations the collection of annotation transformations + * @return the annotation overlay builder, never {@code null} + */ + static Builder builder(IndexView index, Collection annotationTransformations) { + Objects.requireNonNull(index); + if (annotationTransformations == null) { + annotationTransformations = Collections.emptyList(); + } + return new Builder(index, annotationTransformations); + } + + /** + * Returns the index whose annotation information is being overlaid. + * + * @return the index underlying this annotation overlay, never {@code null} + */ + IndexView index(); + + /** + * Returns whether an annotation instance with given {@code name} is declared on given {@code declaration}. + *

+ * Like {@link AnnotationTarget#hasDeclaredAnnotation(DotName)}, and unlike {@link AnnotationTarget#hasAnnotation(DotName)}, + * this method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param name name of the annotation type to look for, must not be {@code null} + * @return {@code true} if the annotation is present, {@code false} otherwise + */ + boolean hasAnnotation(Declaration declaration, DotName name); + + /** + * Returns whether an annotation instance of given {@code clazz} is declared on given {@code declaration}. + *

+ * Like {@link AnnotationTarget#hasDeclaredAnnotation(Class)}, and unlike {@link AnnotationTarget#hasAnnotation(Class)}, + * this method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param clazz the annotation type to look for, must not be {@code null} + * @return {@code true} if the annotation is present, {@code false} otherwise + * @see #hasAnnotation(Declaration, DotName) + */ + default boolean hasAnnotation(Declaration declaration, Class clazz) { + return hasAnnotation(declaration, DotName.createSimple(clazz.getName())); + } + + /** + * Returns whether any annotation instance with one of given {@code names} is declared on given {@code declaration}. + *

+ * This method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param names names of the annotation types to look for, must not be {@code null} + * @return {@code true} if any of the annotations is present, {@code false} otherwise + */ + boolean hasAnyAnnotation(Declaration declaration, Set names); + + /** + * Returns whether any annotation instance of one of given {@code classes} is declared on given {@code declaration}. + *

+ * This method ignores annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param classes annotation types to look for, must not be {@code null} + * @return {@code true} if any of the annotations is present, {@code false} otherwise + */ + default boolean hasAnyAnnotation(Declaration declaration, Class... classes) { + Set names = new HashSet<>(classes.length); + for (Class clazz : classes) { + names.add(DotName.createSimple(clazz.getName())); + } + return hasAnyAnnotation(declaration, names); + } + + /** + * Returns the annotation instance with given {@code name} declared on given {@code declaration}. + *

+ * Like {@link AnnotationTarget#annotation(DotName)}, and unlike {@link AnnotationTarget#annotation(DotName)}, + * this method doesn't return annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param name name of the annotation type to look for, must not be {@code null} + * @return the annotation instance, or {@code null} if not found + */ + AnnotationInstance annotation(Declaration declaration, DotName name); + + /** + * Returns the annotation instance of given {@code clazz} declared on given {@code declaration}. + *

+ * Like {@link AnnotationTarget#annotation(Class)}, and unlike {@link AnnotationTarget#annotation(Class)}, + * this method doesn't return annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param clazz the annotation type to look for, must not be {@code null} + * @return the annotation instance, or {@code null} if not found + * @see #annotation(Declaration, DotName) + */ + default AnnotationInstance annotation(Declaration declaration, Class clazz) { + return annotation(declaration, DotName.createSimple(clazz.getName())); + } + + /** + * Returns the annotation instances with given {@code name} declared on given {@code declaration}. + * If the specified annotation is repeatable, the result also contains all values from the container annotation + * instance. + *

+ * The annotation class must be present in the index underlying this annotation overlay. + *

+ * Like {@link AnnotationTarget#declaredAnnotationsWithRepeatable(DotName, IndexView)}, and unlike + * {@link AnnotationTarget#annotationsWithRepeatable(DotName, IndexView)}, this method doesn't return + * annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param name name of the annotation type, must not be {@code null} + * @return immutable collection of annotation instances, never {@code null} + */ + Collection annotationsWithRepeatable(Declaration declaration, DotName name); + + /** + * Returns the annotation instances of given type ({@code clazz}) declared on given {@code declaration}. + * If the specified annotation is repeatable, the result also contains all values from the container annotation + * instance. + *

+ * The annotation class must be present in the index underlying this annotation overlay. + *

+ * Like {@link AnnotationTarget#declaredAnnotationsWithRepeatable(Class, IndexView)}, and unlike + * {@link AnnotationTarget#annotationsWithRepeatable(Class, IndexView)}, this method doesn't return + * annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @param clazz the annotation type, must not be {@code null} + * @return immutable collection of annotation instances, never {@code null} + * @see #annotationsWithRepeatable(Declaration, DotName) + */ + default Collection annotationsWithRepeatable(Declaration declaration, + Class clazz) { + return annotationsWithRepeatable(declaration, DotName.createSimple(clazz.getName())); + } + + /** + * Returns the annotation instances declared on given {@code declaration}. + *

+ * Like {@link AnnotationTarget#declaredAnnotations()}, and unlike {@link AnnotationTarget#annotations()}, + * this method doesn't return annotations declared on nested annotation targets. This doesn't hold in case of methods + * in the {@linkplain Builder#compatibleMode() compatible mode}, where method parameters are considered + * part of methods. + * + * @param declaration the declaration to inspect, must not be {@code null} + * @return immutable collection of annotation instances, never {@code null} + */ + Collection annotations(Declaration declaration); + + /** + * The builder for an annotation overlay. + */ + final class Builder { + private final IndexView index; + private final Collection annotationTransformations; + + private boolean compatibleMode; + private boolean runtimeAnnotationsOnly; + private boolean inheritedAnnotations; + + Builder(IndexView index, Collection annotationTransformations) { + this.index = index; + this.annotationTransformations = annotationTransformations; + } + + /** + * When called, the built annotation overlay shall treat method parameters as part of methods. + * This means that annotations on method parameters are returned when asking for annotations + * of a method, asking for annotations on method parameters results in an exception, and + * annotation transformations for method parameters are ignored. + *

+ * This method is called {@code compatibleMode} because the built annotation overlay is + * compatible with the previous implementation of the same concept in Quarkus. + * + * @return this builder + */ + public Builder compatibleMode() { + compatibleMode = true; + return this; + } + + /** + * When called, the built annotation overlay shall only return runtime-retained annotations; + * class-retained annotations are ignored. Note that this only applies to annotations present + * in class files (and therefore in Jandex); annotations added to the overlay using + * {@linkplain AnnotationTransformation annotation transformations} are not inspected + * and are always returned. + * + * @return this builder + */ + public Builder runtimeAnnotationsOnly() { + runtimeAnnotationsOnly = true; + return this; + } + + /** + * When called, the built annotation overlay shall return {@linkplain java.lang.annotation.Inherited inherited} + * annotations per the Java rules. + * + * @return this builder + */ + public Builder inheritedAnnotations() { + inheritedAnnotations = true; + return this; + } + + /** + * Builds and returns an annotation overlay based on the configuration of this builder. + * + * @return the annotation overlay, never {@code null} + */ + public AnnotationOverlay build() { + return new AnnotationOverlayImpl(index, compatibleMode, runtimeAnnotationsOnly, inheritedAnnotations, + annotationTransformations); + } + } +} diff --git a/core/src/main/java/org/jboss/jandex/AnnotationOverlayImpl.java b/core/src/main/java/org/jboss/jandex/AnnotationOverlayImpl.java new file mode 100644 index 00000000..e7bd1dfd --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/AnnotationOverlayImpl.java @@ -0,0 +1,354 @@ +package org.jboss.jandex; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +import org.jboss.jandex.AnnotationTransformation.TransformationContext; + +class AnnotationOverlayImpl implements AnnotationOverlay { + private static final Set SENTINEL = Collections.unmodifiableSet(new HashSet<>()); + + final IndexView index; + final boolean compatibleMode; + final boolean runtimeAnnotationsOnly; + final boolean inheritedAnnotations; + final List transformations; + final Map> overlay = new ConcurrentHashMap<>(); + + AnnotationOverlayImpl(IndexView index, boolean compatibleMode, boolean runtimeAnnotationsOnly, boolean inheritedAnnotations, + Collection annotationTransformations) { + this.index = index; + this.compatibleMode = compatibleMode; + this.runtimeAnnotationsOnly = runtimeAnnotationsOnly; + this.inheritedAnnotations = inheritedAnnotations; + if (!compatibleMode) { + for (AnnotationTransformation transformation : annotationTransformations) { + if (transformation.requiresCompatibleMode()) { + throw new IllegalStateException("Compatible mode required by " + transformation); + } + } + } + List transformations = new ArrayList<>(annotationTransformations); + transformations.sort(new Comparator() { + @Override + public int compare(AnnotationTransformation o1, AnnotationTransformation o2) { + return Integer.compare(o2.priority(), o1.priority()); + } + }); + this.transformations = transformations; + } + + @Override + public final IndexView index() { + return index; + } + + @Override + public final boolean hasAnnotation(Declaration declaration, DotName name) { + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + throw new UnsupportedOperationException(); + } + + Collection annotations = getAnnotationsFor(declaration); + for (AnnotationInstance annotation : annotations) { + if (annotation.name().equals(name)) { + return true; + } + } + + if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = index.getClassByName(declaration.asClass().superName()); + while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) { + for (AnnotationInstance annotation : getAnnotationsFor(clazz)) { + ClassInfo annotationClass = index.getClassByName(annotation.name()); + if (annotationClass != null + && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME) + && annotation.name().equals(name)) { + return true; + } + } + clazz = index.getClassByName(clazz.superName()); + } + } + + return false; + } + + @Override + public final boolean hasAnyAnnotation(Declaration declaration, Set names) { + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + throw new UnsupportedOperationException(); + } + + Collection annotations = getAnnotationsFor(declaration); + for (AnnotationInstance annotation : annotations) { + for (DotName name : names) { + if (annotation.name().equals(name)) { + return true; + } + } + } + + if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = index.getClassByName(declaration.asClass().superName()); + while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) { + for (AnnotationInstance annotation : getAnnotationsFor(clazz)) { + ClassInfo annotationClass = index.getClassByName(annotation.name()); + if (annotationClass != null && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)) { + for (DotName name : names) { + if (annotation.name().equals(name)) { + return true; + } + } + } + } + clazz = index.getClassByName(clazz.superName()); + } + } + + return false; + } + + @Override + public final AnnotationInstance annotation(Declaration declaration, DotName name) { + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + throw new UnsupportedOperationException(); + } + + Collection annotations = getAnnotationsFor(declaration); + for (AnnotationInstance annotation : annotations) { + if (annotation.name().equals(name)) { + return annotation; + } + } + + if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = index.getClassByName(declaration.asClass().superName()); + while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) { + for (AnnotationInstance annotation : getAnnotationsFor(clazz)) { + ClassInfo annotationClass = index.getClassByName(annotation.name()); + if (annotationClass != null + && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME) + && annotation.name().equals(name)) { + return annotation; + } + } + clazz = index.getClassByName(clazz.superName()); + } + } + + return null; + } + + @Override + public final Collection annotationsWithRepeatable(Declaration declaration, DotName name) { + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + throw new UnsupportedOperationException(); + } + + DotName containerName = null; + { + ClassInfo annotationClass = index.getClassByName(name); + if (annotationClass != null) { + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); + if (repeatable != null) { + containerName = repeatable.value().asClass().name(); + } + } + } + + List result = new ArrayList<>(); + for (AnnotationInstance annotation : getAnnotationsFor(declaration)) { + if (annotation.name().equals(name)) { + result.add(annotation); + } else if (annotation.name().equals(containerName)) { + AnnotationInstance[] nestedAnnotations = annotation.value().asNestedArray(); + for (AnnotationInstance nestedAnnotation : nestedAnnotations) { + result.add(AnnotationInstance.create(nestedAnnotation, annotation.target())); + } + } + } + + if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = index.getClassByName(declaration.asClass().superName()); + while (result.isEmpty() && clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) { + for (AnnotationInstance annotation : getAnnotationsFor(clazz)) { + ClassInfo annotationClass = index.getClassByName(annotation.name()); + if (annotationClass != null && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME)) { + if (annotation.name().equals(name)) { + result.add(annotation); + } else if (annotation.name().equals(containerName)) { + AnnotationInstance[] nestedAnnotations = annotation.value().asNestedArray(); + for (AnnotationInstance nestedAnnotation : nestedAnnotations) { + result.add(AnnotationInstance.create(nestedAnnotation, annotation.target())); + } + } + } + } + clazz = index.getClassByName(clazz.superName()); + } + } + + return Collections.unmodifiableList(result); + } + + @Override + public final Collection annotations(Declaration declaration) { + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + throw new UnsupportedOperationException(); + } + + Collection result = getAnnotationsFor(declaration); + + if (inheritedAnnotations && declaration.kind() == AnnotationTarget.Kind.CLASS) { + result = new ArrayList<>(result); + ClassInfo clazz = index.getClassByName(declaration.asClass().superName()); + while (clazz != null && !DotName.OBJECT_NAME.equals(clazz.name())) { + for (AnnotationInstance annotation : getAnnotationsFor(clazz)) { + ClassInfo annotationClass = index.getClassByName(annotation.name()); + if (annotationClass != null + && annotationClass.hasDeclaredAnnotation(DotName.INHERITED_NAME) + && result.stream().noneMatch(it -> it.name().equals(annotation.name()))) { + result.add(annotation); + } + } + clazz = index.getClassByName(clazz.superName()); + } + } + + return Collections.unmodifiableCollection(result); + } + + Set getAnnotationsFor(Declaration declaration) { + EquivalenceKey key = EquivalenceKey.of(declaration); + Set annotations = overlay.get(key); + + if (annotations == null) { + Collection original = new HashSet<>(getOriginalAnnotations(declaration)); + TransformationContextImpl transformationContext = new TransformationContextImpl(declaration, original); + for (AnnotationTransformation transformation : transformations) { + if (transformation.supports(declaration.kind())) { + transformation.apply(transformationContext); + } + } + Set result = transformationContext.annotations; + annotations = original.equals(result) ? SENTINEL : Collections.unmodifiableSet(result); + overlay.put(key, annotations); + } + + if (annotations == SENTINEL) { + annotations = getOriginalAnnotations(declaration); + } + return annotations; + } + + final Set getOriginalAnnotations(Declaration declaration) { + Set result = new HashSet<>(); + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD) { + for (AnnotationInstance annotation : declaration.asMethod().annotations()) { + if (annotation.target() != null + && (annotation.target().kind() == AnnotationTarget.Kind.METHOD + || annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) + && (!runtimeAnnotationsOnly || annotation.runtimeVisible())) { + result.add(annotation); + } + } + } else { + for (AnnotationInstance annotation : declaration.declaredAnnotations()) { + if (!runtimeAnnotationsOnly || annotation.runtimeVisible()) { + result.add(annotation); + } + } + } + return result; + } + + private static final class TransformationContextImpl implements TransformationContext { + private final Declaration declaration; + private final Set annotations; + + TransformationContextImpl(Declaration declaration, Collection annotations) { + this.declaration = declaration; + this.annotations = new HashSet<>(annotations); + } + + @Override + public Declaration declaration() { + return declaration; + } + + @Override + public Collection annotations() { + return annotations; + } + + @Override + public boolean hasAnnotation(Class annotationClass) { + Objects.requireNonNull(annotationClass); + return hasAnnotation(DotName.createSimple(annotationClass)); + } + + @Override + public boolean hasAnnotation(DotName annotationName) { + Objects.requireNonNull(annotationName); + for (AnnotationInstance annotation : annotations) { + if (annotation.name().equals(annotationName)) { + return true; + } + } + return false; + } + + @Override + public boolean hasAnnotation(Predicate predicate) { + Objects.requireNonNull(predicate); + for (AnnotationInstance annotation : annotations) { + if (predicate.test(annotation)) { + return true; + } + } + return false; + } + + @Override + public void add(Class annotationClass) { + Objects.requireNonNull(annotationClass); + annotations.add(AnnotationInstance.builder(annotationClass).build()); + } + + @Override + public void add(AnnotationInstance annotation) { + annotations.add(Objects.requireNonNull(annotation)); + } + + @Override + public void addAll(AnnotationInstance... annotations) { + Collections.addAll(this.annotations, Objects.requireNonNull(annotations)); + } + + @Override + public void addAll(Collection annotations) { + this.annotations.addAll(Objects.requireNonNull(annotations)); + } + + @Override + public void remove(Predicate predicate) { + annotations.removeIf(Objects.requireNonNull(predicate)); + } + + @Override + public void removeAll() { + annotations.clear(); + } + } +} diff --git a/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java b/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java new file mode 100644 index 00000000..c050e14c --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java @@ -0,0 +1,884 @@ +package org.jboss.jandex; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An annotation transformation. + * + * @see #priority() + * @see #supports(AnnotationTarget.Kind) + * @see #apply(TransformationContext) + * @see #builder() + * @see #forClasses() + * @see #forFields() + * @see #forMethods() + * @see #forMethodParameters() + * @see #forRecordComponents() + * @since 3.2.0 + */ +public interface AnnotationTransformation { + /** + * The default {@link #priority()} value: 1000. + */ + int DEFAULT_PRIORITY_VALUE = 1000; + + /** + * Returns the priority of this annotation transformation. Annotation transformations + * are applied in descending order of priority values (that is, transformation with + * higher priority value is executed sooner than transformation with smaller priority + * value). + *

+ * By default, the priority is {@link #DEFAULT_PRIORITY_VALUE}. + * + * @return the priority of this annotation transformation + */ + default int priority() { + return DEFAULT_PRIORITY_VALUE; + } + + /** + * Returns whether this annotation transformation supports given {@link AnnotationTarget.Kind kind} + * of declarations. A transformation is only {@linkplain #apply(TransformationContext) applied} + * if it supports the correct kind of declarations. + *

+ * By default, the transformation supports all declaration kinds. + * + * @param kind the kind of declaration, never {@code null} + * @return whether this annotation transformation should apply + */ + default boolean supports(AnnotationTarget.Kind kind) { + return true; + } + + /** + * Implements the actual annotation transformation. + * + * @param context the {@linkplain TransformationContext transformation context}, never {@code null} + */ + void apply(TransformationContext context); + + /** + * Returns whether this annotation transformation requires the annotation overlay to be + * in the {@linkplain AnnotationOverlay.Builder#compatibleMode() compatible mode}. + * When this method returns {@code true} and the annotation overlay is not set to be + * in the compatible mode, an exception is thrown during construction of the overlay. + *

+ * This method returns {@code false} by default and should be overridden sparingly. + * + * @return whether this transformation requires the annotation overlay to be in the compatible mode + */ + default boolean requiresCompatibleMode() { + return false; + } + + /** + * A transformation context. Passed as a singular parameter to {@link #apply(TransformationContext)}. + * + * @see #declaration() + * @see #annotations() + * @see #hasAnnotation(Class) + * @see #hasAnnotation(DotName) + * @see #hasAnnotation(Predicate) + * @see #add(Class) + * @see #add(AnnotationInstance) + * @see #addAll(AnnotationInstance...) + * @see #addAll(Collection) + * @see #remove(Predicate) + * @see #removeAll() + */ + interface TransformationContext { + /** + * Returns the declaration that is being transformed. + * + * @return the declaration that is being transformed + */ + Declaration declaration(); + + /** + * Returns the collection of annotations present on the declaration that is being transformed. + * Reflects all changes done by this annotation transformation and all annotation transformations + * executed prior to this one. + *

+ * Changes made directly to this collection and changes made through the other + * {@code TransformationContext} methods are interchangeable. + * + * @return the collection of annotations present on the declaration that is being transformed + */ + Collection annotations(); + + /** + * Returns whether the {@linkplain #annotations() current set of annotations} contains + * an annotation of given {@code annotationClass}. + * + * @param annotationClass the annotation class, must not be {@code null} + * @return whether the current set of annotations contains an annotation of given class + */ + boolean hasAnnotation(Class annotationClass); + + /** + * Returns whether the {@linkplain #annotations() current set of annotations} contains + * an annotation whose class has given {@code annotationName}. + * + * @param annotationName name of the annotation class, must not be {@code null} + * @return whether the current set of annotations contains an annotation of given class + */ + boolean hasAnnotation(DotName annotationName); + + /** + * Returns whether the {@linkplain #annotations() current set of annotations} contains + * an annotation that matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return whether the current set of annotations contains an annotation of given class + */ + boolean hasAnnotation(Predicate predicate); + + /** + * Adds an annotation of given {@code annotationClass} to + * the {@linkplain #annotations() current set of annotations}. + *

+ * The annotation type must have no members. + * + * @param annotationClass the class of annotation to add, must not be {@code null} + */ + void add(Class annotationClass); + + /** + * Adds the {@code annotation} to the {@linkplain #annotations() current set of annotations}. + * + * @param annotation the annotation to add, must not be {@code null} + */ + void add(AnnotationInstance annotation); + + /** + * Adds all {@code annotations} to the {@linkplain #annotations() current set of annotations}. + * + * @param annotations the annotations to add, must not be {@code null} + */ + void addAll(AnnotationInstance... annotations); + + /** + * Adds all {@code annotations} to the {@linkplain #annotations() current set of annotations}. + * + * @param annotations the annotations to add, must not be {@code null} + */ + void addAll(Collection annotations); + + /** + * Removes annotations that match given {@code predicate} from + * the {@linkplain #annotations() current set of annotations}. + * + * @param predicate the annotation predicate, must not be {@code null} + */ + void remove(Predicate predicate); + + /** + * Removes all annotations from {@linkplain #annotations() current set of annotations}. + */ + void removeAll(); + } + + // --- + + /** + * Returns a builder for annotation transformation of arbitrary declarations. + * + * @return a builder for annotation transformation of arbitrary declarations + */ + static DeclarationBuilder builder() { + return new DeclarationBuilder(); + } + + /** + * Returns a builder for annotation transformation of classes. + * + * @return a builder for annotation transformation of classes + */ + static ClassBuilder forClasses() { + return new ClassBuilder(); + } + + /** + * Returns a builder for annotation transformation of fields. + * + * @return a builder for annotation transformation of fields + */ + static FieldBuilder forFields() { + return new FieldBuilder(); + } + + /** + * Returns a builder for annotation transformation of methods. + * + * @return a builder for annotation transformation of methods + */ + static MethodBuilder forMethods() { + return new MethodBuilder(); + } + + /** + * Returns a builder for annotation transformation of method parameters. + * + * @return a builder for annotation transformation of method parameters + */ + static MethodParameterBuilder forMethodParameters() { + return new MethodParameterBuilder(); + } + + /** + * Returns a builder for annotation transformation of record components. + * + * @return a builder for annotation transformation of record components + */ + static RecordComponentBuilder forRecordComponents() { + return new RecordComponentBuilder(); + } + + /** + * Abstract class for {@linkplain AnnotationTransformation annotation transformation} builders. + * + * @see #priority(int) + * @see #whenAnyMatch(Class...) + * @see #whenAnyMatch(DotName...) + * @see #whenAnyMatch(List) + * @see #whenAnyMatch(Predicate) + * @see #whenAllMatch(Class...) + * @see #whenAllMatch(DotName...) + * @see #whenAllMatch(List) + * @see #whenAllMatch(Predicate) + * @see #whenNoneMatch(Class...) + * @see #whenNoneMatch(DotName...) + * @see #whenNoneMatch(List) + * @see #whenNoneMatch(Predicate) + * @see #when(Predicate) + * @see DeclarationBuilder + * @see ClassBuilder + * @see FieldBuilder + * @see MethodBuilder + * @see MethodParameterBuilder + * @see RecordComponentBuilder + * @param type of this builder + */ + abstract class Builder> { + private final AnnotationTarget.Kind kind; + + private int priority; + private Predicate predicate; + + Builder(AnnotationTarget.Kind kind) { + this.kind = kind; + this.priority = DEFAULT_PRIORITY_VALUE; + } + + /** + * Sets the priority of the built annotation transformation. + * By default, the priority is {@link #DEFAULT_PRIORITY_VALUE}. + * + * @param priority the priority + * @return this builder + */ + public final THIS priority(int priority) { + this.priority = priority; + return self(); + } + + @SafeVarargs + private static Predicate annotationPredicate(Class... classes) { + Objects.requireNonNull(classes); + return annotation -> { + String annotationName = annotation.name().toString(); + for (Class clazz : classes) { + if (annotationName.equals(clazz.getName())) { + return true; + } + } + return false; + }; + } + + private static Predicate annotationPredicate(DotName... classes) { + Objects.requireNonNull(classes); + return annotation -> { + DotName annotationName = annotation.name(); + for (DotName clazz : classes) { + if (annotationName.equals(clazz)) { + return true; + } + } + return false; + }; + } + + /** + * Adds a predicate that tests whether any of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * is of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + @SafeVarargs + public final THIS whenAnyMatch(Class... classes) { + Objects.requireNonNull(classes); + return whenAnyMatch(annotationPredicate(classes)); + } + + /** + * Adds a predicate that tests whether any of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * is of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenAnyMatch(DotName... classes) { + Objects.requireNonNull(classes); + return whenAnyMatch(annotationPredicate(classes)); + } + + /** + * Adds a predicate that tests whether any of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * is of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenAnyMatch(List classes) { + Objects.requireNonNull(classes); + return whenAnyMatch(classes.toArray(new DotName[0])); + } + + /** + * Adds a predicate that tests whether any of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * matches the given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenAnyMatch(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> { + Collection annotations = ctx.annotations(); + for (AnnotationInstance annotation : annotations) { + if (predicate.test(annotation)) { + return true; + } + } + return false; + }); + } + + /** + * Adds a predicate that tests whether all of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * are of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + @SafeVarargs + public final THIS whenAllMatch(Class... classes) { + Objects.requireNonNull(classes); + return whenAllMatch(annotationPredicate(classes)); + } + + /** + * Adds a predicate that tests whether all of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * are of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenAllMatch(DotName... classes) { + Objects.requireNonNull(classes); + return whenAllMatch(annotationPredicate(classes)); + } + + /** + * Adds a predicate that tests whether all of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * are of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenAllMatch(List classes) { + Objects.requireNonNull(classes); + return whenAllMatch(classes.toArray(new DotName[0])); + } + + /** + * Adds a predicate that tests whether all of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * match the given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenAllMatch(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> { + Collection annotations = ctx.annotations(); + for (AnnotationInstance annotation : annotations) { + if (!predicate.test(annotation)) { + return false; + } + } + return true; + }); + } + + /** + * Adds a predicate that tests whether none of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * is of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + @SafeVarargs + public final THIS whenNoneMatch(Class... classes) { + Objects.requireNonNull(classes); + return whenNoneMatch(annotationPredicate(classes)); + } + + /** + * Adds a predicate that tests whether none of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * is of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenNoneMatch(DotName... classes) { + Objects.requireNonNull(classes); + return whenNoneMatch(annotationPredicate(classes)); + } + + /** + * Adds a predicate that tests whether none of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * is of given {@code classes}. + * + * @param classes the annotation classes, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenNoneMatch(List classes) { + Objects.requireNonNull(classes); + return whenNoneMatch(classes.toArray(new DotName[0])); + } + + /** + * Adds a predicate that tests whether none of + * the {@linkplain TransformationContext#annotations() current set of annotations} + * matches the given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public final THIS whenNoneMatch(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> { + Collection annotations = ctx.annotations(); + for (AnnotationInstance annotation : annotations) { + if (predicate.test(annotation)) { + return false; + } + } + return true; + }); + } + + /** + * Adds a predicate to the list of predicates that will be tested before applying the transformation. + * If some of the predicates returns {@code false}, the transformation is not applied. In other words, + * the predicates are combined using logical and (conjunction). + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + */ + public THIS when(Predicate predicate) { + Objects.requireNonNull(predicate); + if (this.predicate == null) { + this.predicate = predicate; + } else { + this.predicate = this.predicate.and(predicate); + } + return self(); + } + + /** + * Builds an annotation transformation based on the given {@code transformation} function. + * + * @param transformation the transformation function, must not be {@code null} + * @return the built annotation transformation, never {@code null} + */ + public AnnotationTransformation transform(Consumer transformation) { + Objects.requireNonNull(transformation); + + return new AnnotationTransformation() { + @Override + public int priority() { + return priority; + } + + @Override + public boolean supports(AnnotationTarget.Kind kind) { + return Builder.this.kind == null || Builder.this.kind == kind; + } + + @Override + public void apply(TransformationContext context) { + if (predicate == null || predicate.test(context)) { + transformation.accept(context); + } + } + }; + } + + @SuppressWarnings("unchecked") + THIS self() { + return (THIS) this; + } + } + + /** + * A builder of {@linkplain AnnotationTransformation annotation transformations} for arbitrary declarations. + * + * @see #whenDeclaration(Predicate) + * @see Builder + */ + class DeclarationBuilder extends Builder { + DeclarationBuilder() { + super(null); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current declaration} + * matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public DeclarationBuilder whenDeclaration(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> predicate.test(ctx.declaration())); + } + } + + /** + * A builder of {@linkplain AnnotationTransformation annotation transformations} for classes. + * + * @see #whenClass(Class) + * @see #whenClass(DotName) + * @see #whenClass(Predicate) + * @see Builder + */ + class ClassBuilder extends Builder { + ClassBuilder() { + super(AnnotationTarget.Kind.CLASS); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current class} + * is the given {@code clazz}. + * + * @param clazz the class, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public ClassBuilder whenClass(Class clazz) { + Objects.requireNonNull(clazz); + return whenClass(DotName.createSimple(clazz)); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current class} + * has given {@code name}. + * + * @param name the class name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public ClassBuilder whenClass(DotName name) { + Objects.requireNonNull(name); + return whenClass(clazz -> clazz.name().equals(name)); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current class} + * matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public ClassBuilder whenClass(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> predicate.test(ctx.declaration().asClass())); + } + } + + /** + * A builder of {@linkplain AnnotationTransformation annotation transformations} for fields. + * + * @see #whenField(Class, String) + * @see #whenField(DotName, String) + * @see #whenField(Predicate) + * @see Builder + */ + class FieldBuilder extends Builder { + FieldBuilder() { + super(AnnotationTarget.Kind.FIELD); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current field} + * has given {@code name} and is declared on given {@code clazz}. + * + * @param clazz the class, must not be {@code null} + * @param name the field name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public FieldBuilder whenField(Class clazz, String name) { + Objects.requireNonNull(clazz); + return whenField(DotName.createSimple(clazz), name); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current field} + * has given {@code name} and is declared on given {@code clazz}. + * + * @param clazz the class name, must not be {@code null} + * @param name the field name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public FieldBuilder whenField(DotName clazz, String name) { + Objects.requireNonNull(clazz); + Objects.requireNonNull(name); + return whenField(field -> field.name().equals(name) && field.declaringClass().name().equals(clazz)); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current field} + * matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public FieldBuilder whenField(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> predicate.test(ctx.declaration().asField())); + } + } + + /** + * A builder of {@linkplain AnnotationTransformation annotation transformations} for methods. + * + * @see #whenMethod(Class, String) + * @see #whenMethod(DotName, String) + * @see #whenMethod(Predicate) + * @see Builder + */ + class MethodBuilder extends Builder { + MethodBuilder() { + super(AnnotationTarget.Kind.METHOD); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current method} + * has given {@code name} and is declared on given {@code clazz}. + * + * @param clazz the class, must not be {@code null} + * @param name the method name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public MethodBuilder whenMethod(Class clazz, String name) { + Objects.requireNonNull(clazz); + return whenMethod(DotName.createSimple(clazz), name); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current method} + * has given {@code name} and is declared on given {@code clazz}. + * + * @param clazz the class name, must not be {@code null} + * @param name the method name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public MethodBuilder whenMethod(DotName clazz, String name) { + Objects.requireNonNull(clazz); + Objects.requireNonNull(name); + return whenMethod(method -> method.name().equals(name) && method.declaringClass().name().equals(clazz)); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current method} + * matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public MethodBuilder whenMethod(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> predicate.test(ctx.declaration().asMethod())); + } + } + + /** + * A builder of {@linkplain AnnotationTransformation annotation transformations} for method parameters. + * + * @see #whenMethodParameter(Class, String) + * @see #whenMethodParameter(DotName, String) + * @see #whenMethodParameter(Predicate) + * @see Builder + */ + class MethodParameterBuilder extends Builder { + MethodParameterBuilder() { + super(AnnotationTarget.Kind.METHOD_PARAMETER); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current method parameter} + * belongs to a method with given {@code name} declared on given {@code clazz}. + * + * @param clazz the class, must not be {@code null} + * @param name the method name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public MethodParameterBuilder whenMethodParameter(Class clazz, String name) { + Objects.requireNonNull(clazz); + return whenMethodParameter(DotName.createSimple(clazz), name); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current method parameter} + * belongs to a method with given {@code name} declared on given {@code clazz}. + * + * @param clazz the class name, must not be {@code null} + * @param name the method name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public MethodParameterBuilder whenMethodParameter(DotName clazz, String name) { + Objects.requireNonNull(clazz); + Objects.requireNonNull(name); + return whenMethodParameter(param -> param.method().name().equals(name) + && param.method().declaringClass().name().equals(clazz)); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current method parameter} + * matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public MethodParameterBuilder whenMethodParameter(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> predicate.test(ctx.declaration().asMethodParameter())); + } + } + + /** + * A builder of {@linkplain AnnotationTransformation annotation transformations} for record components. + * + * @see #whenRecordComponent(Class, String) + * @see #whenRecordComponent(DotName, String) + * @see #whenRecordComponent(Predicate) + * @see Builder + */ + class RecordComponentBuilder extends Builder { + RecordComponentBuilder() { + super(AnnotationTarget.Kind.RECORD_COMPONENT); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current record component} + * has given {@code name} and is declared on given {@code clazz}. + * + * @param clazz the class, must not be {@code null} + * @param name the record component name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public RecordComponentBuilder whenRecordComponent(Class clazz, String name) { + Objects.requireNonNull(clazz); + return whenRecordComponent(DotName.createSimple(clazz), name); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current record component} + * has given {@code name} and is declared on given {@code clazz}. + * + * @param clazz the class name, must not be {@code null} + * @param name the record component name, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public RecordComponentBuilder whenRecordComponent(DotName clazz, String name) { + Objects.requireNonNull(clazz); + Objects.requireNonNull(name); + return whenRecordComponent(component -> component.name().equals(name) + && component.declaringClass().name().equals(clazz)); + } + + /** + * Adds a predicate that tests whether + * the {@linkplain TransformationContext#declaration() current record component} + * matches given {@code predicate}. + * + * @param predicate the predicate, must not be {@code null} + * @return this builder + * @see #when(Predicate) + */ + public RecordComponentBuilder whenRecordComponent(Predicate predicate) { + Objects.requireNonNull(predicate); + return when(ctx -> predicate.test(ctx.declaration().asRecordComponent())); + } + } +} diff --git a/core/src/main/java/org/jboss/jandex/ClassInfo.java b/core/src/main/java/org/jboss/jandex/ClassInfo.java index dc591a40..61c965d1 100644 --- a/core/src/main/java/org/jboss/jandex/ClassInfo.java +++ b/core/src/main/java/org/jboss/jandex/ClassInfo.java @@ -471,7 +471,7 @@ public final List annotationsWithRepeatable(DotName name, In if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); for (AnnotationInstance container : annotations(containingType.name())) { @@ -598,7 +598,7 @@ public final List declaredAnnotationsWithRepeatable(DotName if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); AnnotationInstance container = declaredAnnotation(containingType.name()); diff --git a/core/src/main/java/org/jboss/jandex/DotName.java b/core/src/main/java/org/jboss/jandex/DotName.java index 3a983852..04c3e146 100644 --- a/core/src/main/java/org/jboss/jandex/DotName.java +++ b/core/src/main/java/org/jboss/jandex/DotName.java @@ -49,6 +49,9 @@ public final class DotName implements Comparable { public static final DotName ENUM_NAME; public static final DotName RECORD_NAME; public static final DotName STRING_NAME; + public static final DotName INHERITED_NAME; + public static final DotName REPEATABLE_NAME; + public static final DotName RETENTION_NAME; private final DotName prefix; private final String local; @@ -64,6 +67,9 @@ public final class DotName implements Comparable { ENUM_NAME = new DotName(JAVA_LANG_NAME, "Enum", true, false); RECORD_NAME = new DotName(JAVA_LANG_NAME, "Record", true, false); STRING_NAME = new DotName(JAVA_LANG_NAME, "String", true, false); + INHERITED_NAME = new DotName(JAVA_LANG_ANNOTATION_NAME, "Inherited", true, false); + REPEATABLE_NAME = new DotName(JAVA_LANG_ANNOTATION_NAME, "Repeatable", true, false); + RETENTION_NAME = new DotName(DotName.JAVA_LANG_ANNOTATION_NAME, "Retention", true, false); } /** diff --git a/core/src/main/java/org/jboss/jandex/FieldInfo.java b/core/src/main/java/org/jboss/jandex/FieldInfo.java index 77fb7162..cdc81a34 100644 --- a/core/src/main/java/org/jboss/jandex/FieldInfo.java +++ b/core/src/main/java/org/jboss/jandex/FieldInfo.java @@ -198,7 +198,7 @@ public final List annotationsWithRepeatable(DotName name, In if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); for (AnnotationInstance container : annotations(containingType.name())) { @@ -302,7 +302,7 @@ public final List declaredAnnotationsWithRepeatable(DotName if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); AnnotationInstance container = declaredAnnotation(containingType.name()); diff --git a/core/src/main/java/org/jboss/jandex/Index.java b/core/src/main/java/org/jboss/jandex/Index.java index a35256a5..2ef6b2c4 100644 --- a/core/src/main/java/org/jboss/jandex/Index.java +++ b/core/src/main/java/org/jboss/jandex/Index.java @@ -67,8 +67,6 @@ public final class Index implements IndexView { private static final List EMPTY_ANNOTATION_LIST = Collections.emptyList(); private static final List EMPTY_CLASSINFO_LIST = Collections.emptyList(); - static final DotName REPEATABLE = DotName.createSimple("java.lang.annotation.Repeatable"); - final Map annotations; final Map subclasses; final Map subinterfaces; @@ -310,6 +308,8 @@ public static ClassInfo singleClass(InputStream classData) throws IOException { return index.getKnownClasses().iterator().next(); } + // --- + /** * {@inheritDoc} */ @@ -331,7 +331,7 @@ public Collection getAnnotationsWithRepeatable(DotName annot if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable == null) { // Not a repeatable annotation return getAnnotations(annotationName); diff --git a/core/src/main/java/org/jboss/jandex/MethodInfo.java b/core/src/main/java/org/jboss/jandex/MethodInfo.java index c0fcf4be..bd94fc1c 100644 --- a/core/src/main/java/org/jboss/jandex/MethodInfo.java +++ b/core/src/main/java/org/jboss/jandex/MethodInfo.java @@ -417,7 +417,7 @@ public final List annotationsWithRepeatable(DotName name, In if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); for (AnnotationInstance container : annotations(containingType.name())) { @@ -525,7 +525,7 @@ public List declaredAnnotationsWithRepeatable(DotName name, if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); AnnotationInstance container = declaredAnnotation(containingType.name()); diff --git a/core/src/main/java/org/jboss/jandex/MethodParameterInfo.java b/core/src/main/java/org/jboss/jandex/MethodParameterInfo.java index 2b278939..0e59e117 100644 --- a/core/src/main/java/org/jboss/jandex/MethodParameterInfo.java +++ b/core/src/main/java/org/jboss/jandex/MethodParameterInfo.java @@ -193,7 +193,7 @@ public List annotationsWithRepeatable(DotName name, IndexVie if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); for (AnnotationInstance container : annotations(containingType.name())) { @@ -296,7 +296,7 @@ public List declaredAnnotationsWithRepeatable(DotName name, if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); AnnotationInstance container = declaredAnnotation(containingType.name()); diff --git a/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java new file mode 100644 index 00000000..bbfd2ad7 --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java @@ -0,0 +1,128 @@ +package org.jboss.jandex; + +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * An {@link AnnotationOverlay} that can be freely mutated. The {@link #freeze()} operation + * returns a list of {@linkplain AnnotationTransformation annotation transformations} that + * can later be used to create an equivalent immutable annotation overlay. + * + * @since 3.2.0 + */ +public interface MutableAnnotationOverlay extends AnnotationOverlay { + /** + * Returns a new builder for a mutable annotation overlay for given {@code index}. + * + *

+ * Thread safety + * + *

+ * The object returned by the builder is not thread safe and should be confined to a single thread. + * After calling {@link #freeze()}, the object becomes immutable and can be shared between threads. + * + * @param index the Jandex index, must not be {@code null} + * @return the mutable annotation overlay builder, never {@code null} + */ + static MutableAnnotationOverlay.Builder builder(IndexView index) { + Objects.requireNonNull(index); + return new Builder(index); + } + + /** + * Adds given annotation instance to given {@code declaration}. When asking this annotation + * overlay about annotation information for given declaration, the results will include + * given annotation instance. + * + * @param declaration the declaration to modify, must not be {@code null} + * @param annotation the annotation instance to add to {@code declaration} for, must not be {@code null} + */ + void addAnnotation(Declaration declaration, AnnotationInstance annotation); + + /** + * Removes all annotations matching given {@code predicate} from given {@code declaration}. + * When asking this annotation overlay about annotation information for given declaration, + * the results will not include matching annotation instances. + * + * @param declaration the declaration to modify, must not be {@code null} + * @param predicate the annotation predicate, must not be {@code null} + */ + void removeAnnotations(Declaration declaration, Predicate predicate); + + /** + * Freezes this mutable annotation overlay and returns the annotation transformations to create + * an equivalent immutable annotation overlay. After freezing, the {@link #addAnnotation(Declaration, AnnotationInstance)} + * and {@link #removeAnnotations(Declaration, Predicate)} methods will throw an exception. + * + * @return immutable list of annotation transformations equivalent to mutations performed on this annotation overlay, + * never {@code null} + */ + List freeze(); + + /** + * The builder for a mutable annotation overlay. + */ + final class Builder { + private final IndexView index; + + private boolean compatibleMode; + private boolean runtimeAnnotationsOnly; + private boolean inheritedAnnotations; + + Builder(IndexView index) { + this.index = index; + } + + /** + * When called, the built annotation overlay shall treat method parameters as part of methods. + * This means that annotations on method parameters are returned when asking for annotations + * of a method, asking for annotations on method parameters results in an exception, and + * annotation transformations for methods are produced when adding/removing annotations + * to/from a method parameter. + *

+ * This method is called {@code compatibleMode} because the built annotation overlay is + * compatible with the previous implementation of the same concept in Quarkus. + * + * @return this builder + */ + public Builder compatibleMode() { + compatibleMode = true; + return this; + } + + /** + * When called, the built annotation overlay shall only return runtime-retained annotations; + * class-retained annotations are ignored. Note that this only applies to annotations present + * in class files (and therefore in Jandex); annotations added to the overlay using + * {@link #addAnnotation(Declaration, AnnotationInstance)} are not inspected and are always + * returned. + * + * @return this builder + */ + public Builder runtimeAnnotationsOnly() { + runtimeAnnotationsOnly = true; + return this; + } + + /** + * When called, the built annotation overlay shall return {@linkplain java.lang.annotation.Inherited inherited} + * annotations per the Java rules. + * + * @return this builder + */ + public Builder inheritedAnnotations() { + inheritedAnnotations = true; + return this; + } + + /** + * Builds and returns a mutable annotation overlay based on the configuration of this builder. + * + * @return the mutable annotation overlay, never {@code null} + */ + public MutableAnnotationOverlay build() { + return new MutableAnnotationOverlayImpl(index, compatibleMode, runtimeAnnotationsOnly, inheritedAnnotations); + } + } +} diff --git a/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlayImpl.java b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlayImpl.java new file mode 100644 index 00000000..e53b9f44 --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlayImpl.java @@ -0,0 +1,134 @@ +package org.jboss.jandex; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +final class MutableAnnotationOverlayImpl extends AnnotationOverlayImpl implements MutableAnnotationOverlay { + private volatile boolean frozen; + + MutableAnnotationOverlayImpl(IndexView index, boolean compatibleMode, boolean runtimeAnnotationsOnly, + boolean inheritedAnnotations) { + super(index, compatibleMode, runtimeAnnotationsOnly, inheritedAnnotations, Collections.emptyList()); + } + + @Override + Set getAnnotationsFor(Declaration declaration) { + EquivalenceKey key = EquivalenceKey.of(declaration); + Set annotations = overlay.get(key); + if (annotations == null) { + annotations = getOriginalAnnotations(declaration); + overlay.put(key, annotations); + } + return annotations; + } + + @Override + public void addAnnotation(Declaration declaration, AnnotationInstance annotation) { + if (frozen) { + throw new IllegalStateException("Mutable annotation overlay is already frozen"); + } + + if (annotation.target() == null) { + annotation = AnnotationInstance.create(annotation, declaration); + } + + getAnnotationsFor(declaration).add(annotation); + transformations.add(addTransformation(declaration, annotation)); + } + + private AnnotationTransformation addTransformation(Declaration declaration, AnnotationInstance annotation) { + AnnotationTarget.Kind declarationKind; + EquivalenceKey key; + + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + // the `annotation` has correct `target`, see `addAnnotation()` above, + // so we don't need to do anything else + declarationKind = AnnotationTarget.Kind.METHOD; + key = EquivalenceKey.of(declaration.asMethodParameter().method()); + } else { + declarationKind = declaration.kind(); + key = EquivalenceKey.of(declaration); + } + + return new AnnotationTransformation() { + @Override + public boolean supports(AnnotationTarget.Kind kind) { + return kind == declarationKind; + } + + @Override + public void apply(TransformationContext context) { + if (key.equals(EquivalenceKey.of(context.declaration()))) { + context.add(annotation); + } + } + + @Override + public boolean requiresCompatibleMode() { + return compatibleMode; + } + }; + } + + @Override + public void removeAnnotations(Declaration declaration, Predicate predicate) { + if (frozen) { + throw new IllegalStateException("Mutable annotation overlay is already frozen"); + } + + getAnnotationsFor(declaration).removeIf(predicate); + transformations.add(removeTransformation(declaration, predicate)); + } + + private AnnotationTransformation removeTransformation(Declaration declaration, Predicate predicate) { + AnnotationTarget.Kind declarationKind; + EquivalenceKey key; + Predicate finalPredicate; + + if (compatibleMode && declaration.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + declarationKind = AnnotationTarget.Kind.METHOD; + key = EquivalenceKey.of(declaration.asMethodParameter().method()); + int position = declaration.asMethodParameter().position(); + finalPredicate = new Predicate() { + @Override + public boolean test(AnnotationInstance annotation) { + return annotation.target() != null + && annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == position + && predicate.test(annotation); + } + }; + } else { + declarationKind = declaration.kind(); + key = EquivalenceKey.of(declaration); + finalPredicate = predicate; + } + + return new AnnotationTransformation() { + @Override + public boolean supports(AnnotationTarget.Kind kind) { + return kind == declarationKind; + } + + @Override + public void apply(TransformationContext context) { + if (key.equals(EquivalenceKey.of(context.declaration()))) { + context.remove(finalPredicate); + } + } + + @Override + public boolean requiresCompatibleMode() { + return compatibleMode; + } + }; + } + + @Override + public List freeze() { + frozen = true; + return Collections.unmodifiableList(transformations); + } +} diff --git a/core/src/main/java/org/jboss/jandex/RecordComponentInfo.java b/core/src/main/java/org/jboss/jandex/RecordComponentInfo.java index 63beaa05..14ec74e3 100644 --- a/core/src/main/java/org/jboss/jandex/RecordComponentInfo.java +++ b/core/src/main/java/org/jboss/jandex/RecordComponentInfo.java @@ -190,7 +190,7 @@ public final List annotationsWithRepeatable(DotName name, In if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); for (AnnotationInstance container : annotations(containingType.name())) { @@ -285,7 +285,7 @@ public final List declaredAnnotationsWithRepeatable(DotName if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); AnnotationInstance container = declaredAnnotation(containingType.name()); diff --git a/core/src/main/java/org/jboss/jandex/Type.java b/core/src/main/java/org/jboss/jandex/Type.java index 81c361b1..3ccb5c3d 100644 --- a/core/src/main/java/org/jboss/jandex/Type.java +++ b/core/src/main/java/org/jboss/jandex/Type.java @@ -468,7 +468,7 @@ public final List annotationsWithRepeatable(DotName name, In if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Not an annotation type: " + annotationClass); } - AnnotationInstance repeatable = annotationClass.declaredAnnotation(Index.REPEATABLE); + AnnotationInstance repeatable = annotationClass.declaredAnnotation(DotName.REPEATABLE_NAME); if (repeatable != null) { Type containingType = repeatable.value().asClass(); AnnotationInstance container = annotation(containingType.name()); diff --git a/core/src/test/java/org/jboss/jandex/test/AnnotationOverlayTest.java b/core/src/test/java/org/jboss/jandex/test/AnnotationOverlayTest.java new file mode 100644 index 00000000..ca0602ff --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/AnnotationOverlayTest.java @@ -0,0 +1,323 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationOverlay; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.Declaration; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.Index; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.junit.jupiter.api.Test; + +public class AnnotationOverlayTest { + @Retention(RetentionPolicy.CLASS) + @interface MyClassRetainedAnnotation { + } + + @Inherited + @Retention(RetentionPolicy.RUNTIME) + @interface MyInheritedAnnotation { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MyNotInheritedAnnotation { + String value(); + } + + @MyInheritedAnnotation("i") + @MyNotInheritedAnnotation("ni") + static class AnnotatedSuperClass { + } + + @MyAnnotation("c1") + @MyRepeatableAnnotation("cr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("cr2"), + @MyRepeatableAnnotation("cr3") + }) + @MyClassRetainedAnnotation + static class AnnotatedClass extends AnnotatedSuperClass { + @MyAnnotation("f1") + @MyRepeatableAnnotation("fr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("fr2"), + @MyRepeatableAnnotation("fr3") + }) + @MyClassRetainedAnnotation + Map> field; + + @MyAnnotation("m1") + @MyRepeatableAnnotation("mr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("mr2"), + @MyRepeatableAnnotation("mr3") + }) + @MyClassRetainedAnnotation + void method(@MyAnnotation("m2") @MyClassRetainedAnnotation Map> param, + @MyAnnotation("m3") @MyClassRetainedAnnotation int[] otherParam) { + } + } + + @Test + public void directTransformation_addAnnotation() throws IOException { + AnnotationTransformation transformation = new AnnotationTransformation() { + @Override + public boolean supports(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void apply(TransformationContext context) { + if (context.declaration().asClass().name().toString() + .equals("org.jboss.jandex.test.AnnotationOverlayTest$AnnotatedClass")) { + context.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C1").build()); + } + } + }; + + assertOverlay("c1_C1_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation); + } + + @Test + public void directTransformation_removeAnnotation() throws IOException { + AnnotationTransformation transformation = new AnnotationTransformation() { + @Override + public boolean supports(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void apply(TransformationContext context) { + if (context.declaration().asClass().name().toString() + .equals("org.jboss.jandex.test.AnnotationOverlayTest$AnnotatedClass")) { + context.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + } + } + }; + + assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation); + } + + @Test + public void directTransformation_addAndRemoveAnnotation() throws IOException { + AnnotationTransformation transformation = new AnnotationTransformation() { + @Override + public boolean supports(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void apply(TransformationContext context) { + if (context.declaration().asClass().name().toString() + .equals("org.jboss.jandex.test.AnnotationOverlayTest$AnnotatedClass")) { + context.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + context.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C2").build()); + } + } + }; + + assertOverlay("C2_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation); + } + + @Test + public void builtTransformation_addAnnotation() throws IOException { + AnnotationTransformation transformation = AnnotationTransformation.forClasses() + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .transform(ctx -> { + ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C3").build()); + }); + + assertOverlay("c1_C3_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation); + } + + @Test + public void builtTransformation_removeAnnotation() throws IOException { + AnnotationTransformation transformation = AnnotationTransformation.forClasses() + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .transform(ctx -> { + ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + }); + + assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation); + } + + @Test + public void builtTransformation_addAndRemoveAnnotation() throws IOException { + AnnotationTransformation transformation = AnnotationTransformation.forClasses() + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .transform(ctx -> { + ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C4").build()); + }); + + assertOverlay("C4_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation); + } + + @Test + public void multipleNonconflictingTransformations() throws IOException { + AnnotationTransformation transformation1 = AnnotationTransformation.forClasses() + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .transform(ctx -> { + ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("C5").build()); + }); + AnnotationTransformation transformation2 = AnnotationTransformation.forMethods() + .whenMethod(DotName.createSimple(AnnotatedClass.class), "method") + .transform(ctx -> { + ctx.add(AnnotationInstance.builder(MyOtherAnnotation.class).value("M1").build()); + }); + + assertOverlay("c1_C5_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_M1_mr1_mr2_mr3_m2_m3", transformation1, transformation2); + } + + @Test + public void multipleConflictingTransformations_firstBeforeSecond() throws IOException { + AnnotationTransformation transformation1 = AnnotationTransformation.forClasses() + .priority(10) + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .transform(ctx -> { + ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + }); + AnnotationTransformation transformation2 = AnnotationTransformation.forClasses() + .priority(1) + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .whenAnyMatch(MyAnnotation.DOT_NAME) + .transform(ctx -> { + ctx.add(AnnotationInstance.builder(MyOtherAnnotation.DOT_NAME).value("C6").build()); + }); + + assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation1, transformation2); + } + + @Test + public void multipleConflictingTransformations_secondBeforeFirst() throws IOException { + AnnotationTransformation transformation1 = AnnotationTransformation.forClasses() + .priority(1) + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .transform(ctx -> { + ctx.remove(annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + }); + AnnotationTransformation transformation2 = AnnotationTransformation.forClasses() + .priority(10) + .whenClass(DotName.createSimple(AnnotatedClass.class)) + .whenAnyMatch(MyAnnotation.DOT_NAME) + .transform(ctx -> { + ctx.add(AnnotationInstance.builder(MyOtherAnnotation.DOT_NAME).value("C7").build()); + }); + + assertOverlay("C7_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", transformation1, transformation2); + } + + private void assertOverlay(String expectedValues, AnnotationTransformation... transformations) throws IOException { + Index index = Index.of(AnnotatedSuperClass.class, AnnotatedClass.class, MyAnnotation.class, + MyOtherAnnotation.class, MyRepeatableAnnotation.class, MyRepeatableAnnotation.List.class, + MyClassRetainedAnnotation.class, MyInheritedAnnotation.class, MyNotInheritedAnnotation.class); + + for (boolean inheritedAnnotations : Arrays.asList(true, false)) { + for (boolean runtimeAnnotationsOnly : Arrays.asList(true, false)) { + AnnotationOverlay.Builder builder = AnnotationOverlay.builder(index, Arrays.asList(transformations)); + if (inheritedAnnotations) { + builder.inheritedAnnotations(); + } + if (runtimeAnnotationsOnly) { + builder.runtimeAnnotationsOnly(); + } + AnnotationOverlay overlay = builder.build(); + + StringBuilder values = new StringBuilder(); + + ClassInfo clazz = index.getClassByName(AnnotatedClass.class); + assertNotNull(clazz); + + assertFalse(overlay.hasAnnotation(clazz, MyNotInheritedAnnotation.class)); + assertNull(overlay.annotation(clazz, MyNotInheritedAnnotation.class)); + assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyNotInheritedAnnotation.class).size()); + + if (inheritedAnnotations) { + assertTrue(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class)); + assertNotNull(overlay.annotation(clazz, MyInheritedAnnotation.class)); + assertEquals("i", overlay.annotation(clazz, MyInheritedAnnotation.class).value().asString()); + assertEquals(1, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size()); + } else { + assertFalse(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class)); + assertNull(overlay.annotation(clazz, MyInheritedAnnotation.class)); + assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size()); + } + + FieldInfo field = clazz.field("field"); + assertNotNull(field); + + MethodInfo method = clazz.firstMethod("method"); + assertNotNull(method); + + MethodParameterInfo parameter1 = method.parameters().get(0); + assertNotNull(parameter1); + + MethodParameterInfo parameter2 = method.parameters().get(1); + assertNotNull(parameter2); + + for (Declaration declaration : Arrays.asList(clazz, field, method, parameter1, parameter2)) { + if (overlay.hasAnnotation(declaration, MyAnnotation.DOT_NAME)) { + values.append(overlay.annotation(declaration, MyAnnotation.DOT_NAME).value().asString()).append("_"); + } + if (overlay.hasAnnotation(declaration, MyOtherAnnotation.DOT_NAME)) { + values.append(overlay.annotation(declaration, MyOtherAnnotation.DOT_NAME).value().asString()) + .append("_"); + } + if (declaration != method) { + if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.DOT_NAME)) { + values.append(overlay.annotation(declaration, MyRepeatableAnnotation.DOT_NAME).value().asString()) + .append("_"); + } + if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.List.DOT_NAME)) { + AnnotationInstance annotation = overlay.annotation(declaration, + MyRepeatableAnnotation.List.DOT_NAME); + for (AnnotationInstance nestedAnnotation : annotation.value().asNestedArray()) { + values.append(nestedAnnotation.value().asString()).append("_"); + } + } + } else { // just to test `annotationsWithRepeatable`, no other reason + for (AnnotationInstance annotation : overlay.annotationsWithRepeatable(declaration, + MyRepeatableAnnotation.DOT_NAME)) { + values.append(annotation.value().asString()).append("_"); + } + } + + if (runtimeAnnotationsOnly) { + assertFalse(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class)); + assertNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class)); + assertEquals(0, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size()); + } else { + assertTrue(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class)); + assertNotNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class)); + assertEquals(1, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size()); + } + } + + if (values.length() > 0) { + values.deleteCharAt(values.length() - 1); + } + + assertEquals(expectedValues, values.toString()); + } + } + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/MutableAnnotationOverlayTest.java b/core/src/test/java/org/jboss/jandex/test/MutableAnnotationOverlayTest.java new file mode 100644 index 00000000..8e90c53d --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/MutableAnnotationOverlayTest.java @@ -0,0 +1,201 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.Declaration; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.MutableAnnotationOverlay; +import org.junit.jupiter.api.Test; + +public class MutableAnnotationOverlayTest { + @Retention(RetentionPolicy.CLASS) + @interface MyClassRetainedAnnotation { + } + + @Inherited + @Retention(RetentionPolicy.RUNTIME) + @interface MyInheritedAnnotation { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MyNotInheritedAnnotation { + String value(); + } + + @MyInheritedAnnotation("i") + @MyNotInheritedAnnotation("ni") + static class AnnotatedSuperClass { + } + + @MyAnnotation("c1") + @MyRepeatableAnnotation("cr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("cr2"), + @MyRepeatableAnnotation("cr3") + }) + @MyClassRetainedAnnotation + static class AnnotatedClass extends AnnotatedSuperClass { + @MyAnnotation("f1") + @MyRepeatableAnnotation("fr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("fr2"), + @MyRepeatableAnnotation("fr3") + }) + @MyClassRetainedAnnotation + Map> field; + + @MyAnnotation("m1") + @MyRepeatableAnnotation("mr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("mr2"), + @MyRepeatableAnnotation("mr3") + }) + @MyClassRetainedAnnotation + void method(@MyAnnotation("m2") @MyClassRetainedAnnotation Map> param, + @MyAnnotation("m3") @MyClassRetainedAnnotation int[] otherParam) { + } + } + + @Test + public void addAnnotation() throws IOException { + assertOverlay("c1_C1_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", (index, overlay) -> { + ClassInfo clazz = index.getClassByName("org.jboss.jandex.test.MutableAnnotationOverlayTest$AnnotatedClass"); + overlay.addAnnotation(clazz, AnnotationInstance.builder(MyOtherAnnotation.class).value("C1").build()); + }); + } + + @Test + public void removeAnnotation() throws IOException { + assertOverlay("cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", (index, overlay) -> { + ClassInfo clazz = index.getClassByName("org.jboss.jandex.test.MutableAnnotationOverlayTest$AnnotatedClass"); + overlay.removeAnnotations(clazz, annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + }); + } + + @Test + public void addAndRemoveAnnotation() throws IOException { + assertOverlay("C2_cr1_cr2_cr3_f1_fr1_fr2_fr3_m1_mr1_mr2_mr3_m2_m3", (index, overlay) -> { + ClassInfo clazz = index.getClassByName("org.jboss.jandex.test.MutableAnnotationOverlayTest$AnnotatedClass"); + overlay.removeAnnotations(clazz, annotation -> annotation.name().equals(MyAnnotation.DOT_NAME)); + overlay.addAnnotation(clazz, AnnotationInstance.builder(MyOtherAnnotation.class).value("C2").build()); + }); + } + + private void assertOverlay(String expectedValues, BiConsumer action) + throws IOException { + Index index = Index.of(AnnotatedSuperClass.class, AnnotatedClass.class, MyAnnotation.class, + MyOtherAnnotation.class, MyRepeatableAnnotation.class, MyRepeatableAnnotation.List.class, + MyClassRetainedAnnotation.class, MyInheritedAnnotation.class, MyNotInheritedAnnotation.class); + + for (boolean inheritedAnnotations : Arrays.asList(true, false)) { + for (boolean runtimeAnnotationsOnly : Arrays.asList(true, false)) { + MutableAnnotationOverlay.Builder builder = MutableAnnotationOverlay.builder(index); + if (inheritedAnnotations) { + builder.inheritedAnnotations(); + } + if (runtimeAnnotationsOnly) { + builder.runtimeAnnotationsOnly(); + } + MutableAnnotationOverlay overlay = builder.build(); + + action.accept(index, overlay); + + StringBuilder values = new StringBuilder(); + + ClassInfo clazz = index.getClassByName(AnnotatedClass.class); + assertNotNull(clazz); + + assertFalse(overlay.hasAnnotation(clazz, MyNotInheritedAnnotation.class)); + assertNull(overlay.annotation(clazz, MyNotInheritedAnnotation.class)); + assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyNotInheritedAnnotation.class).size()); + + if (inheritedAnnotations) { + assertTrue(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class)); + assertNotNull(overlay.annotation(clazz, MyInheritedAnnotation.class)); + assertEquals("i", overlay.annotation(clazz, MyInheritedAnnotation.class).value().asString()); + assertEquals(1, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size()); + } else { + assertFalse(overlay.hasAnnotation(clazz, MyInheritedAnnotation.class)); + assertNull(overlay.annotation(clazz, MyInheritedAnnotation.class)); + assertEquals(0, overlay.annotationsWithRepeatable(clazz, MyInheritedAnnotation.class).size()); + } + + FieldInfo field = clazz.field("field"); + assertNotNull(field); + + MethodInfo method = clazz.firstMethod("method"); + assertNotNull(method); + + MethodParameterInfo parameter1 = method.parameters().get(0); + assertNotNull(parameter1); + + MethodParameterInfo parameter2 = method.parameters().get(1); + assertNotNull(parameter2); + + for (Declaration declaration : Arrays.asList(clazz, field, method, parameter1, parameter2)) { + if (overlay.hasAnnotation(declaration, MyAnnotation.DOT_NAME)) { + values.append(overlay.annotation(declaration, MyAnnotation.DOT_NAME).value().asString()).append("_"); + } + if (overlay.hasAnnotation(declaration, MyOtherAnnotation.DOT_NAME)) { + values.append(overlay.annotation(declaration, MyOtherAnnotation.DOT_NAME).value().asString()) + .append("_"); + } + if (declaration != method) { + if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.DOT_NAME)) { + values.append(overlay.annotation(declaration, MyRepeatableAnnotation.DOT_NAME).value().asString()) + .append("_"); + } + if (overlay.hasAnnotation(declaration, MyRepeatableAnnotation.List.DOT_NAME)) { + AnnotationInstance annotation = overlay.annotation(declaration, + MyRepeatableAnnotation.List.DOT_NAME); + for (AnnotationInstance nestedAnnotation : annotation.value().asNestedArray()) { + values.append(nestedAnnotation.value().asString()).append("_"); + } + } + } else { // just to test `annotationsWithRepeatable`, no other reason + for (AnnotationInstance annotation : overlay.annotationsWithRepeatable(declaration, + MyRepeatableAnnotation.DOT_NAME)) { + values.append(annotation.value().asString()).append("_"); + } + } + + if (runtimeAnnotationsOnly) { + assertFalse(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class)); + assertNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class)); + assertEquals(0, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size()); + } else { + assertTrue(overlay.hasAnnotation(declaration, MyClassRetainedAnnotation.class)); + assertNotNull(overlay.annotation(declaration, MyClassRetainedAnnotation.class)); + assertEquals(1, overlay.annotationsWithRepeatable(declaration, MyClassRetainedAnnotation.class).size()); + } + } + + if (values.length() > 0) { + values.deleteCharAt(values.length() - 1); + } + + assertEquals(expectedValues, values.toString()); + } + } + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/MyOtherAnnotation.java b/core/src/test/java/org/jboss/jandex/test/MyOtherAnnotation.java new file mode 100644 index 00000000..135a8397 --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/MyOtherAnnotation.java @@ -0,0 +1,13 @@ +package org.jboss.jandex.test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.jboss.jandex.DotName; + +@Retention(RetentionPolicy.RUNTIME) +@interface MyOtherAnnotation { + String value(); + + DotName DOT_NAME = DotName.createSimple(MyOtherAnnotation.class.getName()); +}