[annotation-inheritance, type-utils] Implement module for annotation inheritance
This commit is contained in:
9
tweed5-annotation-inheritance/build.gradle.kts
Normal file
9
tweed5-annotation-inheritance/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.annotationinheritance.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Package for on-the-fly-generated classes.
|
||||
*/
|
||||
package de.siphalor.tweed5.annotationinheritance.impl.generated;
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.annotationinheritance.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user