From 95b2cbc7dd22cf10f4a5c12885749950fbceaf04 Mon Sep 17 00:00:00 2001 From: Siphalor Date: Fri, 20 Jun 2025 21:25:09 +0200 Subject: [PATCH] [annotation-inheritance, type-utils] Implement module for annotation inheritance --- gradle/libs.versions.toml | 3 + settings.gradle.kts | 1 + .../build.gradle.kts | 9 + .../api/AnnotationInheritance.java | 10 + ...ationInheritanceAwareAnnotatedElement.java | 67 +++ .../api/package-info.java | 4 + .../impl/AnnotationInheritanceResolver.java | 152 +++++++ .../impl/ByteArrayClassLoader.java | 23 ++ .../RepeatableAnnotationContainerHelper.java | 387 ++++++++++++++++++ .../impl/generated/package-info.java | 4 + .../impl/package-info.java | 6 + ...nInheritanceAwareAnnotatedElementTest.java | 145 +++++++ ...peatableAnnotationContainerHelperTest.java | 95 +++++ .../api/annotations/AnnotationRepeatType.java | 116 ++++++ .../LayeredTypeAnnotations.java | 24 +- .../api/annotations/package-info.java | 4 + .../tweed5/typeutils/api/type/ActualType.java | 1 + .../api/type/AnnotationRepeatType.java | 32 -- .../AnnotationRepeatTypeResolver.java | 18 +- .../impl/annotations/package-info.java | 6 + .../AnnotationRepeatTypeTest.java | 25 +- .../LayeredTypeAnnotationsTest.java | 20 +- .../typeutils/api/type/ActualTypeTest.java | 3 +- .../typeutils/test/JavaReflectionTests.java | 5 +- .../{api/type => test}/TestAnnotation.java | 2 +- .../{api/type => test}/TestAnnotations.java | 2 +- .../api/collection/ClassToInstanceMap.java | 6 +- .../pojo/api/weaving/CompoundPojoWeaver.java | 2 +- 28 files changed, 1092 insertions(+), 80 deletions(-) create mode 100644 tweed5-annotation-inheritance/build.gradle.kts create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritance.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElement.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/package-info.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/AnnotationInheritanceResolver.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/ByteArrayClassLoader.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelper.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/generated/package-info.java create mode 100644 tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/package-info.java create mode 100644 tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElementTest.java create mode 100644 tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelperTest.java create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatType.java rename tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/{type => annotations}/LayeredTypeAnnotations.java (75%) create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/package-info.java delete mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatType.java rename tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/{type => annotations}/AnnotationRepeatTypeResolver.java (88%) create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/package-info.java rename tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/{type => annotations}/AnnotationRepeatTypeTest.java (52%) rename tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/{type => annotations}/LayeredTypeAnnotationsTest.java (84%) rename tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/{api/type => test}/TestAnnotation.java (80%) rename tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/{api/type => test}/TestAnnotations.java (86%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75fac96..ab223a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] assertj = "3.26.3" +asm = "9.7" autoservice = "1.1.1" acl = "1.3.5" java-main = "8" @@ -17,6 +18,8 @@ lombok = { id = "io.freefair.lombok", version = "8.13.1" } [libraries] acl = { group = "commons-logging", name = "commons-logging", version.ref = "acl" } assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" } +asm-commons = { group = "org.ow2.asm", name = "asm-commons", version.ref = "asm" } +asm-core = { group = "org.ow2.asm", name = "asm", version.ref = "asm" } autoservice-annotations = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "autoservice" } autoservice-processor = { group = "com.google.auto.service", name = "auto-service", version.ref = "autoservice" } jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b6e522..89d0a38 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ rootProject.name = "tweed5" include("test-utils") +include("tweed5-annotation-inheritance") include("tweed5-construct") include("tweed5-core") include("tweed5-default-extensions") diff --git a/tweed5-annotation-inheritance/build.gradle.kts b/tweed5-annotation-inheritance/build.gradle.kts new file mode 100644 index 0000000..f353d8c --- /dev/null +++ b/tweed5-annotation-inheritance/build.gradle.kts @@ -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) +} diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritance.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritance.java new file mode 100644 index 0000000..fcf3bde --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritance.java @@ -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[] passOn() default {}; + Class[] override() default {}; +} diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElement.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElement.java new file mode 100644 index 0000000..0da2569 --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElement.java @@ -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 resolvedAnnotations; + + @Override + public @Nullable T getAnnotation(@NonNull Class 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 getOrResolveAnnotations() { + if (resolvedAnnotations == null) { + resolvedAnnotations = new AnnotationInheritanceResolver(inner).resolve(); + } + return resolvedAnnotations; + } +} diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/package-info.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/package-info.java new file mode 100644 index 0000000..1b8652a --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/api/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.annotationinheritance.api; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/AnnotationInheritanceResolver.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/AnnotationInheritanceResolver.java new file mode 100644 index 0000000..f099bec --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/AnnotationInheritanceResolver.java @@ -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, Aggregator> aggregators = new LinkedHashMap<>(); + private static final Set> IGNORED_META_ANNOTATIONS = new CopyOnWriteArraySet<>(Arrays.asList( + Target.class, + Retention.class, + AnnotationInheritance.class + )); + + public ClassToInstanceMap resolve() { + resolve(main, Collections.emptySet()); + + ClassToInstanceMap resolvedAnnotations = new ClassToInstanceMap<>(); + + List 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) 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) containerAnnotation.annotationType(), containerAnnotation); + } + } + + return resolvedAnnotations; + } + + private void resolve(AnnotatedElement annotatedElement, Set> overriden) { + AnnotationInheritance inheritanceConfig = annotatedElement.getAnnotation(AnnotationInheritance.class); + + Set> passOnAnnotations = null; + Set> overridenOnwards = new HashSet<>(overriden); + if (annotatedElement != main) { + if (inheritanceConfig == null || inheritanceConfig.passOn().length == 0) { + return; + } + passOnAnnotations = new HashSet<>(inheritanceConfig.passOn().length + 5); + for (Class 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 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 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 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 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 annotations; + } +} diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/ByteArrayClassLoader.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/ByteArrayClassLoader.java new file mode 100644 index 0000000..e163e8b --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/ByteArrayClassLoader.java @@ -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; + } +} diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelper.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelper.java new file mode 100644 index 0000000..129b57b --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelper.java @@ -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, Function> CACHE = new HashMap<>(); + private static final ReadWriteLock CACHE_LOCK = new ReentrantReadWriteLock(); + + @SuppressWarnings("unchecked") + public static C createContainer(R[] elements) { + if (elements.length == 0) { + throw new IllegalArgumentException("elements must not be empty"); + } + Class repeatableClass = (Class) elements[0].annotationType(); + AnnotationRepeatType repeatType = AnnotationRepeatType.getType(repeatableClass); + if (!(repeatType instanceof AnnotationRepeatType.Repeatable)) { + throw new IllegalArgumentException(repeatableClass.getName() + " is not a repeatable"); + } + Class containerClass + = ((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass(); + + CACHE_LOCK.readLock().lock(); + try { + Function constructor = CACHE.get(containerClass); + if (constructor != null) { + return (C) constructor.apply(elements); + } + } finally { + CACHE_LOCK.readLock().unlock(); + } + + Function 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 Function createContainerClassConstructor( + Class containerClass, + Class 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 byte[] createContainerClassBytes( + String generatedClassName, + Class containerClass, + Class 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, + "", + "(" + repeatableArrayClass.descriptorString() + ")V", + null, + null + ); + methodWriter.visitParameter("values", 0); + methodWriter.visitCode(); + methodWriter.visitVarInsn(Opcodes.ALOAD, 0); + methodWriter.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()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, + "", + "(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(); + } +} diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/generated/package-info.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/generated/package-info.java new file mode 100644 index 0000000..5ee7bdc --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/generated/package-info.java @@ -0,0 +1,4 @@ +/** + * Package for on-the-fly-generated classes. + */ +package de.siphalor.tweed5.annotationinheritance.impl.generated; diff --git a/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/package-info.java b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/package-info.java new file mode 100644 index 0000000..7f2d42e --- /dev/null +++ b/tweed5-annotation-inheritance/src/main/java/de/siphalor/tweed5/annotationinheritance/impl/package-info.java @@ -0,0 +1,6 @@ +@ApiStatus.Internal +@NullMarked +package de.siphalor.tweed5.annotationinheritance.impl; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElementTest.java b/tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElementTest.java new file mode 100644 index 0000000..ee260c1 --- /dev/null +++ b/tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/api/AnnotationInheritanceAwareAnnotatedElementTest.java @@ -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 {} +} diff --git a/tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelperTest.java b/tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelperTest.java new file mode 100644 index 0000000..495c495 --- /dev/null +++ b/tweed5-annotation-inheritance/src/test/java/de/siphalor/tweed5/annotationinheritance/impl/RepeatableAnnotationContainerHelperTest.java @@ -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 annotationType() { + return R.class; + } + } + + @Repeatable(Rs.class) + @Retention(RetentionPolicy.RUNTIME) + public @interface R { + int value(); + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface Rs { + R[] value(); + } +} diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatType.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatType.java new file mode 100644 index 0000000..969c2d8 --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatType.java @@ -0,0 +1,116 @@ +package de.siphalor.tweed5.typeutils.api.annotations; + +import de.siphalor.tweed5.typeutils.impl.annotations.AnnotationRepeatTypeResolver; +import lombok.*; +import org.jspecify.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@EqualsAndHashCode +@ToString +public abstract class AnnotationRepeatType { + public static AnnotationRepeatType getType(Class annotationClass) { + return AnnotationRepeatTypeResolver.getType(annotationClass); + } + + private final Class annotationClass; + + public abstract @Nullable Class alternativeAnnotationClass(); + + public static class NonRepeatable extends AnnotationRepeatType { + public NonRepeatable(Class annotationClass) { + super(annotationClass); + } + + @Override + public @Nullable Class alternativeAnnotationClass() { + return null; + } + } + + @Getter + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + public static class Repeatable extends AnnotationRepeatType { + private final Class containerAnnotationClass; + + public Repeatable( + Class annotationClass, + Class containerAnnotationClass + ) { + super(annotationClass); + this.containerAnnotationClass = containerAnnotationClass; + } + + public AnnotationRepeatType.RepeatableContainer containerRepeatType() { + return new RepeatableContainer(containerAnnotationClass, annotationClass()); + } + + @Override + public @Nullable Class alternativeAnnotationClass() { + return containerAnnotationClass; + } + } + + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + public static class RepeatableContainer extends AnnotationRepeatType { + @Getter + private final Class elementAnnotationClass; + @EqualsAndHashCode.Exclude + @ToString.Exclude + @Nullable + private MethodHandle valueHandle; + + public RepeatableContainer( + Class annotationClass, + Class elementAnnotationClass + ) { + super(annotationClass); + this.elementAnnotationClass = elementAnnotationClass; + } + + public AnnotationRepeatType.Repeatable elementRepeatType() { + return new Repeatable(elementAnnotationClass, annotationClass()); + } + + @Override + public @Nullable Class alternativeAnnotationClass() { + return elementAnnotationClass; + } + + public E[] elements(C containerAnnotation) { + if (valueHandle == null) { + try { + valueHandle = MethodHandles.lookup().findVirtual( + annotationClass(), + "value", + MethodType.methodType(elementAnnotationClass.arrayType()) + ); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to resolve value method of container annotation: " + containerAnnotation + + " (" + containerAnnotation.getClass().getName() + ")", + e + ); + } + } + + try { + //noinspection unchecked + return (E[]) valueHandle.invoke(containerAnnotation); + } catch (Throwable e) { + throw new RuntimeException( + "Unexpected exception when calling value method of container annotation: " + + containerAnnotation + "( " + containerAnnotation.getClass().getName() + ")", + e + ); + } + } + } +} diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotations.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/LayeredTypeAnnotations.java similarity index 75% rename from tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotations.java rename to tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/LayeredTypeAnnotations.java index f446337..7d65f2a 100644 --- a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotations.java +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/LayeredTypeAnnotations.java @@ -1,7 +1,7 @@ -package de.siphalor.tweed5.typeutils.api.type; +package de.siphalor.tweed5.typeutils.api.annotations; +import de.siphalor.tweed5.typeutils.api.type.TypeAnnotationLayer; import lombok.Value; -import org.jspecify.annotations.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; @@ -46,7 +46,8 @@ public class LayeredTypeAnnotations implements AnnotatedElement { return layers.get(0).annotatedElement.getAnnotation(annotationClass); } - Class altAnnotationClass = getRepeatAlternativeAnnotation(annotationClass); + Class altAnnotationClass = AnnotationRepeatType.getType(annotationClass) + .alternativeAnnotationClass(); for (Layer layer : layers) { T annotation = layer.annotatedElement.getAnnotation(annotationClass); @@ -75,7 +76,8 @@ public class LayeredTypeAnnotations implements AnnotatedElement { if (annotations.containsKey(layerAnnotationClass)) { continue; } - Class layerAltClass = getRepeatAlternativeAnnotation(layerAnnotationClass); + Class layerAltClass = AnnotationRepeatType.getType(layerAnnotationClass) + .alternativeAnnotationClass(); if (annotations.containsKey(layerAltClass)) { continue; } @@ -100,7 +102,8 @@ public class LayeredTypeAnnotations implements AnnotatedElement { if (annotations.containsKey(layerAnnotationClass)) { continue; } - Class layerAltClass = getRepeatAlternativeAnnotation(layerAnnotationClass); + Class layerAltClass = AnnotationRepeatType.getType(layerAnnotationClass) + .alternativeAnnotationClass(); if (annotations.containsKey(layerAltClass)) { continue; } @@ -110,17 +113,6 @@ public class LayeredTypeAnnotations implements AnnotatedElement { return annotations.values().toArray(new Annotation[0]); } - private static @Nullable Class getRepeatAlternativeAnnotation(Class annotationClass) { - AnnotationRepeatType annotationRepeatType = AnnotationRepeatType.getType(annotationClass); - Class altAnnotationClass = null; - if (annotationRepeatType instanceof AnnotationRepeatType.Repeatable) { - altAnnotationClass = ((AnnotationRepeatType.Repeatable) annotationRepeatType).containerAnnotation(); - } else if (annotationRepeatType instanceof AnnotationRepeatType.RepeatableContainer) { - altAnnotationClass = ((AnnotationRepeatType.RepeatableContainer) annotationRepeatType).componentAnnotation(); - } - return altAnnotationClass; - } - @Value private static class Layer { TypeAnnotationLayer layer; diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/package-info.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/package-info.java new file mode 100644 index 0000000..6b19555 --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/annotations/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.typeutils.api.annotations; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/ActualType.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/ActualType.java index 33fe409..bf442f3 100644 --- a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/ActualType.java +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/ActualType.java @@ -1,5 +1,6 @@ package de.siphalor.tweed5.typeutils.api.type; +import de.siphalor.tweed5.typeutils.api.annotations.LayeredTypeAnnotations; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatType.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatType.java deleted file mode 100644 index 14b10e5..0000000 --- a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatType.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.siphalor.tweed5.typeutils.api.type; - -import de.siphalor.tweed5.typeutils.impl.type.AnnotationRepeatTypeResolver; -import lombok.Value; - -import java.lang.annotation.Annotation; - -public interface AnnotationRepeatType { - static AnnotationRepeatType getType(Class annotationClass) { - return AnnotationRepeatTypeResolver.getType(annotationClass); - } - - class NonRepeatable implements AnnotationRepeatType { - private static final NonRepeatable INSTANCE = new NonRepeatable(); - - public static NonRepeatable instance() { - return INSTANCE; - } - - private NonRepeatable() {} - } - - @Value - class Repeatable implements AnnotationRepeatType { - Class containerAnnotation; - } - - @Value - class RepeatableContainer implements AnnotationRepeatType { - Class componentAnnotation; - } -} diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/type/AnnotationRepeatTypeResolver.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/AnnotationRepeatTypeResolver.java similarity index 88% rename from tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/type/AnnotationRepeatTypeResolver.java rename to tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/AnnotationRepeatTypeResolver.java index 95face4..39aa67f 100644 --- a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/type/AnnotationRepeatTypeResolver.java +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/AnnotationRepeatTypeResolver.java @@ -1,6 +1,6 @@ -package de.siphalor.tweed5.typeutils.impl.type; +package de.siphalor.tweed5.typeutils.impl.annotations; -import de.siphalor.tweed5.typeutils.api.type.AnnotationRepeatType; +import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType; import org.jspecify.annotations.Nullable; import java.lang.annotation.Annotation; @@ -33,9 +33,9 @@ public class AnnotationRepeatTypeResolver { if (container != null) { CACHE_LOCK.writeLock().lock(); try { - AnnotationRepeatType type = new AnnotationRepeatType.Repeatable(container); + AnnotationRepeatType type = new AnnotationRepeatType.Repeatable(annotationClass, container); CACHE.put(annotationClass, type); - CACHE.put(container, new AnnotationRepeatType.RepeatableContainer(annotationClass)); + CACHE.put(container, new AnnotationRepeatType.RepeatableContainer(container, annotationClass)); return type; } finally { CACHE_LOCK.writeLock().unlock(); @@ -45,22 +45,24 @@ public class AnnotationRepeatTypeResolver { if (component != null) { CACHE_LOCK.writeLock().lock(); try { - AnnotationRepeatType type = new AnnotationRepeatType.RepeatableContainer(component); + AnnotationRepeatType type = new AnnotationRepeatType.RepeatableContainer(annotationClass, component); CACHE.put(annotationClass, type); - CACHE.put(component, new AnnotationRepeatType.Repeatable(component)); + CACHE.put(component, new AnnotationRepeatType.Repeatable(component, annotationClass)); return type; } finally { CACHE_LOCK.writeLock().unlock(); } } + AnnotationRepeatType.NonRepeatable type = new AnnotationRepeatType.NonRepeatable(annotationClass); + CACHE_LOCK.writeLock().lock(); try { - CACHE.put(annotationClass, AnnotationRepeatType.NonRepeatable.instance()); + CACHE.put(annotationClass, type); } finally { CACHE_LOCK.writeLock().unlock(); } - return AnnotationRepeatType.NonRepeatable.instance(); + return type; } private static @Nullable Class getRepeatableContainerFromComponentAnnotation( diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/package-info.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/package-info.java new file mode 100644 index 0000000..8546935 --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/annotations/package-info.java @@ -0,0 +1,6 @@ +@ApiStatus.Internal +@NullMarked +package de.siphalor.tweed5.typeutils.impl.annotations; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatTypeTest.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatTypeTest.java similarity index 52% rename from tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatTypeTest.java rename to tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatTypeTest.java index e255bc2..2a098fb 100644 --- a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatTypeTest.java +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/annotations/AnnotationRepeatTypeTest.java @@ -1,5 +1,6 @@ -package de.siphalor.tweed5.typeutils.api.type; +package de.siphalor.tweed5.typeutils.api.annotations; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSources; @@ -17,21 +18,19 @@ class AnnotationRepeatTypeTest { @ValueSource(classes = {Override.class, ParameterizedTest.class}) void getTypeSimple(Class annotationClass) { AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass); - assertThat(type).isInstanceOf(AnnotationRepeatType.NonRepeatable.class); + assertThat(type).isEqualTo(new AnnotationRepeatType.NonRepeatable(annotationClass)); } - @ParameterizedTest - @ValueSource(classes = {ValueSources.class, Rs.class}) - void getTypeRepeatableContainer(Class annotationClass) { - AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass); - assertThat(type).isInstanceOf(AnnotationRepeatType.RepeatableContainer.class); + @Test + void getTypeRepeatableContainer() { + AnnotationRepeatType type = AnnotationRepeatType.getType(Rs.class); + assertThat(type).isEqualTo(new AnnotationRepeatType.RepeatableContainer(Rs.class, R.class)); } - @ParameterizedTest - @ValueSource(classes = {ValueSource.class, R.class}) - void getTypeRepeatableValue(Class annotationClass) { - AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass); - assertThat(type).isInstanceOf(AnnotationRepeatType.Repeatable.class); + @Test + void getTypeRepeatableValue() { + AnnotationRepeatType type = AnnotationRepeatType.getType(R.class); + assertThat(type).isEqualTo(new AnnotationRepeatType.Repeatable(R.class, Rs.class)); } @Repeatable(Rs.class) @@ -42,4 +41,4 @@ class AnnotationRepeatTypeTest { @interface Rs { R[] value(); } -} \ No newline at end of file +} diff --git a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotationsTest.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/annotations/LayeredTypeAnnotationsTest.java similarity index 84% rename from tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotationsTest.java rename to tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/annotations/LayeredTypeAnnotationsTest.java index d2a139e..10e3e9c 100644 --- a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotationsTest.java +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/annotations/LayeredTypeAnnotationsTest.java @@ -1,5 +1,9 @@ -package de.siphalor.tweed5.typeutils.api.type; +package de.siphalor.tweed5.typeutils.api.annotations; +import de.siphalor.tweed5.typeutils.test.TestAnnotation; +import de.siphalor.tweed5.typeutils.test.TestAnnotations; +import de.siphalor.tweed5.typeutils.api.type.TypeAnnotationLayer; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -14,7 +18,7 @@ class LayeredTypeAnnotationsTest { annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, A.class); annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); - assertThat(annotations.getAnnotation(TestAnnotation.class)) + Assertions.assertThat(annotations.getAnnotation(TestAnnotation.class)) .isNotNull() .extracting(TestAnnotation::value) .isEqualTo("a"); @@ -71,6 +75,10 @@ class LayeredTypeAnnotationsTest { .extracting(TestAnnotation::value) .isEqualTo("b"); assertThat(annotations.getAnnotation(TestAnnotations.class)).isNull(); + assertThat(annotations.getAnnotationsByType(TestAnnotation.class)) + .singleElement() + .extracting(TestAnnotation::value) + .isEqualTo("b"); } @Test @@ -88,6 +96,7 @@ class LayeredTypeAnnotationsTest { a -> assertThat(a.value()).isEqualTo("r1"), a -> assertThat(a.value()).isEqualTo("r2") ); + assertThat(annotations.getAnnotationsByType(TestAnnotation.class)).hasSize(2); } @Test @@ -100,6 +109,10 @@ class LayeredTypeAnnotationsTest { .asInstanceOf(type(TestAnnotation.class)) .extracting(TestAnnotation::value) .isEqualTo("b"); + assertThat(annotations.getAnnotationsByType(TestAnnotation.class)) + .singleElement() + .extracting(TestAnnotation::value) + .isEqualTo("b");; } @Test @@ -116,6 +129,7 @@ class LayeredTypeAnnotationsTest { a -> assertThat(a.value()).isEqualTo("r1"), a -> assertThat(a.value()).isEqualTo("r2") ); + assertThat(annotations.getAnnotationsByType(TestAnnotation.class)).hasSize(2); } @TestAnnotation("r1") @@ -130,4 +144,4 @@ class LayeredTypeAnnotationsTest { @TestAnnotation("b") private static class B { } -} \ No newline at end of file +} diff --git a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/ActualTypeTest.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/ActualTypeTest.java index 4de845e..3ef8160 100644 --- a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/ActualTypeTest.java +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/ActualTypeTest.java @@ -1,5 +1,6 @@ package de.siphalor.tweed5.typeutils.api.type; +import de.siphalor.tweed5.typeutils.test.TestAnnotation; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -178,4 +179,4 @@ class ActualTypeTest { @TestAnnotation("baseType") interface Value {} -} \ No newline at end of file +} diff --git a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/JavaReflectionTests.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/JavaReflectionTests.java index 8ed28d2..fab3e0c 100644 --- a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/JavaReflectionTests.java +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/JavaReflectionTests.java @@ -1,6 +1,5 @@ package de.siphalor.tweed5.typeutils.test; -import de.siphalor.tweed5.typeutils.api.type.TestAnnotation; import lombok.SneakyThrows; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.InstanceOfAssertFactories.type; /** - * Various test to demonstrate the workings of Java's reflection type system + * Various test to demonstrate the workings of Java's reflection annotations system */ public class JavaReflectionTests { @SneakyThrows @@ -31,7 +30,7 @@ public class JavaReflectionTests { assertThat(intsField.getAnnotatedType()) .asInstanceOf(type(AnnotatedParameterizedType.class)) .satisfies( - type -> Assertions.assertThat(type.getAnnotation(TestAnnotation.class)).isNotNull(), + type -> assertThat(type.getAnnotation(TestAnnotation.class)).isNotNull(), type -> assertThat(type.getAnnotatedActualTypeArguments()) .singleElement() .isInstanceOf(AnnotatedParameterizedType.class) diff --git a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotation.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/TestAnnotation.java similarity index 80% rename from tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotation.java rename to tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/TestAnnotation.java index 82ad8d5..65883f9 100644 --- a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotation.java +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/TestAnnotation.java @@ -1,4 +1,4 @@ -package de.siphalor.tweed5.typeutils.api.type; +package de.siphalor.tweed5.typeutils.test; import java.lang.annotation.*; diff --git a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotations.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/TestAnnotations.java similarity index 86% rename from tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotations.java rename to tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/TestAnnotations.java index 59c25bf..3d32726 100644 --- a/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotations.java +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/TestAnnotations.java @@ -1,4 +1,4 @@ -package de.siphalor.tweed5.typeutils.api.type; +package de.siphalor.tweed5.typeutils.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java index 6c7984c..ca6426a 100644 --- a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java @@ -43,7 +43,11 @@ public class ClassToInstanceMap implements Iterable @Nullable V put(V value) { - return (V) delegate.put((Class) value.getClass(), value); + return put((Class) value.getClass(), value); + } + + public @Nullable V put(Class key, U value) { + return (V) delegate.put(key, value); } public @Nullable V remove(Class key) { diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java index 9156864..e55759e 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java @@ -10,7 +10,7 @@ import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess; import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving; import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; import de.siphalor.tweed5.typeutils.api.type.ActualType; -import de.siphalor.tweed5.typeutils.api.type.LayeredTypeAnnotations; +import de.siphalor.tweed5.typeutils.api.annotations.LayeredTypeAnnotations; import de.siphalor.tweed5.typeutils.api.type.TypeAnnotationLayer; import de.siphalor.tweed5.weaver.pojo.impl.entry.StaticPojoCompoundConfigEntry; import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector;