[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

@@ -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.

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.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 <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) {
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<T> 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;
}
}

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.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 <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
return new SimpleConfigEntryImpl<>(valueClass);
public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
return new SimpleConfigEntryImpl<>(valueType.declaredType());
}
}

View File

@@ -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
<T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context);
<T> ConfigEntry<T> weaveEntry(ActualType<T> 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 <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.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 <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
return weavingFunction.weaveEntry(valueClass, context);
public @NotNull <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
return weavingFunction.weaveEntry(valueType, context);
}
public interface ExtensionsData extends Patchwork<ExtensionsData> {}
@@ -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(

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.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<T> {
setupWeavers();
WeavingContext weavingContext = createWeavingContext();
ConfigEntry<T> rootEntry = this.weaveEntry(pojoClass, weavingContext);
ConfigEntry<T> rootEntry = this.weaveEntry(ActualType.ofClass(pojoClass), weavingContext);
configContainer.attachAndSealTree(rootEntry);
return configContainer;
@@ -214,19 +211,16 @@ public class TweedPojoWeaverBootstrapper<T> {
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 <U> ConfigEntry<U> weaveEntry(Class<U> dataClass, WeavingContext context) {
private <U> ConfigEntry<U> weaveEntry(ActualType<U> dataClass, WeavingContext context) {
for (TweedPojoWeaver weaver : weavers) {
ConfigEntry<U> configEntry = weaver.weaveEntry(dataClass, context);
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) {

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.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 <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
ConfigEntry<T> entry = compoundWeaver.weaveEntry(valueClass, context);
public @NotNull <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
ConfigEntry<T> entry = compoundWeaver.weaveEntry(valueType, context);
if (entry != null) {
return entry;
} else {
//noinspection unchecked
ConfigEntry<T> configEntry = mock((Class<SimpleConfigEntry<T>>) (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<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())
.hasEntrySatisfying("an-integer", isSimpleEntryForClass(int.class))

View File

@@ -12,21 +12,25 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type;
public class ConfigEntryAssertions {
public static Consumer<Object> 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 <T> Consumer<Object> isCompoundEntryForClassWith(
Class<T> compoundClass,
Consumer<CompoundConfigEntry<T>> 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
);
}
}