[annotation-inheritance, type-utils] Implement module for annotation inheritance

This commit is contained in:
2025-06-20 21:25:09 +02:00
parent 694fb85c31
commit 95b2cbc7dd
28 changed files with 1092 additions and 80 deletions

View File

@@ -0,0 +1,9 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
implementation(project(":tweed5-utils"))
implementation(project(":tweed5-type-utils"))
implementation(libs.asm.core)
}

View File

@@ -0,0 +1,10 @@
package de.siphalor.tweed5.annotationinheritance.api;
import java.lang.annotation.*;
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationInheritance {
Class<? extends Annotation>[] passOn() default {};
Class<? extends Annotation>[] override() default {};
}

View File

@@ -0,0 +1,67 @@
package de.siphalor.tweed5.annotationinheritance.api;
import de.siphalor.tweed5.annotationinheritance.impl.AnnotationInheritanceResolver;
import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType;
import de.siphalor.tweed5.utils.api.collection.ClassToInstanceMap;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
@RequiredArgsConstructor
public class AnnotationInheritanceAwareAnnotatedElement implements AnnotatedElement {
private final AnnotatedElement inner;
private @Nullable ClassToInstanceMap<Annotation> resolvedAnnotations;
@Override
public @Nullable <T extends Annotation> T getAnnotation(@NonNull Class<T> annotationClass) {
if (resolvedAnnotations != null) {
return resolvedAnnotations.get(annotationClass);
}
Annotation[] annotations = inner.getAnnotations();
boolean metaEncountered = false;
T foundAnnotation = null;
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(annotationClass)) {
//noinspection unchecked
foundAnnotation = (T) annotation;
} else if (!metaEncountered) {
metaEncountered = annotation.annotationType().isAnnotationPresent(AnnotationInheritance.class);
}
}
if (foundAnnotation != null) {
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(annotationClass);
if (repeatType instanceof AnnotationRepeatType.NonRepeatable) {
return foundAnnotation;
}
}
if (!metaEncountered) {
return foundAnnotation;
}
return getOrResolveAnnotations().get(annotationClass);
}
@Override
public @NotNull Annotation[] getAnnotations() {
return getOrResolveAnnotations().values().toArray(new Annotation[0]);
}
@Override
public @NotNull Annotation[] getDeclaredAnnotations() {
return inner.getDeclaredAnnotations();
}
private ClassToInstanceMap<Annotation> getOrResolveAnnotations() {
if (resolvedAnnotations == null) {
resolvedAnnotations = new AnnotationInheritanceResolver(inner).resolve();
}
return resolvedAnnotations;
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.annotationinheritance.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,152 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import de.siphalor.tweed5.annotationinheritance.api.AnnotationInheritance;
import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType;
import de.siphalor.tweed5.utils.api.collection.ClassToInstanceMap;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
@RequiredArgsConstructor
public class AnnotationInheritanceResolver {
private final AnnotatedElement main;
private final Map<Class<? extends Annotation>, Aggregator> aggregators = new LinkedHashMap<>();
private static final Set<Class<? extends Annotation>> IGNORED_META_ANNOTATIONS = new CopyOnWriteArraySet<>(Arrays.asList(
Target.class,
Retention.class,
AnnotationInheritance.class
));
public ClassToInstanceMap<Annotation> resolve() {
resolve(main, Collections.emptySet());
ClassToInstanceMap<Annotation> resolvedAnnotations = new ClassToInstanceMap<>();
List<Aggregator> aggregatorList = new ArrayList<>(aggregators.values());
for (int i = aggregatorList.size() - 1; i >= 0; i--) {
Aggregator aggregator = aggregatorList.get(i);
if (aggregator.annotations.size() == 1) {
//noinspection unchecked
resolvedAnnotations.put(
(Class<Annotation>) aggregator.repeatType.annotationClass(),
aggregator.annotations.iterator().next()
);
} else if (!aggregator.annotations.isEmpty()) {
Annotation[] annotations = (Annotation[]) Array.newInstance(
aggregator.repeatType.annotationClass(),
aggregator.annotations.size()
);
int j = aggregator.annotations.size() - 1;
for (Annotation annotation : aggregator.annotations) {
annotations[j--] = annotation;
}
Annotation containerAnnotation = RepeatableAnnotationContainerHelper.createContainer(annotations);
//noinspection unchecked
resolvedAnnotations.put((Class<Annotation>) containerAnnotation.annotationType(), containerAnnotation);
}
}
return resolvedAnnotations;
}
private void resolve(AnnotatedElement annotatedElement, Set<Class<? extends Annotation>> overriden) {
AnnotationInheritance inheritanceConfig = annotatedElement.getAnnotation(AnnotationInheritance.class);
Set<Class<? extends Annotation>> passOnAnnotations = null;
Set<Class<? extends Annotation>> overridenOnwards = new HashSet<>(overriden);
if (annotatedElement != main) {
if (inheritanceConfig == null || inheritanceConfig.passOn().length == 0) {
return;
}
passOnAnnotations = new HashSet<>(inheritanceConfig.passOn().length + 5);
for (Class<? extends Annotation> passOn : inheritanceConfig.passOn()) {
passOnAnnotations.add(passOn);
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(passOn);
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
passOnAnnotations.add(((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass());
}
}
}
if (inheritanceConfig != null) {
for (Class<? extends Annotation> override : inheritanceConfig.override()) {
overridenOnwards.add(override);
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(override);
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
overridenOnwards.add(((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass());
}
}
}
Annotation[] annotations = annotatedElement.getAnnotations();
for (int i = annotations.length - 1; i >= 0; i--) {
Annotation annotation = annotations[i];
if ((passOnAnnotations != null && !passOnAnnotations.contains(annotation.annotationType()))
|| IGNORED_META_ANNOTATIONS.contains(annotation.annotationType())
|| overriden.contains(annotation.annotationType())) {
continue;
}
Aggregator aggregator = aggregators.get(annotation.annotationType());
AnnotationRepeatType repeatType;
if (aggregator != null) {
repeatType = aggregator.repeatType;
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
aggregator.annotations.add(annotation);
}
} else {
repeatType = AnnotationRepeatType.getType(annotation.annotationType());
if (repeatType instanceof AnnotationRepeatType.NonRepeatable) {
aggregator = new Aggregator(repeatType, Collections.singleton(annotation));
aggregators.put(annotation.annotationType(), aggregator);
overridenOnwards.add(annotation.annotationType());
} else if (repeatType instanceof AnnotationRepeatType.Repeatable) {
ArrayList<Annotation> repeatableAnnotations = new ArrayList<>();
repeatableAnnotations.add(annotation);
aggregator = new Aggregator(repeatType, repeatableAnnotations);
aggregators.put(annotation.annotationType(), aggregator);
} else if (repeatType instanceof AnnotationRepeatType.RepeatableContainer) {
AnnotationRepeatType.RepeatableContainer containerRepeatType
= (AnnotationRepeatType.RepeatableContainer) repeatType;
Class<? extends Annotation> elementAnnotationType = containerRepeatType.elementAnnotationClass();
Annotation[] elements = containerRepeatType.elements(annotation);
aggregator = aggregators.get(elementAnnotationType);
if (aggregator != null) {
for (int j = elements.length - 1; j >= 0; j--) {
aggregator.annotations.add(elements[j]);
}
} else {
List<Annotation> repeatedAnnotations = new ArrayList<>(elements.length);
for (int e = elements.length - 1; e >= 0; e--) {
repeatedAnnotations.add(elements[e]);
}
aggregators.put(
containerRepeatType.elementAnnotationClass(),
new Aggregator(containerRepeatType.elementRepeatType(), repeatedAnnotations)
);
}
}
}
if (repeatType instanceof AnnotationRepeatType.NonRepeatable && annotation.annotationType()
.isAnnotationPresent(AnnotationInheritance.class)) {
resolve(annotation.annotationType(), overridenOnwards);
}
}
}
@Value
private static class Aggregator {
AnnotationRepeatType repeatType;
Collection<Annotation> annotations;
}
}

View File

@@ -0,0 +1,23 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import org.jspecify.annotations.Nullable;
class ByteArrayClassLoader extends ClassLoader {
public static Class<?> loadClass(@Nullable String binaryClassName, byte[] byteCode) {
return new ByteArrayClassLoader(ByteArrayClassLoader.class.getClassLoader()).createClass(
binaryClassName,
byteCode
);
}
public ByteArrayClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> createClass(@Nullable String binaryClassName, byte[] byteCode) {
Class<?> clazz = defineClass(binaryClassName, byteCode, 0, byteCode.length);
resolveClass(clazz);
return clazz;
}
}

View File

@@ -0,0 +1,387 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.objectweb.asm.*;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class RepeatableAnnotationContainerHelper {
/**
* Class version to use for generation (Java 8)
*/
private static final int CLASS_VERSION = Opcodes.V1_8;
private static final String GENERATED_PACKAGE = RepeatableAnnotationContainerHelper.class.getPackage().getName()
+ ".generated";
private static String generateUniqueIdentifier() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replace("-", "");
}
private static final Map<Class<? extends Annotation>, Function<Annotation[], Annotation>> CACHE = new HashMap<>();
private static final ReadWriteLock CACHE_LOCK = new ReentrantReadWriteLock();
@SuppressWarnings("unchecked")
public static <R extends Annotation, C extends Annotation> C createContainer(R[] elements) {
if (elements.length == 0) {
throw new IllegalArgumentException("elements must not be empty");
}
Class<R> repeatableClass = (Class<R>) elements[0].annotationType();
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(repeatableClass);
if (!(repeatType instanceof AnnotationRepeatType.Repeatable)) {
throw new IllegalArgumentException(repeatableClass.getName() + " is not a repeatable");
}
Class<? extends Annotation> containerClass
= ((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass();
CACHE_LOCK.readLock().lock();
try {
Function<Annotation[], Annotation> constructor = CACHE.get(containerClass);
if (constructor != null) {
return (C) constructor.apply(elements);
}
} finally {
CACHE_LOCK.readLock().unlock();
}
Function<R[], ? extends Annotation> constructor = createContainerClassConstructor(
containerClass,
repeatableClass
);
CACHE_LOCK.writeLock().lock();
try {
//noinspection rawtypes
CACHE.put(containerClass, (Function) constructor);
} finally {
CACHE_LOCK.writeLock().unlock();
}
return (C) constructor.apply(elements);
}
@SuppressWarnings("unchecked")
private static <R extends Annotation, C extends Annotation> Function<R[], C> createContainerClassConstructor(
Class<C> containerClass,
Class<R> repeatableClass
) {
try {
if (!containerClass.isAnnotation()) {
throw new IllegalArgumentException(containerClass.getName() + " is not a container annotation");
}
String generatedClassName = GENERATED_PACKAGE + ".RepeatableContainer$" + generateUniqueIdentifier();
byte[] bytes = createContainerClassBytes(generatedClassName, containerClass, repeatableClass);
Class<?>
generatedClass
= new ByteArrayClassLoader(RepeatableAnnotationContainerHelper.class.getClassLoader())
.createClass(generatedClassName, bytes);
MethodHandle constructorHandle = MethodHandles.lookup().findConstructor(
generatedClass,
MethodType.methodType(void.class, repeatableClass.arrayType())
);
return (repeatedValues) -> {
try {
return (C) constructorHandle.invoke((Object) repeatedValues);
} catch (Throwable e) {
throw new RuntimeException("Failed to instantiate generated container annotation", e);
}
};
} catch (Exception e) {
throw new IllegalStateException("Class generation failed", e);
}
}
static <R extends Annotation, C extends Annotation> byte[] createContainerClassBytes(
String generatedClassName,
Class<C> containerClass,
Class<R> repeatableClass
) {
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
String generatedClassNameInternal = generatedClassName.replace('.', '/');
classWriter.visit(
CLASS_VERSION,
Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
generatedClassNameInternal,
null,
"java/lang/Object",
new String[]{containerClass.getName().replace('.', '/')}
);
Class<?> repeatableArrayClass = repeatableClass.arrayType();
classWriter.visitField(Opcodes.ACC_PRIVATE, "values", repeatableArrayClass.descriptorString(), null, null);
appendConstructor(classWriter, repeatableArrayClass, generatedClassNameInternal);
appendValueMethod(classWriter, repeatableArrayClass, generatedClassNameInternal);
appendEqualsMethod(classWriter, repeatableArrayClass, containerClass, generatedClassNameInternal);
appendHashCodeMethod(classWriter, repeatableArrayClass, generatedClassNameInternal);
appendToStringMethod(classWriter, repeatableArrayClass, containerClass, generatedClassNameInternal);
appendAnnotationTypeMethod(classWriter, containerClass);
classWriter.visitEnd();
return classWriter.toByteArray();
}
private static void appendConstructor(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"<init>",
"(" + repeatableArrayClass.descriptorString() + ")V",
null,
null
);
methodWriter.visitParameter("values", 0);
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitVarInsn(Opcodes.ALOAD, 1);
methodWriter.visitFieldInsn(
Opcodes.PUTFIELD,
generatedClassNameInternal,
"values",
repeatableArrayClass.descriptorString()
);
methodWriter.visitInsn(Opcodes.RETURN);
methodWriter.visitMaxs(2, 2);
methodWriter.visitEnd();
}
private static void appendValueMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"value",
"()" + repeatableArrayClass.descriptorString(),
null,
null
);
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
repeatableArrayClass.descriptorString()
);
methodWriter.visitInsn(Opcodes.ARETURN);
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
private static void appendEqualsMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
Class<?> containerAnnotationClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"equals",
"(Ljava/lang/Object;)Z",
null,
null
);
methodWriter.visitParameter("other", 0);
Label falseLabel = new Label();
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 1);
String containerAnnotationClassBinaryName = Type.getInternalName(containerAnnotationClass);
methodWriter.visitTypeInsn(Opcodes.INSTANCEOF, containerAnnotationClassBinaryName);
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
repeatableArrayClass.descriptorString()
);
methodWriter.visitVarInsn(Opcodes.ALOAD, 1);
methodWriter.visitMethodInsn(
Opcodes.INVOKEINTERFACE,
containerAnnotationClassBinaryName,
"value",
"()" + repeatableArrayClass.descriptorString(),
true
);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Arrays",
"equals",
"([Ljava/lang/Object;[Ljava/lang/Object;)Z",
false
);
methodWriter.visitInsn(Opcodes.IRETURN);
methodWriter.visitLabel(falseLabel);
methodWriter.visitLdcInsn(false);
methodWriter.visitInsn(Opcodes.IRETURN);
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private static void appendHashCodeMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"hashCode",
"()I",
null,
null
);
final int keyHashCode = "value".hashCode() * 127;
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
repeatableArrayClass.descriptorString()
);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Arrays",
"hashCode",
"([Ljava/lang/Object;)I",
false
);
methodWriter.visitLdcInsn(keyHashCode);
methodWriter.visitInsn(Opcodes.IXOR);
methodWriter.visitInsn(Opcodes.IRETURN);
methodWriter.visitMaxs(2, 2);
methodWriter.visitEnd();
}
private static void appendToStringMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
Class<?> containerAnnotationClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"toString",
"()Ljava/lang/String;",
null,
null
);
String prefix = "@" + containerAnnotationClass.getName() + "(value=";
String suffix = ")";
String stringBuilderBinaryName = StringBuilder.class.getName().replace('.', '/');
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
repeatableArrayClass.descriptorString()
);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Arrays",
"toString",
"([Ljava/lang/Object;)Ljava/lang/String;",
false
);
methodWriter.visitInsn(Opcodes.DUP);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/String",
"length",
"()I",
false
);
methodWriter.visitLdcInsn(prefix.length() + suffix.length());
methodWriter.visitInsn(Opcodes.IADD);
methodWriter.visitTypeInsn(Opcodes.NEW, stringBuilderBinaryName);
methodWriter.visitInsn(Opcodes.DUP_X1); // S, I, SB -> S, SB, I, SB
methodWriter.visitInsn(Opcodes.SWAP); // S, SB, I, SB -> S, SB, SB, I
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
stringBuilderBinaryName,
"<init>",
"(I)V",
false
);
methodWriter.visitLdcInsn(prefix);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"append",
"(Ljava/lang/CharSequence;)L" + stringBuilderBinaryName + ";",
false
);
methodWriter.visitInsn(Opcodes.SWAP); // S, SB -> SB, S
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"append",
"(Ljava/lang/String;)L" + stringBuilderBinaryName + ";",
false
);
methodWriter.visitLdcInsn(suffix);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"append",
"(Ljava/lang/CharSequence;)L" + stringBuilderBinaryName + ";",
false
);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"toString",
"()Ljava/lang/String;",
false
);
methodWriter.visitInsn(Opcodes.ARETURN);
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private static void appendAnnotationTypeMethod(
ClassWriter classWriter,
Class<?> containerAnnotationClass
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"annotationType",
"()Ljava/lang/Class;",
null,
null
);
methodWriter.visitCode();
methodWriter.visitLdcInsn(Type.getType(containerAnnotationClass));
methodWriter.visitInsn(Opcodes.ARETURN);
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
}

View File

@@ -0,0 +1,4 @@
/**
* Package for on-the-fly-generated classes.
*/
package de.siphalor.tweed5.annotationinheritance.impl.generated;

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.annotationinheritance.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,145 @@
package de.siphalor.tweed5.annotationinheritance.api;
import org.junit.jupiter.api.Test;
import java.lang.annotation.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.array;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
class AnnotationInheritanceAwareAnnotatedElementTest {
@Test
void getAnnotationAForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(A.class))
.isNotNull()
.extracting(A::value)
.isEqualTo(1);
}
@Test
void getAnnotationBForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(B.class))
.isNotNull()
.extracting(B::value)
.isEqualTo(2);
}
@Test
void getAnnotationCForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(C.class))
.isNotNull()
.extracting(C::value)
.isEqualTo(10);
}
@Test
void getAnnotationRForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(R.class)).isNull();
}
@Test
void getAnnotationRsForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
Rs rs = element.getAnnotation(Rs.class);
assertThat(rs)
.isNotNull()
.extracting(Rs::value)
.asInstanceOf(array(R[].class))
.satisfiesExactly(
r -> assertThat(r.value()).isEqualTo(4),
r -> assertThat(r.value()).isEqualTo(2),
r -> assertThat(r.value()).isEqualTo(3),
r -> assertThat(r.value()).isEqualTo(10)
);
}
@Test
void getAnnotationsForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotations())
.satisfiesExactlyInAnyOrder(
a -> assertThat(a).isInstanceOf(BequeatherThree.class),
a -> assertThat(a).isInstanceOf(BequeatherTwo.class),
a -> assertThat(a).isInstanceOf(BequeatherOne.class),
a -> assertThat(a).asInstanceOf(type(A.class)).extracting(A::value).isEqualTo(1),
a -> assertThat(a).asInstanceOf(type(B.class)).extracting(B::value).isEqualTo(2),
a -> assertThat(a).asInstanceOf(type(C.class)).extracting(C::value).isEqualTo(10),
a -> assertThat(a).asInstanceOf(type(Rs.class))
.extracting(Rs::value)
.asInstanceOf(array(R[].class))
.satisfiesExactly(
r -> assertThat(r).extracting(R::value).isEqualTo(4),
r -> assertThat(r).extracting(R::value).isEqualTo(2),
r -> assertThat(r).extracting(R::value).isEqualTo(3),
r -> assertThat(r).extracting(R::value).isEqualTo(10)
)
);
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface A {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface B {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface C {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Repeatable(Rs.class)
public @interface R {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface Rs {
R[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@A(1)
@B(1)
@C(1)
@R(1)
@R(2)
@AnnotationInheritance(passOn = {A.class, B.class, C.class, R.class})
public @interface BequeatherOne {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@BequeatherOne
@B(2)
@R(2)
@R(3)
@AnnotationInheritance(passOn = {BequeatherOne.class, B.class, R.class}, override = R.class)
public @interface BequeatherTwo {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@R(4)
@AnnotationInheritance(passOn = {R.class, A.class})
public @interface BequeatherThree {}
@BequeatherThree
@BequeatherTwo
@C(10)
@R(10)
public static class Target1 {}
}

View File

@@ -0,0 +1,95 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@RepeatableAnnotationContainerHelperTest.R(1)
@RepeatableAnnotationContainerHelperTest.R(2)
@RepeatableAnnotationContainerHelperTest.R(3)
class RepeatableAnnotationContainerHelperTest {
@Test
@Disabled("Dumping the class is only for debugging purposes")
@SneakyThrows
void dumpClass() {
byte[] bytes = RepeatableAnnotationContainerHelper.createContainerClassBytes(
"de.siphalor.tweed5.annotationinheritance.impl.generated.DumpIt",
Rs.class,
R.class
);
Path path = Path.of(getClass().getSimpleName() + ".class");
Files.write(path, bytes);
System.out.println("Dumped to " + path.toAbsolutePath());
}
@Test
void test() {
R[] elements = {new RImpl(1), new RImpl(2), new RImpl(3)};
Annotation result = RepeatableAnnotationContainerHelper.createContainer(elements);
assertThat(result).asInstanceOf(type(Rs.class))
.extracting(Rs::value)
.isEqualTo(elements);
assertThat(result.annotationType()).isEqualTo(Rs.class);
assertThat(result.toString()).containsSubsequence("Rs(value=", "1", "2", "3");
Rs ref = RepeatableAnnotationContainerHelperTest.class.getAnnotation(Rs.class);
assertThat(result.equals(ref)).isTrue();
assertThat(result.hashCode()).isEqualTo(ref.hashCode());
}
@RequiredArgsConstructor
@Getter
public static class RImpl implements R {
private final int value;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof R) {
return ((R) obj).value() == this.value();
}
return false;
}
@Override
public int hashCode() {
return (127 * "value".hashCode()) ^ Integer.valueOf(value).hashCode();
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public Class<? extends Annotation> annotationType() {
return R.class;
}
}
@Repeatable(Rs.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface R {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Rs {
R[] value();
}
}