From e30e6d05474687ffe6c02a9a0a2628bdef774fd9 Mon Sep 17 00:00:00 2001 From: Siphalor Date: Tue, 4 Mar 2025 23:47:40 +0100 Subject: [PATCH] [type-utils, weaver-pojo] Introduce a submodule focused on Java types for POJO weaving --- settings.gradle.kts | 1 + tweed5-type-utils/build.gradle.kts | 3 + .../tweed5/typeutils/api/type/ActualType.java | 275 ++++++++++++++++++ .../api/type/AnnotationRepeatType.java | 33 +++ .../api/type/LayeredTypeAnnotations.java | 130 +++++++++ .../api/type/TypeAnnotationLayer.java | 7 + .../tweed5/typeutils/impl/package-info.java | 5 + .../type/AnnotationRepeatTypeResolver.java | 106 +++++++ .../typeutils/api/type/ActualTypeTest.java | 181 ++++++++++++ .../api/type/AnnotationRepeatTypeTest.java | 45 +++ .../api/type/LayeredTypeAnnotationsTest.java | 133 +++++++++ .../typeutils/api/type/TestAnnotation.java | 10 + .../typeutils/api/type/TestAnnotations.java | 12 + .../typeutils/test/JavaReflectionTests.java | 63 ++++ .../serde/api/ReadWritePojoPostProcessor.java | 16 +- tweed5-weaver-pojo/build.gradle.kts | 1 + .../pojo/api/annotation/CompoundWeaving.java | 2 +- .../weaver/pojo/api/weaving/Annotations.java | 140 --------- .../pojo/api/weaving/CompoundPojoWeaver.java | 54 ++-- .../pojo/api/weaving/TrivialPojoWeaver.java | 5 +- .../api/weaving/TweedPojoWeavingFunction.java | 5 +- .../pojo/api/weaving/WeavingContext.java | 10 +- .../weaving/TweedPojoWeaverBootstrapper.java | 16 +- .../api/weaving/CompoundPojoWeaverTest.java | 16 +- .../pojo/test/ConfigEntryAssertions.java | 14 +- 25 files changed, 1079 insertions(+), 204 deletions(-) create mode 100644 tweed5-type-utils/build.gradle.kts create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/ActualType.java create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatType.java create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotations.java create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/TypeAnnotationLayer.java create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/package-info.java create mode 100644 tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/type/AnnotationRepeatTypeResolver.java create mode 100644 tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/ActualTypeTest.java create mode 100644 tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatTypeTest.java create mode 100644 tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotationsTest.java create mode 100644 tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotation.java create mode 100644 tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotations.java create mode 100644 tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/JavaReflectionTests.java delete mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java diff --git a/settings.gradle.kts b/settings.gradle.kts index 188916e..09617f3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ include("tweed5-patchwork") include("tweed5-serde-api") include("tweed5-serde-extension") include("tweed5-serde-hjson") +include("tweed5-type-utils") include("tweed5-utils") include("tweed5-weaver-pojo") include("tweed5-weaver-pojo-serde-extension") diff --git a/tweed5-type-utils/build.gradle.kts b/tweed5-type-utils/build.gradle.kts new file mode 100644 index 0000000..5bc3d71 --- /dev/null +++ b/tweed5-type-utils/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("de.siphalor.tweed5.base-module") +} 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 new file mode 100644 index 0000000..af45a95 --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/ActualType.java @@ -0,0 +1,275 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Represents a runtime type with the parameters and annotations that are actually in use. + * + * @param the type represented by this class + */ +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class ActualType implements AnnotatedElement { + /** + * The raw {@link Class} that the type has been originally declared as. + */ + @Getter + private final Class declaredType; + /** + * The {@link AnnotatedType} that represents the type that is actually in use (without parameters). + */ + @Getter(AccessLevel.PROTECTED) + @Nullable + private final AnnotatedType usedType; + /** + * The {@link AnnotatedParameterizedType} that represents the type that is actually in use with parameters. + */ + @Nullable + private final AnnotatedParameterizedType usedParameterizedType; + + /** + * A representation of the layered annotations of this type. + * These usually consist of the annotations on {@link #declaredType()} combined with those from {@link #usedType()} + */ + private final LayeredTypeAnnotations layeredTypeAnnotations; + + /** + * Internal cache for the resolved actual type parameters. + */ + @Nullable + private List<@NotNull ActualType> resolvedParameters; + + /** + * Creates a basic actual type from just a declared class. + */ + public static ActualType ofClass(Class clazz) { + return new ActualType<>( + clazz, + null, + null, + LayeredTypeAnnotations.of(TypeAnnotationLayer.TYPE_DECLARATION, clazz) + ); + } + + /** + * Creates an actual type from a Java type usage. + * + * @throws UnsupportedOperationException when the given annotated type is not yet supported by this class + */ + public static ActualType ofUsedType(@NotNull AnnotatedType annotatedType) throws UnsupportedOperationException { + Class clazz = getDeclaredClassForUsedType(annotatedType); + + LayeredTypeAnnotations layeredTypeAnnotations = new LayeredTypeAnnotations(); + layeredTypeAnnotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, clazz); + layeredTypeAnnotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, annotatedType); + + if (annotatedType instanceof AnnotatedParameterizedType) { + return new ActualType<>(clazz, annotatedType, (AnnotatedParameterizedType) annotatedType, layeredTypeAnnotations); + } else { + return new ActualType<>(clazz, annotatedType, null, layeredTypeAnnotations); + } + } + + /** + * Resolves the declared {@link Class} of the {@link AnnotatedType} as Java has no generic way to do that. + * + * @throws UnsupportedOperationException if the given parameter is not supported yet + */ + private static @NotNull Class getDeclaredClassForUsedType(@NotNull AnnotatedType annotatedType) throws UnsupportedOperationException { + if (annotatedType.getType() instanceof Class) { + return (Class) annotatedType.getType(); + } else if (annotatedType.getType() instanceof ParameterizedType) { + return (Class) ((ParameterizedType) annotatedType.getType()).getRawType(); + } else if (annotatedType instanceof AnnotatedWildcardType) { + AnnotatedType[] upperBounds = ((AnnotatedWildcardType) annotatedType).getAnnotatedUpperBounds(); + if (upperBounds.length == 1) { + return getDeclaredClassForUsedType(upperBounds[0]); + } + return Object.class; + } else { + throw new UnsupportedOperationException( + "Failed to resolve raw class of annotated type: " + annotatedType + " (" + annotatedType.getClass() + ")" + ); + } + } + + @Override + public A getAnnotation(@NotNull Class annotationClass) { + return layeredTypeAnnotations.getAnnotation(annotationClass); + } + + @Override + public @NotNull Annotation @NotNull [] getAnnotations() { + return layeredTypeAnnotations.getAnnotations(); + } + + @Override + public @NotNull Annotation @NotNull [] getDeclaredAnnotations() { + return layeredTypeAnnotations.getDeclaredAnnotations(); + } + + /** + * Resolves the type parameters of this type as {@link ActualType}s. + */ + public @NotNull List<@NotNull ActualType> parameters() { + if (resolvedParameters != null) { + return resolvedParameters; + } else if (usedParameterizedType == null) { + int paramCount = declaredType.getTypeParameters().length; + if (paramCount == 0) { + resolvedParameters = Collections.emptyList(); + } else { + resolvedParameters = new ArrayList<>(paramCount); + for (int i = 0; i < paramCount; i++) { + resolvedParameters.add(ActualType.ofClass(Object.class)); + } + } + } else { + resolvedParameters = Arrays.stream(usedParameterizedType.getAnnotatedActualTypeArguments()) + .map(ActualType::ofUsedType) + .collect(Collectors.toList()); + } + return resolvedParameters; + } + + /** + * Resolves the actual type parameters of a super-class or super-interface of this type. + * @param targetClass the class to check + * @return the list of type parameters if the given class is assignable from this type or {@code null} if not + */ + public @Nullable List> getTypesOfSuperArguments(@NotNull Class targetClass) { + if (targetClass.getTypeParameters().length == 0) { + if (targetClass.isAssignableFrom(declaredType)) { + return Collections.emptyList(); + } else { + return null; + } + } + + ActualType superType = getViewOnSuperType(targetClass, this); + if (superType == null) { + return null; + } + return superType.parameters(); + } + + private static @Nullable ActualType getViewOnSuperType( + Class targetClass, + ActualType currentType + ) { + Class currentClass = currentType.declaredType(); + if (currentClass == targetClass) { + return currentType; + } + + List<@NotNull ActualType> currentParameters = currentType.parameters(); + + Map paramMap; + if (currentParameters.isEmpty()) { + paramMap = Collections.emptyMap(); + } else { + paramMap = new HashMap<>(); + for (int i = 0; i < currentParameters.size(); i++) { + paramMap.put(currentClass.getTypeParameters()[i].getName(), currentParameters.get(i).usedType()); + } + } + + if (targetClass.isInterface()) { + for (AnnotatedType annotatedInterface : currentClass.getAnnotatedInterfaces()) { + ActualType interfaceType = resolveTypeWithParameters(annotatedInterface, paramMap); + @Nullable ActualType resultType = getViewOnSuperType(targetClass, interfaceType); + if (resultType != null) { + return resultType; + } + } + } + if (currentClass != Object.class && !currentClass.isInterface()) { + ActualType superType = resolveTypeWithParameters(currentClass.getAnnotatedSuperclass(), paramMap); + @Nullable ActualType resultType = getViewOnSuperType(targetClass, superType); + if (resultType != null) { + return resultType; + } + } + return null; + } + + private static ActualType resolveTypeWithParameters(AnnotatedType annotatedType, Map parameters) { + if (annotatedType instanceof AnnotatedTypeVariable) { + ActualType actualType = ofUsedType(parameters.get(annotatedType.getType().getTypeName())); + actualType.layeredTypeAnnotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, annotatedType); + return actualType; + } else if (annotatedType instanceof AnnotatedParameterizedType) { + List> resolvedParameters = Arrays.stream(((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments()) + .map(p -> resolveTypeWithParameters(p, parameters)) + .collect(Collectors.toList()); + ActualType actualType = ofUsedType(annotatedType); + actualType.resolvedParameters = resolvedParameters; + return actualType; + } else { + return ofUsedType(annotatedType); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ActualType)) { + return false; + } else if (usedParameterizedType != null) { + return usedParameterizedType.equals(((ActualType) obj).usedParameterizedType); + } else if (usedType != null) { + return usedType.equals(((ActualType) obj).usedType); + } else { + return declaredType.equals(((ActualType) obj).declaredType); + } + } + + @Override + public int hashCode() { + return getMostSpecificTypeObject().hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (usedType != null) { + appendAnnotationsToString(sb, usedType.getAnnotations()); + } + sb.append(declaredType.getName()); + List<@NotNull ActualType> parameters = parameters(); + if (!parameters.isEmpty()) { + sb.append("<"); + for (@NotNull ActualType parameter : parameters) { + sb.append(parameter); + sb.append(", "); + } + sb.setLength(sb.length() - 2); + sb.append(">"); + } + return sb.toString(); + } + + private void appendAnnotationsToString(@NotNull StringBuilder sb, @NotNull Annotation[] annotations) { + for (@NotNull Annotation annotation : annotations) { + sb.append(annotation); + sb.append(' '); + } + } + + protected Object getMostSpecificTypeObject() { + if (usedParameterizedType != null) { + return usedParameterizedType; + } else if (usedType != null) { + return usedType; + } else { + return declaredType; + } + } +} 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 new file mode 100644 index 0000000..2dde51a --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatType.java @@ -0,0 +1,33 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import de.siphalor.tweed5.typeutils.impl.type.AnnotationRepeatTypeResolver; +import lombok.Value; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public interface AnnotationRepeatType { + static AnnotationRepeatType getType(@NotNull 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/api/type/LayeredTypeAnnotations.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotations.java new file mode 100644 index 0000000..d7166ff --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotations.java @@ -0,0 +1,130 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import lombok.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.*; + +public class LayeredTypeAnnotations implements AnnotatedElement { + private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; + + public static LayeredTypeAnnotations of(@NotNull TypeAnnotationLayer layer, @NotNull AnnotatedElement annotatedElement) { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.layers.add(new Layer(layer, annotatedElement)); + return annotations; + } + + private final List layers = new ArrayList<>(); + + public void appendLayerFrom(@NotNull TypeAnnotationLayer layer, @NotNull AnnotatedElement annotatedElement) { + int i; + for (i = 0; i < layers.size(); i++) { + if (layer.compareTo(layers.get(i).layer()) > 0) { + break; + } + } + layers.add(i, new Layer(layer, annotatedElement)); + } + + public void prependLayerFrom(@NotNull TypeAnnotationLayer layer, @NotNull AnnotatedElement annotatedElement) { + int i; + for (i = 0; i < layers.size(); i++) { + if (layer.compareTo(layers.get(i).layer()) >= 0) { + break; + } + } + layers.add(i, new Layer(layer, annotatedElement)); + } + + @Override + public T getAnnotation(@NotNull Class annotationClass) { + if (layers.isEmpty()) { + return null; + } else if (layers.size() == 1) { + return layers.get(0).annotatedElement.getAnnotation(annotationClass); + } + + Class altAnnotationClass = getRepeatAlternativeAnnotation(annotationClass); + + for (Layer layer : layers) { + T annotation = layer.annotatedElement.getAnnotation(annotationClass); + if (annotation != null) { + return annotation; + } + if (altAnnotationClass != null && layer.annotatedElement.isAnnotationPresent(altAnnotationClass)) { + return null; + } + } + return null; + } + + @Override + public @NotNull Annotation @NotNull [] getAnnotations() { + if (layers.isEmpty()) { + return EMPTY_ANNOTATIONS; + } else if (layers.size() == 1) { + return layers.get(0).annotatedElement.getAnnotations(); + } + + Map, Annotation> annotations = new HashMap<>(); + for (Layer layer : layers) { + for (Annotation layerAnnotation : layer.annotatedElement.getAnnotations()) { + Class layerAnnotationClass = layerAnnotation.annotationType(); + if (annotations.containsKey(layerAnnotationClass)) { + continue; + } + Class layerAltClass = getRepeatAlternativeAnnotation(layerAnnotationClass); + if (annotations.containsKey(layerAltClass)) { + continue; + } + annotations.put(layerAnnotationClass, layerAnnotation); + } + } + return annotations.values().toArray(new Annotation[0]); + } + + @Override + public @NotNull Annotation @NotNull [] getDeclaredAnnotations() { + if (layers.isEmpty()) { + return EMPTY_ANNOTATIONS; + } else if (layers.size() == 1) { + return layers.get(0).annotatedElement.getDeclaredAnnotations(); + } + + Map, Annotation> annotations = new HashMap<>(); + for (Layer layer : layers) { + for (Annotation layerAnnotation : layer.annotatedElement.getDeclaredAnnotations()) { + Class layerAnnotationClass = layerAnnotation.annotationType(); + if (annotations.containsKey(layerAnnotationClass)) { + continue; + } + Class layerAltClass = getRepeatAlternativeAnnotation(layerAnnotationClass); + if (annotations.containsKey(layerAltClass)) { + continue; + } + annotations.put(layerAnnotationClass, layerAnnotation); + } + } + return annotations.values().toArray(new Annotation[0]); + } + + private static @Nullable Class getRepeatAlternativeAnnotation(@NotNull 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; + AnnotatedElement annotatedElement; + } +} diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/TypeAnnotationLayer.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/TypeAnnotationLayer.java new file mode 100644 index 0000000..9e6d4a8 --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/api/type/TypeAnnotationLayer.java @@ -0,0 +1,7 @@ +package de.siphalor.tweed5.typeutils.api.type; + +public enum TypeAnnotationLayer implements Comparable { + TYPE_DECLARATION, + TYPE_PARAMETER, + TYPE_USE, +} diff --git a/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/package-info.java b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/package-info.java new file mode 100644 index 0000000..de4336b --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/package-info.java @@ -0,0 +1,5 @@ +@ApiStatus.Internal + +package de.siphalor.tweed5.typeutils.impl; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file 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/type/AnnotationRepeatTypeResolver.java new file mode 100644 index 0000000..f9e24be --- /dev/null +++ b/tweed5-type-utils/src/main/java/de/siphalor/tweed5/typeutils/impl/type/AnnotationRepeatTypeResolver.java @@ -0,0 +1,106 @@ +package de.siphalor.tweed5.typeutils.impl.type; + +import de.siphalor.tweed5.typeutils.api.type.AnnotationRepeatType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class AnnotationRepeatTypeResolver { + private static final Map, AnnotationRepeatType> CACHE = new HashMap<>(); + private static final ReadWriteLock CACHE_LOCK = new ReentrantReadWriteLock(); + + public static AnnotationRepeatType getType(@NotNull Class annotationClass) { + CACHE_LOCK.readLock().lock(); + try { + AnnotationRepeatType cachedValue = CACHE.get(annotationClass); + if (cachedValue != null) { + return cachedValue; + } + } finally { + CACHE_LOCK.readLock().unlock(); + } + return determineType(annotationClass); + } + + private static AnnotationRepeatType determineType(@NotNull Class annotationClass) { + Class container = getRepeatableContainerFromComponentAnnotation(annotationClass); + if (container != null) { + CACHE_LOCK.writeLock().lock(); + try { + AnnotationRepeatType type = new AnnotationRepeatType.Repeatable(container); + CACHE.put(annotationClass, type); + CACHE.put(container, new AnnotationRepeatType.RepeatableContainer(annotationClass)); + return type; + } finally { + CACHE_LOCK.writeLock().unlock(); + } + } + Class component = getRepeatableComponentFromContainerAnnotation(annotationClass); + if (component != null) { + CACHE_LOCK.writeLock().lock(); + try { + AnnotationRepeatType type = new AnnotationRepeatType.RepeatableContainer(component); + CACHE.put(annotationClass, type); + CACHE.put(component, new AnnotationRepeatType.Repeatable(component)); + return type; + } finally { + CACHE_LOCK.writeLock().unlock(); + } + } + + CACHE_LOCK.writeLock().lock(); + try { + CACHE.put(annotationClass, AnnotationRepeatType.NonRepeatable.instance()); + } finally { + CACHE_LOCK.writeLock().unlock(); + } + return AnnotationRepeatType.NonRepeatable.instance(); + } + + @Nullable + private static Class getRepeatableContainerFromComponentAnnotation( + Class annotationClass + ) { + Repeatable repeatableDeclaration = annotationClass.getAnnotation(Repeatable.class); + if (repeatableDeclaration == null) { + return null; + } + return repeatableDeclaration.value(); + } + + @Nullable + private static Class getRepeatableComponentFromContainerAnnotation( + Class annotationClass + ) { + try { + Method method = annotationClass.getMethod("value"); + Class returnType = method.getReturnType(); + if (!returnType.isArray()) { + return null; + } + Class componentType = returnType.getComponentType(); + if (!componentType.isAnnotation()) { + return null; + } + Repeatable repeatableDeclaration = componentType.getAnnotation(Repeatable.class); + if (repeatableDeclaration == null) { + return null; + } + if (repeatableDeclaration.value() != annotationClass) { + return null; + } + + //noinspection unchecked + return (Class) componentType; + } catch (NoSuchMethodException ignored) { + return null; + } + } +} 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 new file mode 100644 index 0000000..4de845e --- /dev/null +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/ActualTypeTest.java @@ -0,0 +1,181 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; + +class ActualTypeTest { + + @Test + void ofClass() { + assertThat(ActualType.ofClass(TypeTestClass.class)) + .isNotNull() + .satisfies( + type -> assertThat(type.declaredType()).isEqualTo(TypeTestClass.class), + type -> assertThat(type.getAnnotation(TestAnnotation.class)).isNotNull() + .extracting(TestAnnotation::value).isEqualTo("classy"), + type -> assertThat(type.usedType()).isNull() + ); + } + + @SneakyThrows + @Test + void ofUsedTypeSimpleAnnotated() { + ActualType actualType = ActualType.ofUsedType( + TypeTestClass.class.getField("missingParamMap").getAnnotatedType() + ); + + assertThat(actualType.declaredType()).isEqualTo(Map.class); + } + + @SneakyThrows + @Test + void ofUsedTypeParameterized() { + ActualType actualType = ActualType.ofUsedType( + TypeTestClass.class.getField("wildcardParamMap").getAnnotatedType() + ); + + assertThat(actualType.declaredType()).isEqualTo(Map.class); + } + + @SneakyThrows + @Test + void parameters() { + ActualType stringListType = ActualType.ofUsedType( + TypeTestClass.class.getField("stringList").getAnnotatedType() + ); + + assertThat(stringListType.parameters()) + .singleElement() + .satisfies( + type -> assertThat(type.declaredType()).isEqualTo(String.class), + type -> assertThat(type.getAnnotation(TestAnnotation.class)) + .isNotNull() + .extracting(TestAnnotation::value) + .isEqualTo("hi") + ); + } + + @ParameterizedTest + @ValueSource(classes = {List.class, Map.class, Integer.class}) + void getTypesOfSuperArgumentsNotInherited(Class targetType) { + ActualType actualType = ActualType.ofClass(String.class); + + assertThat(actualType.getTypesOfSuperArguments(targetType)).isNull(); + } + + @Test + void getTypesOfSuperArgumentsInheritedWithoutParameters() { + ActualType actualType = ActualType.ofClass(String.class); + + assertThat(actualType.getTypesOfSuperArguments(CharSequence.class)).isNotNull().isEmpty(); + } + + @SneakyThrows + @ParameterizedTest + @ValueSource(strings = {"missingParamMap", "wildcardParamMap"}) + void getTypesOfSuperArgumentsMissingParameters(String field) { + ActualType actualType = ActualType.ofUsedType( + TypeTestClass.class.getField(field).getAnnotatedType() + ); + + assertThat(actualType.getTypesOfSuperArguments(Map.class)) + .satisfiesExactly( + t -> assertThat(t.declaredType()).isEqualTo(Object.class), + t -> assertThat(t.declaredType()).isEqualTo(Object.class) + ); + } + + @SneakyThrows + @ParameterizedTest + @ValueSource(classes = {List.class, AbstractCollection.class, Collection.class, Iterable.class}) + void getTypesOfSuperArgumentSimpleList(Class targetType) { + ActualType stringListType = ActualType.ofUsedType( + TypeTestClass.class.getField("stringList").getAnnotatedType() + ); + + assertThat(stringListType.getTypesOfSuperArguments(targetType)).singleElement().satisfies( + type -> assertThat(type.getAnnotation(TestAnnotation.class)) + .asInstanceOf(type(TestAnnotation.class)) + .extracting(TestAnnotation::value) + .isEqualTo("hi"), + type -> assertThat(type.declaredType()).isEqualTo(String.class) + ); + } + + @SneakyThrows + @ParameterizedTest + @CsvSource({ + "useMap, elemUse", + "declMap, elemDecl", + "subValueMap, subType", + "valueMap, baseType", + }) + void getTypesOfSuperArgumentMapOverride(String field, String expectedAnnoValue) { + ActualType actualType = ActualType.ofUsedType( + TypeTestClass.class.getField(field).getAnnotatedType() + ); + + assertThat(actualType.getTypesOfSuperArguments(Map.class)).satisfiesExactly( + type -> assertThat(type.declaredType()).isEqualTo(String.class), + type -> assertThat(type).satisfies( + t -> assertThat(t.getAnnotation(TestAnnotation.class)) + .asInstanceOf(type(TestAnnotation.class)) + .extracting(TestAnnotation::value) + .isEqualTo("list"), + t -> assertThat(t.declaredType()).isEqualTo(List.class), + t -> assertThat(t.parameters()) + .singleElement() + .extracting(v -> v.getAnnotation(TestAnnotation.class)) + .as("List element type should have correct annotation") + .asInstanceOf(type(TestAnnotation.class)) + .extracting(TestAnnotation::value) + .isEqualTo(expectedAnnoValue) + ) + ); + } + + @SneakyThrows + @Test + void getTypesOfSuperArgumentMapWildcardBounded() { + ActualType actualType = ActualType.ofUsedType( + TypeTestClass.class.getField("wildcardBoundedParamMap").getAnnotatedType() + ); + + assertThat(actualType.getTypesOfSuperArguments(Map.class)).satisfiesExactly( + type -> assertThat(type.declaredType()).isEqualTo(CharSequence.class), + type -> assertThat(type.declaredType()).isEqualTo(Number.class) + ); + } + + @TestAnnotation("classy") + @SuppressWarnings("unused") + static class TypeTestClass { + public ArrayList<@TestAnnotation("hi") String> stringList; + public String2ValueMultimap<@TestAnnotation("elemUse") SubValue> useMap; + public String2ValueMultimap declMap; + public Map> subValueMap; + public Map> valueMap; + @SuppressWarnings("rawtypes") + public Map missingParamMap; + public Map wildcardParamMap; + public Map wildcardBoundedParamMap; + } + + interface String2ValueMultimap + extends Map> { + } + + @TestAnnotation("subType") + interface SubValue extends Value {} + + @TestAnnotation("baseType") + interface Value {} +} \ No newline at end of file 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/type/AnnotationRepeatTypeTest.java new file mode 100644 index 0000000..e255bc2 --- /dev/null +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/AnnotationRepeatTypeTest.java @@ -0,0 +1,45 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.ValueSources; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnnotationRepeatTypeTest { + + @ParameterizedTest + @ValueSource(classes = {Override.class, ParameterizedTest.class}) + void getTypeSimple(Class annotationClass) { + AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass); + assertThat(type).isInstanceOf(AnnotationRepeatType.NonRepeatable.class); + } + + @ParameterizedTest + @ValueSource(classes = {ValueSources.class, Rs.class}) + void getTypeRepeatableContainer(Class annotationClass) { + AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass); + assertThat(type).isInstanceOf(AnnotationRepeatType.RepeatableContainer.class); + } + + @ParameterizedTest + @ValueSource(classes = {ValueSource.class, R.class}) + void getTypeRepeatableValue(Class annotationClass) { + AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass); + assertThat(type).isInstanceOf(AnnotationRepeatType.Repeatable.class); + } + + @Repeatable(Rs.class) + @Retention(RetentionPolicy.RUNTIME) + @interface R { } + + @Retention(RetentionPolicy.RUNTIME) + @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/type/LayeredTypeAnnotationsTest.java new file mode 100644 index 0000000..d2a139e --- /dev/null +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/LayeredTypeAnnotationsTest.java @@ -0,0 +1,133 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import org.junit.jupiter.api.Test; + +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 LayeredTypeAnnotationsTest { + + @Test + void appendInSameLayer() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, A.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); + + assertThat(annotations.getAnnotation(TestAnnotation.class)) + .isNotNull() + .extracting(TestAnnotation::value) + .isEqualTo("a"); + } + + @Test + void appendInDifferentLayer() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, B.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, A.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, B.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); + + assertThat(annotations.getAnnotation(TestAnnotation.class)) + .isNotNull() + .extracting(TestAnnotation::value) + .isEqualTo("a"); + } + + @Test + void prependInSameLayer() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.prependLayerFrom(TypeAnnotationLayer.TYPE_USE, A.class); + annotations.prependLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); + + assertThat(annotations.getAnnotation(TestAnnotation.class)) + .isNotNull() + .extracting(TestAnnotation::value) + .isEqualTo("b"); + } + + @Test + void prependInDifferentLayer() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.prependLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, B.class); + annotations.prependLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); + annotations.prependLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, B.class); + annotations.prependLayerFrom(TypeAnnotationLayer.TYPE_USE, A.class); + + assertThat(annotations.getAnnotation(TestAnnotation.class)) + .isNotNull() + .extracting(TestAnnotation::value) + .isEqualTo("a"); + } + + @Test + void getAnnotationOverrideRepeatableWins() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, Repeated.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); + + assertThat(annotations.getAnnotation(TestAnnotation.class)) + .isNotNull() + .extracting(TestAnnotation::value) + .isEqualTo("b"); + assertThat(annotations.getAnnotation(TestAnnotations.class)).isNull(); + } + + @Test + void getAnnotationOverrideRepeatableContainerWins() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, B.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, Repeated.class); + + assertThat(annotations.getAnnotation(TestAnnotation.class)).isNull(); + assertThat(annotations.getAnnotation(TestAnnotations.class)) + .isNotNull() + .extracting(TestAnnotations::value) + .asInstanceOf(array(TestAnnotation[].class)) + .satisfiesExactly( + a -> assertThat(a.value()).isEqualTo("r1"), + a -> assertThat(a.value()).isEqualTo("r2") + ); + } + + @Test + void getAnnotationsOverrideRepeatableWins() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, Repeated.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, B.class); + + assertThat(annotations.getAnnotations()).singleElement() + .asInstanceOf(type(TestAnnotation.class)) + .extracting(TestAnnotation::value) + .isEqualTo("b"); + } + + @Test + void getAnnotationsOverrideRepeatableContainerWins() { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, B.class); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, Repeated.class); + + assertThat(annotations.getAnnotations()).singleElement() + .asInstanceOf(type(TestAnnotations.class)) + .extracting(TestAnnotations::value) + .asInstanceOf(array(TestAnnotation[].class)) + .satisfiesExactly( + a -> assertThat(a.value()).isEqualTo("r1"), + a -> assertThat(a.value()).isEqualTo("r2") + ); + } + + @TestAnnotation("r1") + @TestAnnotation("r2") + private static class Repeated { + } + + @TestAnnotation("a") + private static class A { + } + + @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/TestAnnotation.java b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotation.java new file mode 100644 index 0000000..82ad8d5 --- /dev/null +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotation.java @@ -0,0 +1,10 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +@Repeatable(TestAnnotations.class) +public @interface TestAnnotation { + String value(); +} 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/api/type/TestAnnotations.java new file mode 100644 index 0000000..59c25bf --- /dev/null +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/api/type/TestAnnotations.java @@ -0,0 +1,12 @@ +package de.siphalor.tweed5.typeutils.api.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface TestAnnotations { + TestAnnotation[] value(); +} 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 new file mode 100644 index 0000000..8ed28d2 --- /dev/null +++ b/tweed5-type-utils/src/test/java/de/siphalor/tweed5/typeutils/test/JavaReflectionTests.java @@ -0,0 +1,63 @@ +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; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +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 + */ +public class JavaReflectionTests { + @SneakyThrows + @Test + void parameterizedType() { + Field intsField = TestClass.class.getField("ints"); + assertThat(intsField.getGenericType()) + .asInstanceOf(type(ParameterizedType.class)) + .extracting(ParameterizedType::getRawType) + .isInstanceOf(Class.class) + .isEqualTo(List.class); + assertThat(intsField.getAnnotatedType()) + .asInstanceOf(type(AnnotatedParameterizedType.class)) + .satisfies( + type -> Assertions.assertThat(type.getAnnotation(TestAnnotation.class)).isNotNull(), + type -> assertThat(type.getAnnotatedActualTypeArguments()) + .singleElement() + .isInstanceOf(AnnotatedParameterizedType.class) + .satisfies(arg -> Assertions.assertThat(arg.getAnnotation(TestAnnotation.class)).isNull()) + ); + assertThat(TestClass.class.getField("string").getGenericType()).isInstanceOf(Class.class) + .isEqualTo(String.class); + } + + @Test + void repeatableAnnotation() { + assertThat(TestClass.class.getAnnotationsByType(TestAnnotation.class)).hasSize(3); + assertThat(TestClass.class.getAnnotations()) + .doesNotHaveAnyElementsOfTypes(TestAnnotation.class); + assertThat(TestClass.class.getAnnotation(TestAnnotation.class)).isNull(); + } + + @TestAnnotation("a") + @TestAnnotation("b") + @TestAnnotation("c") + static class TestClass { + public String string; + @TestAnnotation("x") + public List> ints; + public TestCollection collection; + } + + interface TestCollection extends Collection> {} +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java index 356fff7..cbd68c3 100644 --- a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java @@ -74,9 +74,13 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor return; } - readWriteExtension.setEntryReaderWriterDefinition(configEntry, createDefinitionFromEntryConfig(entryConfig, context)); + EntryReaderWriterDefinition definition = createDefinitionFromEntryConfig(entryConfig, context); + if (definition != null) { + readWriteExtension.setEntryReaderWriterDefinition(configEntry, definition); + } } + @Nullable private EntryReaderWriterDefinition createDefinitionFromEntryConfig(EntryReadWriteConfig entryConfig, WeavingContext context) { String readerSpecText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader(); String writerSpecText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer(); @@ -90,6 +94,10 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor writerSpec = specFromText(writerSpecText, context); } + if (readerSpec == null && writerSpec == null) { + return null; + } + //noinspection unchecked TweedEntryReader reader = readerSpec == null ? TweedEntryReaderWriterImpls.NOOP_READER_WRITER @@ -165,10 +173,10 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor private T loadClassIfExists(Class baseClass, String className, T[] arguments) { try { Class clazz = Class.forName(className); - Class[] argClassses = new Class[arguments.length]; - Arrays.fill(argClassses, baseClass); + Class[] argClasses = new Class[arguments.length]; + Arrays.fill(argClasses, baseClass); - Constructor constructor = clazz.getConstructor(argClassses); + Constructor constructor = clazz.getConstructor(argClasses); //noinspection unchecked return (T) constructor.newInstance((Object[]) arguments); diff --git a/tweed5-weaver-pojo/build.gradle.kts b/tweed5-weaver-pojo/build.gradle.kts index 7b6beba..e300c1d 100644 --- a/tweed5-weaver-pojo/build.gradle.kts +++ b/tweed5-weaver-pojo/build.gradle.kts @@ -5,4 +5,5 @@ plugins { dependencies { api(project(":tweed5-core")) api(project(":tweed5-naming-format")) + api(project(":tweed5-type-utils")) } \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java index 9379d29..b39dbfd 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java @@ -11,7 +11,7 @@ import java.lang.annotation.Target; * Marks this class as a class that should be woven as a {@link de.siphalor.tweed5.core.api.entry.CompoundConfigEntry}. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.FIELD}) +@Target({ElementType.TYPE, ElementType.TYPE_USE}) public @interface CompoundWeaving { /** * The naming format to use for this POJO. diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java deleted file mode 100644 index 1e57f3c..0000000 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java +++ /dev/null @@ -1,140 +0,0 @@ -package de.siphalor.tweed5.weaver.pojo.api.weaving; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.lang.annotation.Annotation; -import java.lang.annotation.ElementType; -import java.lang.annotation.Repeatable; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Array; -import java.util.*; - -/** - * Represents multi-level annotations across multiple Java elements. - * E.g. annotations on a field overriding annotations declared on the field type. - */ -public class Annotations { - private static final List ELEMENT_TYPE_ORDER = Arrays.asList( - ElementType.TYPE_USE, - ElementType.FIELD, - ElementType.CONSTRUCTOR, - ElementType.METHOD, - ElementType.LOCAL_VARIABLE, - ElementType.TYPE_PARAMETER, - ElementType.TYPE, - ElementType.ANNOTATION_TYPE, - ElementType.PACKAGE - ); - private final Map elements = new EnumMap<>(ElementType.class); - - public void addAnnotationsFrom(ElementType elementType, AnnotatedElement element) { - elements.put(elementType, element); - } - - @Nullable - public T getAnnotation(Class annotationClass) { - for (ElementType elementType : ELEMENT_TYPE_ORDER) { - AnnotatedElement annotatedElement = elements.get(elementType); - if (annotatedElement != null) { - T annotation = annotatedElement.getAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - } - } - return null; - } - - @Nullable - public T getAnnotation(ElementType elementType, Class annotationType) { - AnnotatedElement annotatedElement = elements.get(elementType); - if (annotatedElement == null) { - return null; - } - return annotatedElement.getAnnotation(annotationType); - } - - @NotNull - public T[] getAnnotationHierarchy(Class annotationClass) { - List hierarchy = new ArrayList<>(elements.size()); - for (ElementType elementType : ELEMENT_TYPE_ORDER) { - AnnotatedElement element = elements.get(elementType); - if (element != null) { - T annotation = element.getAnnotation(annotationClass); - if (annotation != null) { - hierarchy.add(annotation); - } - } - } - //noinspection unchecked - return hierarchy.toArray((T[]) Array.newInstance(annotationClass, hierarchy.size())); - } - - @NotNull - public T[] getAnnotations(Class annotationClass) { - for (ElementType elementType : ELEMENT_TYPE_ORDER) { - AnnotatedElement annotatedElement = elements.get(elementType); - if (annotatedElement != null) { - T[] annotations = annotatedElement.getAnnotationsByType(annotationClass); - if (annotations.length != 0) { - return annotations; - } - } - } - //noinspection unchecked - return (T[]) Array.newInstance(annotationClass, 0); - } - - @NotNull - public T[] getAnnotations(ElementType elementType, Class annotationType) { - AnnotatedElement annotatedElement = elements.get(elementType); - if (annotatedElement == null) { - //noinspection unchecked - return (T[]) Array.newInstance(annotationType, 0); - } - return annotatedElement.getAnnotationsByType(annotationType); - } - - @NotNull - public T[][] getAnnotationsHierachy(Class annotationClass) { - List hierarchy = new ArrayList<>(ELEMENT_TYPE_ORDER.size()); - for (ElementType elementType : ELEMENT_TYPE_ORDER) { - AnnotatedElement annotatedElement = elements.get(elementType); - if (annotatedElement != null) { - T[] annotations = annotatedElement.getAnnotationsByType(annotationClass); - if (annotations.length != 0) { - hierarchy.add(annotations); - } - } - } - //noinspection unchecked - return hierarchy.toArray((T[][]) Array.newInstance(annotationClass, 0, 0)); - } - - @NotNull - public Annotation[] getAllAnnotations() { - Map, Annotation[]> annotations = new HashMap<>(); - for (ElementType elementType : ELEMENT_TYPE_ORDER) { - AnnotatedElement annotatedElement = elements.get(elementType); - if (annotatedElement != null) { - for (Annotation annotation : annotatedElement.getAnnotations()) { - annotations.putIfAbsent(annotation.annotationType(), new Annotation[]{annotation}); - - Repeatable repeatable = annotation.annotationType().getAnnotation(Repeatable.class); - if (repeatable != null) { - annotations.put(repeatable.value(), annotatedElement.getAnnotationsByType(repeatable.value())); - } - } - } - } - - if (annotations.isEmpty()) { - return new Annotation[0]; - } else if (annotations.size() == 1) { - return annotations.values().iterator().next(); - } else { - return annotations.values().stream().flatMap(Arrays::stream).toArray(Annotation[]::new); - } - } -} 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 db04966..939ef4f 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 @@ -7,6 +7,9 @@ import de.siphalor.tweed5.namingformat.api.NamingFormatCollector; import de.siphalor.tweed5.namingformat.api.NamingFormats; 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.type.TypeAnnotationLayer; import de.siphalor.tweed5.weaver.pojo.impl.entry.StaticPojoCompoundConfigEntry; import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector; import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException; @@ -15,8 +18,8 @@ import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfi import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.lang.annotation.ElementType; import java.lang.invoke.MethodHandle; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.util.Map; @@ -40,7 +43,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver { } @Override - public @Nullable ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { + public @Nullable ConfigEntry weaveEntry(ActualType valueType, WeavingContext context) { if (context.annotations().getAnnotation(CompoundWeaving.class) == null) { return null; } @@ -49,7 +52,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver { WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy(); weavingConfigAccess.set(newExtensionsData, weavingConfig); - PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueClass); + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueType.declaredType()); WeavableCompoundConfigEntry compoundEntry = instantiateCompoundEntry(introspector, weavingConfig); @@ -62,7 +65,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver { return compoundEntry; } catch (Exception e) { - throw new PojoWeavingException("Exception occurred trying to weave compound for class " + valueClass.getName(), e); + throw new PojoWeavingException("Exception occurred trying to weave compound for class " + valueType, e); } } @@ -82,27 +85,8 @@ public class CompoundPojoWeaver implements TweedPojoWeaver { return CompoundWeavingConfigImpl.withOverrides(parent, local); } - private WeavingContext createSubContextForProperty( - PojoClassIntrospector.Property property, - String name, - WeavingContext.ExtensionsData newExtensionsData, - WeavingContext parentContext - ) { - return parentContext.subContextBuilder(name) - .annotations(collectAnnotationsForField(property.field())) - .extensionsData(newExtensionsData) - .build(); - } - - private Annotations collectAnnotationsForField(Field field) { - Annotations annotations = new Annotations(); - annotations.addAnnotationsFrom(ElementType.TYPE, field.getType()); - annotations.addAnnotationsFrom(ElementType.FIELD, field); - return annotations; - } - @Nullable - private CompoundWeavingConfig createWeavingConfigFromAnnotations(Annotations annotations) { + private CompoundWeavingConfig createWeavingConfigFromAnnotations(@NotNull AnnotatedElement annotations) { CompoundWeaving annotation = annotations.getAnnotation(CompoundWeaving.class); if (annotation == null) { return null; @@ -163,7 +147,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver { // TODO throw new UnsupportedOperationException("Final config entries are not supported in weaving yet."); } else { - subEntry = subContext.weaveEntry(property.field().getType(), subContext); + subEntry = subContext.weaveEntry(ActualType.ofUsedType(property.field().getAnnotatedType()), subContext); } return new StaticPojoCompoundConfigEntry.SubEntry( @@ -192,4 +176,24 @@ public class CompoundPojoWeaver implements TweedPojoWeaver { } return namingFormat; } + + private WeavingContext createSubContextForProperty( + PojoClassIntrospector.Property property, + String name, + WeavingContext.ExtensionsData newExtensionsData, + WeavingContext parentContext + ) { + return parentContext.subContextBuilder(name) + .annotations(collectAnnotationsForField(property.field())) + .extensionsData(newExtensionsData) + .build(); + } + + private AnnotatedElement collectAnnotationsForField(Field field) { + LayeredTypeAnnotations annotations = new LayeredTypeAnnotations(); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_DECLARATION, field.getType()); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, field); + annotations.appendLayerFrom(TypeAnnotationLayer.TYPE_USE, field.getAnnotatedType()); + return annotations; + } } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java index 8ef13b7..1932650 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java @@ -2,6 +2,7 @@ package de.siphalor.tweed5.weaver.pojo.api.weaving; import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; +import de.siphalor.tweed5.typeutils.api.type.ActualType; import org.jetbrains.annotations.Nullable; public class TrivialPojoWeaver implements TweedPojoWeaver { @@ -11,7 +12,7 @@ public class TrivialPojoWeaver implements TweedPojoWeaver { } @Override - public @Nullable ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { - return new SimpleConfigEntryImpl<>(valueClass); + public @Nullable ConfigEntry weaveEntry(ActualType valueType, WeavingContext context) { + return new SimpleConfigEntryImpl<>(valueType.declaredType()); } } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java index b54c1f4..17b2c60 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java @@ -1,6 +1,7 @@ package de.siphalor.tweed5.weaver.pojo.api.weaving; import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.typeutils.api.type.ActualType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -12,7 +13,7 @@ public interface TweedPojoWeavingFunction { * @return The resulting, sealed config entry or {@code null}, if the weaving function is not applicable to the given parameters. */ @Nullable - ConfigEntry weaveEntry(Class valueClass, WeavingContext context); + ConfigEntry weaveEntry(ActualType valueType, WeavingContext context); @FunctionalInterface interface NonNull extends TweedPojoWeavingFunction { @@ -24,6 +25,6 @@ public interface TweedPojoWeavingFunction { * @throws RuntimeException when a valid config entry could not be resolved. */ @Override - @NotNull ConfigEntry weaveEntry(Class valueClass, WeavingContext context); + @NotNull ConfigEntry weaveEntry(ActualType valueType, WeavingContext context); } } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java index 556e696..621b5ea 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java @@ -3,11 +3,13 @@ package de.siphalor.tweed5.weaver.pojo.api.weaving; import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.patchwork.api.Patchwork; +import de.siphalor.tweed5.typeutils.api.type.ActualType; import lombok.*; import lombok.experimental.Accessors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.AnnotatedElement; import java.util.Arrays; @Value @@ -24,7 +26,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull { @NotNull ExtensionsData extensionsData; @NotNull - Annotations annotations; + AnnotatedElement annotations; public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer configContainer) { return new Builder(null, weavingFunction, configContainer, new String[0]); @@ -41,8 +43,8 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull { } @Override - public @NotNull ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { - return weavingFunction.weaveEntry(valueClass, context); + public @NotNull ConfigEntry weaveEntry(ActualType valueType, WeavingContext context) { + return weavingFunction.weaveEntry(valueType, context); } public interface ExtensionsData extends Patchwork {} @@ -57,7 +59,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull { private final ConfigContainer configContainer; private final String[] path; private ExtensionsData extensionsData; - private Annotations annotations; + private AnnotatedElement annotations; public WeavingContext build() { return new WeavingContext( diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java index d8b5e24..6cecdb9 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java @@ -8,10 +8,8 @@ import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator; import de.siphalor.tweed5.patchwork.impl.PatchworkClass; import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator; import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart; -import de.siphalor.tweed5.utils.api.collection.ClassToInstancesMultimap; -import de.siphalor.tweed5.utils.api.collection.InheritanceMap; import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving; -import de.siphalor.tweed5.weaver.pojo.api.weaving.Annotations; +import de.siphalor.tweed5.typeutils.api.type.ActualType; import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver; import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext; import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor; @@ -20,7 +18,6 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import java.lang.annotation.Annotation; -import java.lang.annotation.ElementType; import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -168,7 +165,7 @@ public class TweedPojoWeaverBootstrapper { setupWeavers(); WeavingContext weavingContext = createWeavingContext(); - ConfigEntry rootEntry = this.weaveEntry(pojoClass, weavingContext); + ConfigEntry rootEntry = this.weaveEntry(ActualType.ofClass(pojoClass), weavingContext); configContainer.attachAndSealTree(rootEntry); return configContainer; @@ -214,19 +211,16 @@ public class TweedPojoWeaverBootstrapper { try { WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke(); - Annotations annotations = new Annotations(); - annotations.addAnnotationsFrom(ElementType.TYPE, pojoClass); - return WeavingContext.builder(this::weaveEntry, configContainer) .extensionsData(extensionsData) - .annotations(annotations) + .annotations(pojoClass) .build(); } catch (Throwable e) { throw new PojoWeavingException("Failed to create weaving context's extension data"); } } - private ConfigEntry weaveEntry(Class dataClass, WeavingContext context) { + private ConfigEntry weaveEntry(ActualType dataClass, WeavingContext context) { for (TweedPojoWeaver weaver : weavers) { ConfigEntry configEntry = weaver.weaveEntry(dataClass, context); if (configEntry != null) { @@ -238,7 +232,7 @@ public class TweedPojoWeaverBootstrapper { } } - throw new PojoWeavingException("Failed to weave " + dataClass.getName() + ": No matching weavers found"); + throw new PojoWeavingException("Failed to weave " + dataClass + ": No matching weavers found"); } private void applyPostProcessors(ConfigEntry configEntry, WeavingContext context) { diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java index 0513255..6a463ea 100644 --- a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java @@ -7,14 +7,13 @@ import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; import de.siphalor.tweed5.namingformat.api.NamingFormat; 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.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig; import lombok.AllArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; -import java.lang.annotation.ElementType; - import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isCompoundEntryForClassWith; import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass; import static org.assertj.core.api.Assertions.assertThat; @@ -33,28 +32,25 @@ class CompoundPojoWeaverTest { } }); - Annotations annotations = new Annotations(); - annotations.addAnnotationsFrom(ElementType.TYPE, Compound.class); - WeavingContext weavingContext = WeavingContext.builder(new TweedPojoWeavingFunction.NonNull() { @Override - public @NotNull ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { - ConfigEntry entry = compoundWeaver.weaveEntry(valueClass, context); + public @NotNull ConfigEntry weaveEntry(ActualType valueType, WeavingContext context) { + ConfigEntry entry = compoundWeaver.weaveEntry(valueType, context); if (entry != null) { return entry; } else { //noinspection unchecked ConfigEntry configEntry = mock((Class>) (Class) SimpleConfigEntry.class); - when(configEntry.valueClass()).thenReturn(valueClass); + when(configEntry.valueClass()).thenReturn(valueType.declaredType()); return configEntry; } } }, mock(ConfigContainer.class)) .extensionsData(new ExtensionsDataMock(null)) - .annotations(annotations) + .annotations(Compound.class) .build(); - ConfigEntry resultEntry = compoundWeaver.weaveEntry(Compound.class, weavingContext); + ConfigEntry resultEntry = compoundWeaver.weaveEntry(ActualType.ofClass(Compound.class), weavingContext); assertThat(resultEntry).satisfies(isCompoundEntryForClassWith(Compound.class, compoundEntry -> assertThat(compoundEntry.subEntries()) .hasEntrySatisfying("an-integer", isSimpleEntryForClass(int.class)) diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java index e52d1c2..21dc891 100644 --- a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java @@ -12,21 +12,25 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type; public class ConfigEntryAssertions { public static Consumer isSimpleEntryForClass(Class valueClass) { return object -> assertThat(object) + .as("Should be a simple config entry for class " + valueClass.getName()) .asInstanceOf(type(SimpleConfigEntry.class)) .extracting(ConfigEntry::valueClass) .isEqualTo(valueClass); } - @SuppressWarnings("unchecked") public static Consumer isCompoundEntryForClassWith( Class compoundClass, Consumer> condition ) { return object -> assertThat(object) + .as("Should be a compound config entry for class " + compoundClass.getSimpleName()) .asInstanceOf(type(CompoundConfigEntry.class)) - .satisfies(compoundEntry -> { - assertThat(compoundEntry.valueClass()).isEqualTo(compoundClass); - condition.accept(compoundEntry); - }); + .as("Compound entry for class " + compoundClass.getSimpleName()) + .satisfies( + compoundEntry -> assertThat(compoundEntry.valueClass()) + .as("Value class of compound entry should match") + .isEqualTo(compoundClass), + condition::accept + ); } }