[type-utils, weaver-pojo] Introduce a submodule focused on Java types for POJO weaving

This commit is contained in:
2025-03-04 23:47:40 +01:00
parent 0eac7e42aa
commit e30e6d0547
25 changed files with 1079 additions and 204 deletions

View File

@@ -7,6 +7,7 @@ include("tweed5-patchwork")
include("tweed5-serde-api") include("tweed5-serde-api")
include("tweed5-serde-extension") include("tweed5-serde-extension")
include("tweed5-serde-hjson") include("tweed5-serde-hjson")
include("tweed5-type-utils")
include("tweed5-utils") include("tweed5-utils")
include("tweed5-weaver-pojo") include("tweed5-weaver-pojo")
include("tweed5-weaver-pojo-serde-extension") include("tweed5-weaver-pojo-serde-extension")

View File

@@ -0,0 +1,3 @@
plugins {
id("de.siphalor.tweed5.base-module")
}

View File

@@ -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 <T> the type represented by this class
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class ActualType<T> implements AnnotatedElement {
/**
* The raw {@link Class} that the type has been originally declared as.
*/
@Getter
private final Class<T> 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 <T> ActualType<T> ofClass(Class<T> 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 extends Annotation> A getAnnotation(@NotNull Class<A> 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<ActualType<?>> 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<String, AnnotatedType> 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<String, AnnotatedType> 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<ActualType<?>> 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;
}
}
}

View File

@@ -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<? extends Annotation> 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<? extends Annotation> containerAnnotation;
}
@Value
class RepeatableContainer implements AnnotationRepeatType {
Class<? extends Annotation> componentAnnotation;
}
}

View File

@@ -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<Layer> 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 extends Annotation> T getAnnotation(@NotNull Class<T> annotationClass) {
if (layers.isEmpty()) {
return null;
} else if (layers.size() == 1) {
return layers.get(0).annotatedElement.getAnnotation(annotationClass);
}
Class<? extends Annotation> 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<Class<? extends Annotation>, Annotation> annotations = new HashMap<>();
for (Layer layer : layers) {
for (Annotation layerAnnotation : layer.annotatedElement.getAnnotations()) {
Class<? extends Annotation> layerAnnotationClass = layerAnnotation.annotationType();
if (annotations.containsKey(layerAnnotationClass)) {
continue;
}
Class<? extends Annotation> 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<Class<? extends Annotation>, Annotation> annotations = new HashMap<>();
for (Layer layer : layers) {
for (Annotation layerAnnotation : layer.annotatedElement.getDeclaredAnnotations()) {
Class<? extends Annotation> layerAnnotationClass = layerAnnotation.annotationType();
if (annotations.containsKey(layerAnnotationClass)) {
continue;
}
Class<? extends Annotation> layerAltClass = getRepeatAlternativeAnnotation(layerAnnotationClass);
if (annotations.containsKey(layerAltClass)) {
continue;
}
annotations.put(layerAnnotationClass, layerAnnotation);
}
}
return annotations.values().toArray(new Annotation[0]);
}
private static <T extends Annotation> @Nullable Class<? extends Annotation> getRepeatAlternativeAnnotation(@NotNull Class<T> annotationClass) {
AnnotationRepeatType annotationRepeatType = AnnotationRepeatType.getType(annotationClass);
Class<? extends Annotation> 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;
}
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.typeutils.api.type;
public enum TypeAnnotationLayer implements Comparable<TypeAnnotationLayer> {
TYPE_DECLARATION,
TYPE_PARAMETER,
TYPE_USE,
}

View File

@@ -0,0 +1,5 @@
@ApiStatus.Internal
package de.siphalor.tweed5.typeutils.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -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<Class<? extends Annotation>, AnnotationRepeatType> CACHE = new HashMap<>();
private static final ReadWriteLock CACHE_LOCK = new ReentrantReadWriteLock();
public static AnnotationRepeatType getType(@NotNull Class<? extends Annotation> 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<? extends Annotation> annotationClass) {
Class<? extends Annotation> 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<? extends Annotation> 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<? extends Annotation> getRepeatableContainerFromComponentAnnotation(
Class<? extends Annotation> annotationClass
) {
Repeatable repeatableDeclaration = annotationClass.getAnnotation(Repeatable.class);
if (repeatableDeclaration == null) {
return null;
}
return repeatableDeclaration.value();
}
@Nullable
private static Class<? extends Annotation> getRepeatableComponentFromContainerAnnotation(
Class<? extends Annotation> 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<? extends Annotation>) componentType;
} catch (NoSuchMethodException ignored) {
return null;
}
}
}

View File

@@ -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<String> actualType = ActualType.ofClass(String.class);
assertThat(actualType.getTypesOfSuperArguments(targetType)).isNull();
}
@Test
void getTypesOfSuperArgumentsInheritedWithoutParameters() {
ActualType<String> 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<SubValue> declMap;
public Map<String, @TestAnnotation("list") List<SubValue>> subValueMap;
public Map<String, @TestAnnotation("list") List<Value>> valueMap;
@SuppressWarnings("rawtypes")
public Map missingParamMap;
public Map<?, ?> wildcardParamMap;
public Map<? extends CharSequence, ? extends Number> wildcardBoundedParamMap;
}
interface String2ValueMultimap<V extends Value>
extends Map<String, @TestAnnotation("list") List<@TestAnnotation("elemDecl") V>> {
}
@TestAnnotation("subType")
interface SubValue extends Value {}
@TestAnnotation("baseType")
interface Value {}
}

View File

@@ -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<? extends Annotation> annotationClass) {
AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass);
assertThat(type).isInstanceOf(AnnotationRepeatType.NonRepeatable.class);
}
@ParameterizedTest
@ValueSource(classes = {ValueSources.class, Rs.class})
void getTypeRepeatableContainer(Class<? extends Annotation> annotationClass) {
AnnotationRepeatType type = AnnotationRepeatType.getType(annotationClass);
assertThat(type).isInstanceOf(AnnotationRepeatType.RepeatableContainer.class);
}
@ParameterizedTest
@ValueSource(classes = {ValueSource.class, R.class})
void getTypeRepeatableValue(Class<? extends Annotation> 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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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<List<@TestAnnotation("y") Integer>> ints;
public TestCollection<Long, String> collection;
}
interface TestCollection<A, B> extends Collection<Map<A, B>> {}
}

View File

@@ -74,9 +74,13 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor
return; 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) { private EntryReaderWriterDefinition createDefinitionFromEntryConfig(EntryReadWriteConfig entryConfig, WeavingContext context) {
String readerSpecText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader(); String readerSpecText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader();
String writerSpecText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer(); String writerSpecText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer();
@@ -90,6 +94,10 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor
writerSpec = specFromText(writerSpecText, context); writerSpec = specFromText(writerSpecText, context);
} }
if (readerSpec == null && writerSpec == null) {
return null;
}
//noinspection unchecked //noinspection unchecked
TweedEntryReader<?, ?> reader = readerSpec == null TweedEntryReader<?, ?> reader = readerSpec == null
? TweedEntryReaderWriterImpls.NOOP_READER_WRITER ? TweedEntryReaderWriterImpls.NOOP_READER_WRITER
@@ -165,10 +173,10 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor
private <T> T loadClassIfExists(Class<T> baseClass, String className, T[] arguments) { private <T> T loadClassIfExists(Class<T> baseClass, String className, T[] arguments) {
try { try {
Class<?> clazz = Class.forName(className); Class<?> clazz = Class.forName(className);
Class<?>[] argClassses = new Class<?>[arguments.length]; Class<?>[] argClasses = new Class<?>[arguments.length];
Arrays.fill(argClassses, baseClass); Arrays.fill(argClasses, baseClass);
Constructor<?> constructor = clazz.getConstructor(argClassses); Constructor<?> constructor = clazz.getConstructor(argClasses);
//noinspection unchecked //noinspection unchecked
return (T) constructor.newInstance((Object[]) arguments); return (T) constructor.newInstance((Object[]) arguments);

View File

@@ -5,4 +5,5 @@ plugins {
dependencies { dependencies {
api(project(":tweed5-core")) api(project(":tweed5-core"))
api(project(":tweed5-naming-format")) api(project(":tweed5-naming-format"))
api(project(":tweed5-type-utils"))
} }

View File

@@ -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}. * Marks this class as a class that should be woven as a {@link de.siphalor.tweed5.core.api.entry.CompoundConfigEntry}.
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD}) @Target({ElementType.TYPE, ElementType.TYPE_USE})
public @interface CompoundWeaving { public @interface CompoundWeaving {
/** /**
* The naming format to use for this POJO. * The naming format to use for this POJO.

View File

@@ -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<ElementType> 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<ElementType, AnnotatedElement> elements = new EnumMap<>(ElementType.class);
public void addAnnotationsFrom(ElementType elementType, AnnotatedElement element) {
elements.put(elementType, element);
}
@Nullable
public <T extends Annotation> T getAnnotation(Class<T> 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 extends Annotation> T getAnnotation(ElementType elementType, Class<T> annotationType) {
AnnotatedElement annotatedElement = elements.get(elementType);
if (annotatedElement == null) {
return null;
}
return annotatedElement.getAnnotation(annotationType);
}
@NotNull
public <T extends Annotation> T[] getAnnotationHierarchy(Class<T> annotationClass) {
List<T> 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 extends Annotation> T[] getAnnotations(Class<T> 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 extends Annotation> T[] getAnnotations(ElementType elementType, Class<T> annotationType) {
AnnotatedElement annotatedElement = elements.get(elementType);
if (annotatedElement == null) {
//noinspection unchecked
return (T[]) Array.newInstance(annotationType, 0);
}
return annotatedElement.getAnnotationsByType(annotationType);
}
@NotNull
public <T extends Annotation> T[][] getAnnotationsHierachy(Class<T> annotationClass) {
List<T[]> 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<Class<? extends Annotation>, 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);
}
}
}

View File

@@ -7,6 +7,9 @@ import de.siphalor.tweed5.namingformat.api.NamingFormatCollector;
import de.siphalor.tweed5.namingformat.api.NamingFormats; import de.siphalor.tweed5.namingformat.api.NamingFormats;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving; import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; 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.entry.StaticPojoCompoundConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector; import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException; 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.annotation.ElementType;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Map; import java.util.Map;
@@ -40,7 +43,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
} }
@Override @Override
public @Nullable <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) { public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
if (context.annotations().getAnnotation(CompoundWeaving.class) == null) { if (context.annotations().getAnnotation(CompoundWeaving.class) == null) {
return null; return null;
} }
@@ -49,7 +52,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy(); WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy();
weavingConfigAccess.set(newExtensionsData, weavingConfig); weavingConfigAccess.set(newExtensionsData, weavingConfig);
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueClass); PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueType.declaredType());
WeavableCompoundConfigEntry<T> compoundEntry = instantiateCompoundEntry(introspector, weavingConfig); WeavableCompoundConfigEntry<T> compoundEntry = instantiateCompoundEntry(introspector, weavingConfig);
@@ -62,7 +65,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
return compoundEntry; return compoundEntry;
} catch (Exception e) { } 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); 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 @Nullable
private CompoundWeavingConfig createWeavingConfigFromAnnotations(Annotations annotations) { private CompoundWeavingConfig createWeavingConfigFromAnnotations(@NotNull AnnotatedElement annotations) {
CompoundWeaving annotation = annotations.getAnnotation(CompoundWeaving.class); CompoundWeaving annotation = annotations.getAnnotation(CompoundWeaving.class);
if (annotation == null) { if (annotation == null) {
return null; return null;
@@ -163,7 +147,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
// TODO // TODO
throw new UnsupportedOperationException("Final config entries are not supported in weaving yet."); throw new UnsupportedOperationException("Final config entries are not supported in weaving yet.");
} else { } else {
subEntry = subContext.weaveEntry(property.field().getType(), subContext); subEntry = subContext.weaveEntry(ActualType.ofUsedType(property.field().getAnnotatedType()), subContext);
} }
return new StaticPojoCompoundConfigEntry.SubEntry( return new StaticPojoCompoundConfigEntry.SubEntry(
@@ -192,4 +176,24 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
} }
return namingFormat; 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;
}
} }

View File

@@ -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.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public class TrivialPojoWeaver implements TweedPojoWeaver { public class TrivialPojoWeaver implements TweedPojoWeaver {
@@ -11,7 +12,7 @@ public class TrivialPojoWeaver implements TweedPojoWeaver {
} }
@Override @Override
public @Nullable <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) { public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
return new SimpleConfigEntryImpl<>(valueClass); return new SimpleConfigEntryImpl<>(valueType.declaredType());
} }
} }

View File

@@ -1,6 +1,7 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving; package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.NotNull;
import org.jetbrains.annotations.Nullable; 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. * @return The resulting, sealed config entry or {@code null}, if the weaving function is not applicable to the given parameters.
*/ */
@Nullable @Nullable
<T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context); <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context);
@FunctionalInterface @FunctionalInterface
interface NonNull extends TweedPojoWeavingFunction { interface NonNull extends TweedPojoWeavingFunction {
@@ -24,6 +25,6 @@ public interface TweedPojoWeavingFunction {
* @throws RuntimeException when a valid config entry could not be resolved. * @throws RuntimeException when a valid config entry could not be resolved.
*/ */
@Override @Override
@NotNull <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context); @NotNull <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context);
} }
} }

View File

@@ -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.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.patchwork.api.Patchwork; import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import lombok.*; import lombok.*;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.reflect.AnnotatedElement;
import java.util.Arrays; import java.util.Arrays;
@Value @Value
@@ -24,7 +26,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
@NotNull @NotNull
ExtensionsData extensionsData; ExtensionsData extensionsData;
@NotNull @NotNull
Annotations annotations; AnnotatedElement annotations;
public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer<?> configContainer) { public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer<?> configContainer) {
return new Builder(null, weavingFunction, configContainer, new String[0]); return new Builder(null, weavingFunction, configContainer, new String[0]);
@@ -41,8 +43,8 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
} }
@Override @Override
public @NotNull <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) { public @NotNull <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
return weavingFunction.weaveEntry(valueClass, context); return weavingFunction.weaveEntry(valueType, context);
} }
public interface ExtensionsData extends Patchwork<ExtensionsData> {} public interface ExtensionsData extends Patchwork<ExtensionsData> {}
@@ -57,7 +59,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
private final ConfigContainer<?> configContainer; private final ConfigContainer<?> configContainer;
private final String[] path; private final String[] path;
private ExtensionsData extensionsData; private ExtensionsData extensionsData;
private Annotations annotations; private AnnotatedElement annotations;
public WeavingContext build() { public WeavingContext build() {
return new WeavingContext( return new WeavingContext(

View File

@@ -8,10 +8,8 @@ import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass; import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator; import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart; 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.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.TweedPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext; import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor; 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 org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@@ -168,7 +165,7 @@ public class TweedPojoWeaverBootstrapper<T> {
setupWeavers(); setupWeavers();
WeavingContext weavingContext = createWeavingContext(); WeavingContext weavingContext = createWeavingContext();
ConfigEntry<T> rootEntry = this.weaveEntry(pojoClass, weavingContext); ConfigEntry<T> rootEntry = this.weaveEntry(ActualType.ofClass(pojoClass), weavingContext);
configContainer.attachAndSealTree(rootEntry); configContainer.attachAndSealTree(rootEntry);
return configContainer; return configContainer;
@@ -214,19 +211,16 @@ public class TweedPojoWeaverBootstrapper<T> {
try { try {
WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke(); WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke();
Annotations annotations = new Annotations();
annotations.addAnnotationsFrom(ElementType.TYPE, pojoClass);
return WeavingContext.builder(this::weaveEntry, configContainer) return WeavingContext.builder(this::weaveEntry, configContainer)
.extensionsData(extensionsData) .extensionsData(extensionsData)
.annotations(annotations) .annotations(pojoClass)
.build(); .build();
} catch (Throwable e) { } catch (Throwable e) {
throw new PojoWeavingException("Failed to create weaving context's extension data"); throw new PojoWeavingException("Failed to create weaving context's extension data");
} }
} }
private <U> ConfigEntry<U> weaveEntry(Class<U> dataClass, WeavingContext context) { private <U> ConfigEntry<U> weaveEntry(ActualType<U> dataClass, WeavingContext context) {
for (TweedPojoWeaver weaver : weavers) { for (TweedPojoWeaver weaver : weavers) {
ConfigEntry<U> configEntry = weaver.weaveEntry(dataClass, context); ConfigEntry<U> configEntry = weaver.weaveEntry(dataClass, context);
if (configEntry != null) { if (configEntry != null) {
@@ -238,7 +232,7 @@ public class TweedPojoWeaverBootstrapper<T> {
} }
} }
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) { private void applyPostProcessors(ConfigEntry<?> configEntry, WeavingContext context) {

View File

@@ -7,14 +7,13 @@ import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.namingformat.api.NamingFormat; import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving; import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; 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 de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test; 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.isCompoundEntryForClassWith;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass; import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass;
import static org.assertj.core.api.Assertions.assertThat; 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() { WeavingContext weavingContext = WeavingContext.builder(new TweedPojoWeavingFunction.NonNull() {
@Override @Override
public @NotNull <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) { public @NotNull <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
ConfigEntry<T> entry = compoundWeaver.weaveEntry(valueClass, context); ConfigEntry<T> entry = compoundWeaver.weaveEntry(valueType, context);
if (entry != null) { if (entry != null) {
return entry; return entry;
} else { } else {
//noinspection unchecked //noinspection unchecked
ConfigEntry<T> configEntry = mock((Class<SimpleConfigEntry<T>>) (Class<?>) SimpleConfigEntry.class); ConfigEntry<T> configEntry = mock((Class<SimpleConfigEntry<T>>) (Class<?>) SimpleConfigEntry.class);
when(configEntry.valueClass()).thenReturn(valueClass); when(configEntry.valueClass()).thenReturn(valueType.declaredType());
return configEntry; return configEntry;
} }
} }
}, mock(ConfigContainer.class)) }, mock(ConfigContainer.class))
.extensionsData(new ExtensionsDataMock(null)) .extensionsData(new ExtensionsDataMock(null))
.annotations(annotations) .annotations(Compound.class)
.build(); .build();
ConfigEntry<Compound> resultEntry = compoundWeaver.weaveEntry(Compound.class, weavingContext); ConfigEntry<Compound> resultEntry = compoundWeaver.weaveEntry(ActualType.ofClass(Compound.class), weavingContext);
assertThat(resultEntry).satisfies(isCompoundEntryForClassWith(Compound.class, compoundEntry -> assertThat(compoundEntry.subEntries()) assertThat(resultEntry).satisfies(isCompoundEntryForClassWith(Compound.class, compoundEntry -> assertThat(compoundEntry.subEntries())
.hasEntrySatisfying("an-integer", isSimpleEntryForClass(int.class)) .hasEntrySatisfying("an-integer", isSimpleEntryForClass(int.class))

View File

@@ -12,21 +12,25 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type;
public class ConfigEntryAssertions { public class ConfigEntryAssertions {
public static Consumer<Object> isSimpleEntryForClass(Class<?> valueClass) { public static Consumer<Object> isSimpleEntryForClass(Class<?> valueClass) {
return object -> assertThat(object) return object -> assertThat(object)
.as("Should be a simple config entry for class " + valueClass.getName())
.asInstanceOf(type(SimpleConfigEntry.class)) .asInstanceOf(type(SimpleConfigEntry.class))
.extracting(ConfigEntry::valueClass) .extracting(ConfigEntry::valueClass)
.isEqualTo(valueClass); .isEqualTo(valueClass);
} }
@SuppressWarnings("unchecked")
public static <T> Consumer<Object> isCompoundEntryForClassWith( public static <T> Consumer<Object> isCompoundEntryForClassWith(
Class<T> compoundClass, Class<T> compoundClass,
Consumer<CompoundConfigEntry<T>> condition Consumer<CompoundConfigEntry<T>> condition
) { ) {
return object -> assertThat(object) return object -> assertThat(object)
.as("Should be a compound config entry for class " + compoundClass.getSimpleName())
.asInstanceOf(type(CompoundConfigEntry.class)) .asInstanceOf(type(CompoundConfigEntry.class))
.satisfies(compoundEntry -> { .as("Compound entry for class " + compoundClass.getSimpleName())
assertThat(compoundEntry.valueClass()).isEqualTo(compoundClass); .satisfies(
condition.accept(compoundEntry); compoundEntry -> assertThat(compoundEntry.valueClass())
}); .as("Value class of compound entry should match")
.isEqualTo(compoundClass),
condition::accept
);
} }
} }