[weaver-pojo] Introduce pojo weaver post processors

This commit is contained in:
2024-12-09 23:35:26 +01:00
parent aaf05d1a33
commit f10a23a0f5
27 changed files with 1078 additions and 97 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)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface CompoundWeaving {
/**
* The naming format to use for this POJO.

View File

@@ -6,6 +6,7 @@ import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.weaver.pojo.api.weaving.CompoundPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TrivialPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -22,5 +23,7 @@ public @interface PojoWeaving {
TrivialPojoWeaver.class,
};
Class<? extends TweedPojoWeavingPostProcessor>[] postProcessors() default {};
Class<? extends TweedExtension>[] extensions() default {};
}

View File

@@ -0,0 +1,140 @@
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

@@ -1,6 +1,5 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.collection.TypedMultimap;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
@@ -8,19 +7,18 @@ 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.weaver.pojo.impl.entry.StaticPojoCompoundConfigEntry;
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.entry.StaticPojoCompoundConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfigImpl;
import org.jetbrains.annotations.NotNull;
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;
import java.util.*;
import java.lang.reflect.Field;
import java.util.Map;
/**
* A weaver that weaves classes with the {@link CompoundWeaving} annotation as compound entries.
@@ -43,11 +41,11 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
@Override
public @Nullable <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
if (!valueClass.isAnnotationPresent(CompoundWeaving.class)) {
if (context.annotations().getAnnotation(CompoundWeaving.class) == null) {
return null;
}
try {
CompoundWeavingConfig weavingConfig = getOrCreateWeavingConfig(valueClass, context);
CompoundWeavingConfig weavingConfig = getOrCreateWeavingConfig(context);
WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy();
weavingConfigAccess.set(newExtensionsData, weavingConfig);
@@ -68,7 +66,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
}
}
private CompoundWeavingConfig getOrCreateWeavingConfig(Class<?> valueClass, WeavingContext context) {
private CompoundWeavingConfig getOrCreateWeavingConfig(WeavingContext context) {
CompoundWeavingConfig parent;
if (context.extensionsData().isPatchworkPartSet(CompoundWeavingConfig.class)) {
parent = (CompoundWeavingConfig) context.extensionsData();
@@ -76,7 +74,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
parent = DEFAULT_WEAVING_CONFIG;
}
CompoundWeavingConfig local = getWeavingConfigFromClassAnnotation(valueClass);
CompoundWeavingConfig local = createWeavingConfigFromAnnotations(context.annotations());
if (local == null) {
return parent;
}
@@ -91,23 +89,21 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
WeavingContext parentContext
) {
return parentContext.subContextBuilder(name)
.additionalData(createAdditionalDataFromAnnotations(property.field().getAnnotations()))
.annotations(collectAnnotationsForField(property.field()))
.extensionsData(newExtensionsData)
.build();
}
private TypedMultimap<Object> createAdditionalDataFromAnnotations(Annotation[] annotations) {
if (annotations.length == 0) {
return TypedMultimap.empty();
}
TypedMultimap<Object> additionalData = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
Collections.addAll(additionalData, annotations);
return TypedMultimap.unmodifiable(additionalData);
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 getWeavingConfigFromClassAnnotation(Class<?> clazz) {
CompoundWeaving annotation = clazz.getAnnotation(CompoundWeaving.class);
private CompoundWeavingConfig createWeavingConfigFromAnnotations(Annotations annotations) {
CompoundWeaving annotation = annotations.getAnnotation(CompoundWeaving.class);
if (annotation == null) {
return null;
}

View File

@@ -1,8 +1,8 @@
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.core.api.collection.TypedMultimap;
import lombok.*;
import lombok.experimental.Accessors;
import org.jetbrains.annotations.NotNull;
@@ -14,26 +14,30 @@ import java.util.Arrays;
public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
@Nullable
WeavingContext parent;
ExtensionsData extensionsData;
@Getter(AccessLevel.NONE)
@NotNull
TweedPojoWeavingFunction.NonNull weavingFunction;
@NotNull
ConfigContainer<?> configContainer;
@NotNull
String[] path;
TypedMultimap<Object> additionalData;
@NotNull
ExtensionsData extensionsData;
@NotNull
Annotations annotations;
public static Builder builder() {
return new Builder(null, new String[0]);
public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer<?> configContainer) {
return new Builder(null, weavingFunction, configContainer, new String[0]);
}
public static Builder builder(String baseName) {
return new Builder(null, new String[]{ baseName });
public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer<?> configContainer, String baseName) {
return new Builder(null, weavingFunction, configContainer, new String[]{ baseName });
}
public Builder subContextBuilder(String subPathName) {
String[] newPath = Arrays.copyOf(path, path.length + 1);
newPath[path.length] = subPathName;
return new Builder(this, newPath)
.extensionsData(extensionsData)
.weavingFunction(weavingFunction);
return new Builder(this, weavingFunction, configContainer, newPath).extensionsData(extensionsData);
}
@Override
@@ -49,18 +53,20 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
public static class Builder {
@Nullable
private final WeavingContext parent;
private final TweedPojoWeavingFunction.NonNull weavingFunction;
private final ConfigContainer<?> configContainer;
private final String[] path;
private ExtensionsData extensionsData;
private TweedPojoWeavingFunction.NonNull weavingFunction;
private TypedMultimap<Object> additionalData;
private Annotations annotations;
public WeavingContext build() {
return new WeavingContext(
parent,
extensionsData,
weavingFunction,
configContainer,
path,
additionalData
extensionsData,
annotations
);
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
public interface TweedPojoWeavingPostProcessor {
void apply(ConfigEntry<?> configEntry, WeavingContext context);
}

View File

@@ -9,13 +9,13 @@ import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class StaticPojoCompoundConfigEntry<T> extends BaseConfigEntry<T> implements WeavableCompoundConfigEntry<T> {
private final MethodHandle noArgsConstructor;
private final Map<String, SubEntry> subEntries = new HashMap<>();
private final Map<String, ConfigEntry<?>> subConfigEntries = new HashMap<>();
private final Map<String, SubEntry> subEntries = new LinkedHashMap<>();
private final Map<String, ConfigEntry<?>> subConfigEntries = new LinkedHashMap<>();
public StaticPojoCompoundConfigEntry(@NotNull Class<T> valueClass, @NotNull MethodHandle noArgsConstructor) {
super(valueClass);
@@ -67,7 +67,7 @@ public class StaticPojoCompoundConfigEntry<T> extends BaseConfigEntry<T> impleme
public T instantiateCompoundValue() {
try {
//noinspection unchecked
return (T) noArgsConstructor.invokeExact();
return (T) noArgsConstructor.invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to instantiate compound class", e);
}

View File

@@ -14,7 +14,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Slf4j
@@ -46,7 +46,7 @@ public class PojoClassIntrospector {
public Map<String, Property> properties() {
if (this.properties == null) {
this.properties = new HashMap<>();
this.properties = new LinkedHashMap<>();
Class<?> currentClass = clazz;
while (currentClass != null) {
appendClassProperties(currentClass);

View File

@@ -8,27 +8,36 @@ 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.weaver.pojo.api.weaving.TweedPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor;
import lombok.*;
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;
import java.util.*;
import java.util.stream.Collectors;
/**
* A class that sets up and handles all the bits and bobs for weaving a {@link ConfigContainer} out of a POJO.
* The POJO must be annotated with {@link PojoWeaving}.
*/
@Slf4j
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedPojoWeaverBootstrapper<T> {
private final Class<T> pojoClass;
private final ConfigContainer<T> configContainer;
private final Collection<TweedPojoWeaver> weavers;
private final Collection<TweedPojoWeavingPostProcessor> postProcessors;
private PatchworkClass<WeavingContext.ExtensionsData> contextExtensionsDataClass;
public static <T> TweedPojoWeaverBootstrapper<T> create(Class<T> pojoClass) {
@@ -37,11 +46,14 @@ public class TweedPojoWeaverBootstrapper<T> {
//noinspection unchecked
ConfigContainer<T> configContainer = (ConfigContainer<T>) createConfigContainer((Class<? extends ConfigContainer<?>>) rootWeavingConfig.container());
Collection<TweedPojoWeaver> weavers = loadWeavers(Arrays.asList(rootWeavingConfig.weavers()));
Collection<TweedPojoWeavingPostProcessor> postProcessors = loadPostProcessors(Arrays.asList(rootWeavingConfig.postProcessors()));
Collection<TweedExtension> extensions = loadExtensions(Arrays.asList(rootWeavingConfig.extensions()));
configContainer.registerExtensions(extensions.toArray(new TweedExtension[0]));
configContainer.finishExtensionSetup();
return new TweedPojoWeaverBootstrapper<>(pojoClass, configContainer, loadWeavers(Arrays.asList(rootWeavingConfig.weavers())));
return new TweedPojoWeaverBootstrapper<>(pojoClass, configContainer, weavers, postProcessors);
}
private static Collection<TweedExtension> loadExtensions(Collection<Class<? extends TweedExtension>> extensionClasses) {
@@ -53,11 +65,15 @@ public class TweedPojoWeaverBootstrapper<T> {
}
private static Collection<TweedPojoWeaver> loadWeavers(Collection<Class<? extends TweedPojoWeaver>> weaverClasses) {
List<TweedPojoWeaver> weavers = new ArrayList<>();
for (Class<? extends TweedPojoWeaver> weaverClass : weaverClasses) {
weavers.add(checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass));
}
return weavers;
return weaverClasses.stream()
.map(weaverClass -> checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass))
.collect(Collectors.toList());
}
private static Collection<TweedPojoWeavingPostProcessor> loadPostProcessors(Collection<Class<? extends TweedPojoWeavingPostProcessor>> postProcessorClasses) {
return postProcessorClasses.stream()
.map(postProcessorClass -> checkImplementsAndInstantiate(TweedPojoWeavingPostProcessor.class, postProcessorClass))
.collect(Collectors.toList());
}
private static ConfigContainer<?> createConfigContainer(Class<? extends ConfigContainer<?>> containerClass) {
@@ -197,9 +213,13 @@ public class TweedPojoWeaverBootstrapper<T> {
private WeavingContext createWeavingContext() {
try {
WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke();
return WeavingContext.builder()
Annotations annotations = new Annotations();
annotations.addAnnotationsFrom(ElementType.TYPE, pojoClass);
return WeavingContext.builder(this::weaveEntry, configContainer)
.extensionsData(extensionsData)
.weavingFunction(this::weaveEntry)
.annotations(annotations)
.build();
} catch (Throwable e) {
throw new PojoWeavingException("Failed to create weaving context's extension data");
@@ -210,7 +230,10 @@ public class TweedPojoWeaverBootstrapper<T> {
for (TweedPojoWeaver weaver : weavers) {
ConfigEntry<U> configEntry = weaver.weaveEntry(dataClass, context);
if (configEntry != null) {
configEntry.seal(configContainer);
if (!configEntry.sealed()) {
configEntry.seal(configContainer);
}
applyPostProcessors(configEntry, context);
return configEntry;
}
}
@@ -218,6 +241,16 @@ public class TweedPojoWeaverBootstrapper<T> {
throw new PojoWeavingException("Failed to weave " + dataClass.getName() + ": No matching weavers found");
}
private void applyPostProcessors(ConfigEntry<?> configEntry, WeavingContext context) {
for (TweedPojoWeavingPostProcessor postProcessor : postProcessors) {
try {
postProcessor.apply(configEntry, context);
} catch (Exception e) {
log.error("Failed to apply Tweed POJO weaver post processor", e);
}
}
}
@Setter
private static class RegisteredExtensionDataImpl<E> implements RegisteredExtensionData<WeavingContext.ExtensionsData, E> {
private MethodHandle setter;

View File

@@ -1,5 +1,6 @@
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.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
@@ -12,6 +13,8 @@ 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;
@@ -30,9 +33,10 @@ class CompoundPojoWeaverTest {
}
});
WeavingContext weavingContext = WeavingContext.builder()
.extensionsData(new ExtensionsDataMock(null))
.weavingFunction(new TweedPojoWeavingFunction.NonNull() {
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);
@@ -45,7 +49,9 @@ class CompoundPojoWeaverTest {
return configEntry;
}
}
})
}, mock(ConfigContainer.class))
.extensionsData(new ExtensionsDataMock(null))
.annotations(annotations)
.build();
ConfigEntry<Compound> resultEntry = compoundWeaver.weaveEntry(Compound.class, weavingContext);