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..6403c79a
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/AnnotationOverlay.java
@@ -0,0 +1,242 @@
+package org.jboss.jandex;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 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
+ */
+public interface AnnotationOverlay {
+ /**
+ * Returns a new builder for an annotation overlay for given {@code index} and a given list
+ * 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, must not be {@code null}
+ * @return the annotation overlay builder, never {@code null}
+ */
+ static Builder builder(IndexView index, Collection annotationTransformations) {
+ return new Builder(Objects.requireNonNull(index), Objects.requireNonNull(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.
+ *
+ * @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 type ({@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.
+ *
+ * @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 extends Annotation> clazz) {
+ return hasAnnotation(declaration, DotName.createSimple(clazz.getName()));
+ }
+
+ /**
+ * 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.
+ *
+ * @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 extends Annotation>... classes) {
+ List names = new ArrayList<>(classes.length);
+ for (Class extends Annotation> clazz : classes) {
+ names.add(DotName.createSimple(clazz.getName()));
+ }
+ return hasAnyAnnotation(declaration, names);
+ }
+
+ /**
+ * 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.
+ *
+ * @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, Iterable 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.
+ *
+ * @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 type ({@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.
+ *
+ * @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 extends Annotation> 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.
+ *
+ * @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.
+ *
+ * @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 extends Annotation> 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.
+ *
+ * @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..a9dec73d
--- /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, Iterable 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 extends Annotation> 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 extends Annotation> 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..cd944fca
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/AnnotationTransformation.java
@@ -0,0 +1,807 @@
+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)
+ * @since 3.2
+ */
+public interface AnnotationTransformation {
+ /**
+ * The default {@link #priority()} value: 1000.
+ */
+ int DEFAULT_PRIORITY = 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}.
+ *
+ * @return the priority of this annotation transformation
+ */
+ default int priority() {
+ return DEFAULT_PRIORITY;
+ }
+
+ /**
+ * 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 extends Annotation> 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 extends Annotation> 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 Builder> {
+ private final AnnotationTarget.Kind kind;
+
+ private int priority;
+ private Predicate predicate;
+
+ Builder(AnnotationTarget.Kind kind) {
+ this.kind = kind;
+ this.priority = DEFAULT_PRIORITY;
+ }
+
+ /**
+ * Sets the priority of the built annotation transformation.
+ * By default, the priority is {@link #DEFAULT_PRIORITY}.
+ *
+ * @param priority the priority
+ * @return this builder
+ */
+ public final THIS priority(int priority) {
+ this.priority = priority;
+ return self();
+ }
+
+ @SafeVarargs
+ private static Predicate annotationPredicate(Class extends Annotation>... classes) {
+ Objects.requireNonNull(classes);
+ return annotation -> {
+ String annotationName = annotation.name().toString();
+ for (Class extends Annotation> 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 extends Annotation>... 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 extends Annotation>... 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 extends Annotation>... 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;
+ }
+ }
+
+ 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()));
+ }
+ }
+
+ 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()));
+ }
+ }
+
+ 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()));
+ }
+ }
+
+ 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()));
+ }
+ }
+
+ 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()));
+ }
+ }
+
+ 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/DotName.java b/core/src/main/java/org/jboss/jandex/DotName.java
index 1bc66b8a..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,7 @@ 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;
@@ -66,6 +67,7 @@ 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/Index.java b/core/src/main/java/org/jboss/jandex/Index.java
index b76517bb..2ef6b2c4 100644
--- a/core/src/main/java/org/jboss/jandex/Index.java
+++ b/core/src/main/java/org/jboss/jandex/Index.java
@@ -308,6 +308,8 @@ public static ClassInfo singleClass(InputStream classData) throws IOException {
return index.getKnownClasses().iterator().next();
}
+ // ---
+
/**
* {@inheritDoc}
*/
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..d58a4809
--- /dev/null
+++ b/core/src/main/java/org/jboss/jandex/MutableAnnotationOverlay.java
@@ -0,0 +1,126 @@
+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
+ */
+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) {
+ return new Builder(Objects.requireNonNull(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
+ */
+ 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/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());
+}