[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

@@ -9,7 +9,7 @@ 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.ClassToInstanceMap;
import de.siphalor.tweed5.utils.api.collection.InheritanceMap;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
@@ -20,14 +20,18 @@ import java.util.*;
public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final ClassToInstanceMap<TweedExtension> extensions = new ClassToInstanceMap<>();
private final InheritanceMap<TweedExtension> extensions = new InheritanceMap<>(TweedExtension.class);
private ConfigEntry<T> rootEntry;
private PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass;
private Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions;
@Override
public <E extends TweedExtension> @Nullable E extension(Class<E> extensionClass) {
return extensions.get(extensionClass);
try {
return extensions.getSingleInstance(extensionClass);
} catch (InheritanceMap.NonUniqueResultException e) {
return null;
}
}
@Override
@@ -39,8 +43,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
public void registerExtension(TweedExtension extension) {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
TweedExtension previous = extensions.put(extension);
if (previous != null) {
if (!extensions.putIfAbsent(extension)) {
throw new IllegalArgumentException("Extension " + extension.getClass().getName() + " is already registered");
}
}
@@ -69,7 +72,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Override
public void registerExtension(TweedExtension extension) {
if (!extensions.containsClass(extension.getClass())) {
if (!extensions.containsAnyInstanceForClass(extension.getClass())) {
additionalExtensions.add(extension);
}
}
@@ -82,7 +85,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
}
for (TweedExtension additionalExtension : additionalExtensions) {
extensions.put(additionalExtension);
extensions.putIfAbsent(additionalExtension);
}
extensionsToSetup = new ArrayList<>(additionalExtensions);
additionalExtensions.clear();

View File

@@ -119,12 +119,12 @@ class ValidationFallbackExtensionImplTest {
readerWriterData.set(intEntry.extensionsData(), new EntryReaderWriterDefinition() {
@Override
public TweedEntryReader<?, ?> reader() {
return TweedEntryReaderWriters.nullableReaderWriter(TweedEntryReaderWriters.intReaderWriter());
return TweedEntryReaderWriters.nullableReader(TweedEntryReaderWriters.intReaderWriter());
}
@Override
public TweedEntryWriter<?, ?> writer() {
return TweedEntryReaderWriters.nullableReaderWriter(TweedEntryReaderWriters.intReaderWriter());
return TweedEntryReaderWriters.nullableWriter(TweedEntryReaderWriters.intReaderWriter());
}
});

View File

@@ -7,6 +7,8 @@ import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
public interface ReadWriteExtension extends TweedExtension {
void setEntryReaderWriterDefinition(ConfigEntry<?> entry, EntryReaderWriterDefinition readerWriterDefinition);
ReadWriteContextExtensionsData createReadWriteContextExtensionsData();
<T> T read(TweedDataReader reader, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryReadException;

View File

@@ -1,6 +1,5 @@
package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import lombok.RequiredArgsConstructor;
/**
@@ -13,14 +12,11 @@ public interface TweedReaderWriterProvider {
/**
* The context where reader and writer factories may be registered.<br />
* The reader and writer ids must be globally unique. It is therefore recommended to scope custom reader and writer ids in your own namespace (e.g. "de.siphalor.custom.blub")
* Ids may consist of alphanumeric characters and dots.
*/
interface ProviderContext {
void registerReaderFactory(String id, ReaderWriterFactory<? extends TweedEntryReader<?, ?>> readerFactory);
void registerWriterFactory(String id, ReaderWriterFactory<? extends TweedEntryWriter<?, ?>> writerFactory);
default void registerReaderWriterFactory(String id, ReaderWriterFactory<? extends TweedEntryReaderWriter<?, ?>> readerWriterFactory) {
registerReaderFactory(id, readerWriterFactory);
registerWriterFactory(id, readerWriterFactory);
}
void registerReaderFactory(String id, ReaderWriterFactory<TweedEntryReader<?, ?>> readerFactory);
void registerWriterFactory(String id, ReaderWriterFactory<TweedEntryWriter<?, ?>> writerFactory);
}
/**

View File

@@ -3,6 +3,8 @@ package de.siphalor.tweed5.data.extension.api.readwrite;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@@ -43,8 +45,12 @@ public class TweedEntryReaderWriters {
return TweedEntryReaderWriterImpls.STRING_READER_WRITER;
}
public static <T, C extends ConfigEntry<T>> TweedEntryReaderWriter<T, C> nullableReaderWriter(TweedEntryReaderWriter<T, C> delegate) {
return new TweedEntryReaderWriterImpls.NullableReaderWriter<>(delegate);
public static <T, C extends ConfigEntry<T>> TweedEntryReader<T, C> nullableReader(TweedEntryReader<T, C> delegate) {
return new TweedEntryReaderWriterImpls.NullableReader<>(delegate);
}
public static <T, C extends ConfigEntry<T>> TweedEntryWriter<T, C> nullableWriter(TweedEntryWriter<T, C> delegate) {
return new TweedEntryReaderWriterImpls.NullableWriter<>(delegate);
}
public static <T, C extends Collection<T>> TweedEntryReaderWriter<C, CoherentCollectionConfigEntry<T, C>> coherentCollectionReaderWriter() {

View File

@@ -1,6 +1,8 @@
package de.siphalor.tweed5.data.extension.impl;
import com.google.auto.service.AutoService;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.TweedReaderWriterProvider;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*;
@@ -9,22 +11,48 @@ import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWr
public class DefaultTweedEntryReaderWriterImplsProvider implements TweedReaderWriterProvider {
@Override
public void provideReaderWriters(ProviderContext context) {
context.registerReaderWriterFactory("boolean", new StaticReaderWriterFactory<>(booleanReaderWriter()));
context.registerReaderWriterFactory("byte", new StaticReaderWriterFactory<>(byteReaderWriter()));
context.registerReaderWriterFactory("short", new StaticReaderWriterFactory<>(shortReaderWriter()));
context.registerReaderWriterFactory("int", new StaticReaderWriterFactory<>(intReaderWriter()));
context.registerReaderWriterFactory("long", new StaticReaderWriterFactory<>(longReaderWriter()));
context.registerReaderWriterFactory("float", new StaticReaderWriterFactory<>(floatReaderWriter()));
context.registerReaderWriterFactory("double", new StaticReaderWriterFactory<>(doubleReaderWriter()));
context.registerReaderWriterFactory("string", new StaticReaderWriterFactory<>(stringReaderWriter()));
context.registerReaderWriterFactory("coherent_collection", new StaticReaderWriterFactory<>(coherentCollectionReaderWriter()));
context.registerReaderWriterFactory("compound", new StaticReaderWriterFactory<>(compoundReaderWriter()));
StaticReaderWriterFactory<TweedEntryReader<?, ?>> booleanReaderFactory = new StaticReaderWriterFactory<>(booleanReaderWriter());
StaticReaderWriterFactory<TweedEntryWriter<?, ?>> booleanWriterFactory = new StaticReaderWriterFactory<>(booleanReaderWriter());
context.registerReaderFactory("tweed5.bool", booleanReaderFactory);
context.registerReaderFactory("tweed5.boolean", booleanReaderFactory);
context.registerWriterFactory("tweed5.bool", booleanWriterFactory);
context.registerWriterFactory("tweed5.boolean", booleanWriterFactory);
context.registerReaderFactory("tweed5.byte", new StaticReaderWriterFactory<>(byteReaderWriter()));
context.registerWriterFactory("tweed5.byte", new StaticReaderWriterFactory<>(byteReaderWriter()));
context.registerReaderFactory("tweed5.short", new StaticReaderWriterFactory<>(shortReaderWriter()));
context.registerWriterFactory("tweed5.short", new StaticReaderWriterFactory<>(shortReaderWriter()));
StaticReaderWriterFactory<TweedEntryReader<?, ?>> integerReaderFactory =
new StaticReaderWriterFactory<>(intReaderWriter());
StaticReaderWriterFactory<TweedEntryWriter<?, ?>> integerWriterFactory =
new StaticReaderWriterFactory<>(intReaderWriter());
context.registerReaderFactory("tweed5.int", integerReaderFactory);
context.registerReaderFactory("tweed5.integer", integerReaderFactory);
context.registerWriterFactory("tweed5.int", integerWriterFactory);
context.registerWriterFactory("tweed5.integer", integerWriterFactory);
context.registerReaderFactory("tweed5.long", new StaticReaderWriterFactory<>(longReaderWriter()));
context.registerWriterFactory("tweed5.long", new StaticReaderWriterFactory<>(longReaderWriter()));
context.registerReaderFactory("tweed5.float", new StaticReaderWriterFactory<>(floatReaderWriter()));
context.registerWriterFactory("tweed5.float", new StaticReaderWriterFactory<>(floatReaderWriter()));
context.registerReaderFactory("tweed5.double", new StaticReaderWriterFactory<>(doubleReaderWriter()));
context.registerWriterFactory("tweed5.double", new StaticReaderWriterFactory<>(doubleReaderWriter()));
context.registerReaderFactory("tweed5.string", new StaticReaderWriterFactory<>(stringReaderWriter()));
context.registerWriterFactory("tweed5.string", new StaticReaderWriterFactory<>(stringReaderWriter()));
context.registerReaderFactory("tweed5.collection.coherent", new StaticReaderWriterFactory<>(coherentCollectionReaderWriter()));
context.registerWriterFactory("tweed5.collection.coherent", new StaticReaderWriterFactory<>(coherentCollectionReaderWriter()));
context.registerReaderFactory("tweed5.compound", new StaticReaderWriterFactory<>(compoundReaderWriter()));
context.registerWriterFactory("tweed5.compound", new StaticReaderWriterFactory<>(compoundReaderWriter()));
context.registerReaderWriterFactory("nullable", delegateReaderWriters -> {
if (delegateReaderWriters.length != 1) {
throw new IllegalArgumentException("Nullable reader writer requires a single delegate argument, got " + delegateReaderWriters.length);
context.registerReaderFactory("tweed5.nullable", delegateReaders -> {
if (delegateReaders.length != 1) {
throw new IllegalArgumentException("Nullable reader requires a single delegate argument, got " + delegateReaders.length);
}
return nullableReaderWriter(delegateReaderWriters[0]);
return nullableReader(delegateReaders[0]);
});
context.registerWriterFactory("tweed5.nullable", delegateWriters -> {
if (delegateWriters.length != 1) {
throw new IllegalArgumentException("Nullable writer requires a single delegate argument, got " + delegateWriters.length);
}
return nullableWriter(delegateWriters[0]);
});
}
}

View File

@@ -31,6 +31,7 @@ import java.util.Map;
@AutoService(ReadWriteExtension.class)
public class ReadWriteExtensionImpl implements ReadWriteExtension {
private RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> readerWriterDefinitionExtension;
private RegisteredExtensionData<EntryExtensionsData, ReadWriteEntryDataExtension> readWriteEntryDataExtension;
private DefaultMiddlewareContainer<TweedEntryReader<?, ?>> entryReaderMiddlewareContainer;
private DefaultMiddlewareContainer<TweedEntryWriter<?, ?>> entryWriterMiddlewareContainer;
@@ -44,8 +45,8 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
@Override
public void setup(TweedExtensionSetupContext context) {
readerWriterDefinitionExtension = context.registerEntryExtensionData(EntryReaderWriterDefinition.class);
readWriteEntryDataExtension = context.registerEntryExtensionData(ReadWriteEntryDataExtension.class);
context.registerEntryExtensionData(EntryReaderWriterDefinition.class);
Collection<TweedExtension> extensions = context.configContainer().extensions();
@@ -120,6 +121,11 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
));
}
@Override
public void setEntryReaderWriterDefinition(ConfigEntry<?> entry, EntryReaderWriterDefinition readerWriterDefinition) {
readerWriterDefinitionExtension.set(entry.extensionsData(), readerWriterDefinition);
}
@Override
public ReadWriteContextExtensionsData createReadWriteContextExtensionsData() {
try {

View File

@@ -32,16 +32,22 @@ public class TweedEntryReaderWriterImpls {
public static final TweedEntryReaderWriter<Object, ConfigEntry<Object>> NOOP_READER_WRITER = new NoopReaderWriter();
@RequiredArgsConstructor
public static class NullableReaderWriter<T, C extends ConfigEntry<T>> implements TweedEntryReaderWriter<T, C> {
private final TweedEntryReaderWriter<T, C> delegate;
public static class NullableReader<T, C extends ConfigEntry<T>> implements TweedEntryReader<T, C> {
private final TweedEntryReader<T, C> delegate;
@Override
public T read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException {
if (reader.peekToken().isNull()) {
reader.readToken();
return null;
}
return delegate.read(reader, entry, context);
}
}
@RequiredArgsConstructor
public static class NullableWriter<T, C extends ConfigEntry<T>> implements TweedEntryWriter<T, C> {
private final TweedEntryWriter<T, C> delegate;
@Override
public void write(TweedDataVisitor writer, T value, C entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
@@ -137,9 +143,10 @@ public class TweedEntryReaderWriterImpls {
//noinspection unchecked
ConfigEntry<Object> subEntry = (ConfigEntry<Object>) compoundEntries.get(key);
TweedEntryReader<Object, ConfigEntry<Object>> subEntryReaderChain = ReadWriteExtensionImpl.getReaderChain(subEntry);
if (subEntryReaderChain != null) {
Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context);
entry.set(compoundValue, key, subEntryValue);
}
} else {
throw new TweedEntryReadException("Unexpected token " + token + ": Expected map key or map end");
}
@@ -159,11 +166,13 @@ public class TweedEntryReaderWriterImpls {
String key = e.getKey();
ConfigEntry<Object> subEntry = e.getValue();
writer.visitMapEntryKey(key);
TweedEntryWriter<Object, ConfigEntry<Object>> subEntryWriterChain = ReadWriteExtensionImpl.getWriterChain(subEntry);
if (subEntryWriterChain != null) {
writer.visitMapEntryKey(key);
subEntryWriterChain.write(writer, entry.get(value, key), subEntry, context);
}
}
writer.visitMapEnd();
}

View File

@@ -0,0 +1,181 @@
package de.siphalor.tweed5.utils.api.collection;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import java.util.*;
@SuppressWarnings("unchecked")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class InheritanceMap<T> {
private static final InheritanceMap<Object> EMPTY = unmodifiable(new InheritanceMap<>(Object.class));
private final Class<T> baseClass;
private final Map<T, Collection<Class<? extends T>>> instanceToClasses;
private final Map<Class<? extends T>, Collection<T>> classToInstances;
public static <T> InheritanceMap<T> empty() {
return (InheritanceMap<T>) EMPTY;
}
public static <T> InheritanceMap<T> unmodifiable(InheritanceMap<T> map) {
return new Unmodifiable<>(map);
}
public InheritanceMap(Class<T> baseClass) {
this(baseClass, new IdentityHashMap<>(), new HashMap<>());
}
public int size() {
return instanceToClasses.size();
}
public boolean isEmpty() {
return instanceToClasses.isEmpty();
}
public boolean containsAnyInstanceForClass(Class<? extends T> clazz) {
return !classToInstances.getOrDefault(clazz, Collections.emptyList()).isEmpty();
}
public boolean containsSingleInstanceForClass(Class<? extends T> clazz) {
return classToInstances.getOrDefault(clazz, Collections.emptyList()).size() == 1;
}
public boolean containsInstance(T instance) {
return instanceToClasses.containsKey(instance);
}
public <V extends T> Collection<V> getAllInstances(Class<V> clazz) {
return (Collection<V>) classToInstances.getOrDefault(clazz, Collections.emptyList());
}
public <V extends T> V getSingleInstance(Class<V> clazz) throws NonUniqueResultException {
Collection<T> instances = classToInstances.getOrDefault(clazz, Collections.emptyList());
if (instances.isEmpty()) {
return null;
} else if (instances.size() == 1) {
return (V) instances.iterator().next();
} else {
throw new NonUniqueResultException("Multiple instances for class " + clazz.getName() + " exist.");
}
}
public boolean putAll(T... instances) {
boolean changed = false;
for (T instance : instances) {
changed = put(instance) || changed;
}
return changed;
}
public boolean put(T instance) {
if (instanceToClasses.containsKey(instance)) {
return false;
}
putInternal(instance);
return true;
}
public boolean putIfAbsent(T instance) {
Collection<T> existingInstances = classToInstances.getOrDefault(instance.getClass(), Collections.emptyList());
if (existingInstances.isEmpty()) {
putInternal(instance);
return true;
} else {
return false;
}
}
public <V extends T> V removeInstance(V instance) {
if (!instanceToClasses.containsKey(instance)) {
return null;
}
Collection<Class<? extends T>> classes = instanceToClasses.getOrDefault(instance, Collections.emptyList());
for (Class<? extends T> implemented : classes) {
classToInstances.getOrDefault(implemented, Collections.emptyList()).remove(instance);
}
instanceToClasses.remove(instance);
return instance;
}
public void clear() {
instanceToClasses.clear();
classToInstances.clear();
}
public Set<T> values() {
return instanceToClasses.keySet();
}
private void putInternal(T instance) {
Collection<Class<? extends T>> classes = findClasses((Class<? extends T>) instance.getClass());
instanceToClasses.put(instance, classes);
for (Class<? extends T> implementedClass : classes) {
classToInstances.computeIfAbsent(implementedClass, c -> new ArrayList<>()).add(instance);
}
}
private Collection<Class<? extends T>> findClasses(Class<? extends T> clazz) {
List<Class<? extends T>> classes = new ArrayList<>();
Class<?> superClass = clazz;
while (superClass != Object.class && superClass != baseClass && baseClass.isAssignableFrom(superClass)) {
classes.add((Class<? extends T>) superClass);
if (baseClass == Object.class || baseClass.isInterface()) {
classes.addAll(findOnlyInterfaces((Class<? extends T>) superClass));
}
superClass = superClass.getSuperclass();
}
return classes;
}
private Collection<Class<? extends T>> findOnlyInterfaces(Class<? extends T> clazz) {
List<Class<? extends T>> classes = new ArrayList<>();
for (Class<?> implemented : clazz.getInterfaces()) {
if (baseClass != implemented && baseClass.isAssignableFrom(implemented)) {
classes.add((Class<? extends T>) implemented);
classes.addAll(findOnlyInterfaces((Class<? extends T>) implemented));
}
}
return classes;
}
public static class NonUniqueResultException extends Exception {
public NonUniqueResultException(String message) {
super(message);
}
}
private static class Unmodifiable<T> extends InheritanceMap<T> {
public Unmodifiable(InheritanceMap<T> delegate) {
super(delegate.baseClass, delegate.instanceToClasses, delegate.classToInstances);
}
@Override
public boolean put(T instance) {
throw new UnsupportedOperationException();
}
@Override
public boolean putIfAbsent(T instance) {
throw new UnsupportedOperationException();
}
@Override
public <V extends T> V removeInstance(V instance) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -0,0 +1,19 @@
package de.siphalor.tweed5.utils.api.collection;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class InheritanceMapTest {
@Test
void full() {
InheritanceMap<Object> map = new InheritanceMap<>(Object.class);
map.put(123L);
map.put(123);
map.put(456);
assertThat(map.getAllInstances(Long.class)).containsExactlyInAnyOrder(123L);
assertThat(map.getAllInstances(Integer.class)).containsExactlyInAnyOrder(123, 456);
assertThat(map.getAllInstances(Number.class)).containsExactlyInAnyOrder(123L, 123, 456);
}
}

View File

@@ -0,0 +1,6 @@
dependencies {
api(project(":tweed5-weaver-pojo"))
api(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@code <spec> = <id> [ "(" <spec> ( "," <spec> )* ")" ] }
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EntryReadWriteConfig {
String value() default "";
String writer() default "";
String reader() default "";
}

View File

@@ -0,0 +1,182 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.impl.SerdePojoReaderWriterSpec;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
@Slf4j
public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor {
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>>> readerFactories = new HashMap<>();
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>>> writerFactories = new HashMap<>();
public ReadWritePojoPostProcessor() {
loadProviders();
}
private void loadProviders() {
ServiceLoader<TweedReaderWriterProvider> serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class);
for (TweedReaderWriterProvider readerWriterProvider : serviceLoader) {
TweedReaderWriterProvider.ProviderContext providerContext = new TweedReaderWriterProvider.ProviderContext() {
@Override
public void registerReaderFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>> readerFactory
) {
if (readerFactories.putIfAbsent(id, readerFactory) != null) {
log.warn(
"Found duplicate Tweed entry reader id \"{}\" in provider class {}",
id,
readerWriterProvider.getClass().getName()
);
}
}
@Override
public void registerWriterFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>> writerFactory
) {
if (writerFactories.putIfAbsent(id, writerFactory) != null) {
log.warn(
"Found duplicate Tweed entry writer id \"{}\" in provider class {}",
id,
readerWriterProvider.getClass().getName()
);
}
}
};
readerWriterProvider.provideReaderWriters(providerContext);
}
}
@Override
public void apply(ConfigEntry<?> configEntry, WeavingContext context) {
EntryReadWriteConfig entryConfig = context.annotations().getAnnotation(EntryReadWriteConfig.class);
if (entryConfig == null) {
return;
}
ReadWriteExtension readWriteExtension = context.configContainer().extension(ReadWriteExtension.class);
if (readWriteExtension == null) {
log.error("You must not use {} without the {}", this.getClass().getSimpleName(), ReadWriteExtension.class.getSimpleName());
return;
}
readWriteExtension.setEntryReaderWriterDefinition(configEntry, createDefinitionFromEntryConfig(entryConfig, context));
}
private EntryReaderWriterDefinition createDefinitionFromEntryConfig(EntryReadWriteConfig entryConfig, WeavingContext context) {
String readerSpecText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader();
String writerSpecText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer();
SerdePojoReaderWriterSpec readerSpec;
SerdePojoReaderWriterSpec writerSpec;
if (readerSpecText.equals(writerSpecText)) {
readerSpec = writerSpec = specFromText(readerSpecText, context);
} else {
readerSpec = specFromText(readerSpecText, context);
writerSpec = specFromText(writerSpecText, context);
}
//noinspection unchecked
TweedEntryReader<?, ?> reader = readerSpec == null
? TweedEntryReaderWriterImpls.NOOP_READER_WRITER
: resolveReaderWriterFromSpec((Class<TweedEntryReader<?, ?>>)(Object) TweedEntryReader.class, readerFactories, readerSpec, context);
//noinspection unchecked
TweedEntryWriter<?, ?> writer = writerSpec == null
? TweedEntryReaderWriterImpls.NOOP_READER_WRITER
: resolveReaderWriterFromSpec((Class<TweedEntryWriter<?, ?>>)(Object) TweedEntryWriter.class, writerFactories, writerSpec, context);
return new EntryReaderWriterDefinition() {
@Override
public TweedEntryReader<?, ?> reader() {
return reader;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return writer;
}
};
}
@Nullable
private SerdePojoReaderWriterSpec specFromText(String specText, WeavingContext context) {
if (specText.isEmpty()) {
return null;
}
try {
return SerdePojoReaderWriterSpec.parse(specText);
} catch (SerdePojoReaderWriterSpec.ParseException e) {
log.warn(
"Failed to parse definition for reader or writer on entry {}, entry will not be included in serde",
context.path(),
e
);
return null;
}
}
private <T> T resolveReaderWriterFromSpec(
Class<T> baseClass,
Map<String, TweedReaderWriterProvider.ReaderWriterFactory<T>> factories,
SerdePojoReaderWriterSpec spec,
WeavingContext context
) {
//noinspection unchecked
T[] arguments = spec.arguments()
.stream()
.map(argSpec -> resolveReaderWriterFromSpec(baseClass, factories, argSpec, context))
.toArray(length -> (T[]) Array.newInstance(baseClass, length));
TweedReaderWriterProvider.ReaderWriterFactory<T> factory = factories.get(spec.identifier());
T instance;
if (factory != null) {
instance = factory.create(arguments);
} else {
instance = loadClassIfExists(baseClass, spec.identifier(), arguments);
}
if (instance == null) {
log.warn(
"Failed to resolve reader or writer factory \"{}\" for entry {}, entry will not be included in serde",
spec.identifier(),
context.path()
);
return null;
}
return instance;
}
private <T> T loadClassIfExists(Class<T> baseClass, String className, T[] arguments) {
try {
Class<?> clazz = Class.forName(className);
Class<?>[] argClassses = new Class<?>[arguments.length];
Arrays.fill(argClassses, baseClass);
Constructor<?> constructor = clazz.getConstructor(argClassses);
//noinspection unchecked
return (T) constructor.newInstance((Object[]) arguments);
} catch (ClassNotFoundException e) {
return null;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
log.warn("Failed to instantiate class {}", className, e);
return null;
}
}
}

View File

@@ -0,0 +1,170 @@
package de.siphalor.tweed5.weaver.pojoext.serde.impl;
import lombok.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.PrimitiveIterator;
@Value
public class SerdePojoReaderWriterSpec {
String identifier;
List<SerdePojoReaderWriterSpec> arguments;
public static SerdePojoReaderWriterSpec parse(String input) throws ParseException {
Lexer lexer = new Lexer(input.codePoints().iterator());
SerdePojoReaderWriterSpec spec = parseSpec(lexer);
lexer.chompWhitespace();
int codePoint = lexer.nextCodePoint();
if (codePoint != -1) {
throw lexer.createException("Found trailing text after spec", codePoint);
}
return spec;
}
private static SerdePojoReaderWriterSpec parseSpec(Lexer lexer) throws ParseException {
lexer.chompWhitespace();
String identifier = lexer.nextIdentifier();
lexer.chompWhitespace();
int codePoint = lexer.peekCodePoint();
if (codePoint == '(') {
lexer.nextCodePoint();
lexer.chompWhitespace();
if (lexer.peekCodePoint() == ')') {
lexer.nextCodePoint();
return new SerdePojoReaderWriterSpec(identifier, Collections.emptyList());
}
SerdePojoReaderWriterSpec spec = new SerdePojoReaderWriterSpec(identifier, parseSpecList(lexer));
codePoint = lexer.nextCodePoint();
if (codePoint != ')') {
throw lexer.createException("Argument list must be ended with a closing parenthesis", codePoint);
}
return spec;
} else {
return new SerdePojoReaderWriterSpec(identifier, Collections.emptyList());
}
}
private static List<SerdePojoReaderWriterSpec> parseSpecList(Lexer lexer) throws ParseException {
List<SerdePojoReaderWriterSpec> specs = new ArrayList<>();
while (true) {
specs.add(parseSpec(lexer));
lexer.chompWhitespace();
int codePoint = lexer.peekCodePoint();
if (codePoint != ',') {
break;
}
lexer.nextCodePoint();
}
return Collections.unmodifiableList(specs);
}
@RequiredArgsConstructor
private static class Lexer {
private static final int EMPTY = -2;
private final PrimitiveIterator.OfInt codePointIterator;
private int peek = EMPTY;
private int index;
public String nextIdentifier() throws ParseException {
int codePoint = nextCodePoint();
if (codePoint == -1) {
throw createException("Expected identifier, got end of input", codePoint);
} else if (!isIdentifierChar(codePoint)) {
throw createException("Expected identifier (alphanumeric character)", codePoint);
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.appendCodePoint(codePoint);
boolean dot = false;
while ((codePoint = peekCodePoint()) >= 0) {
if (isIdentifierChar(codePoint)) {
stringBuilder.appendCodePoint(nextCodePoint());
dot = false;
} else if (codePoint == '.') {
if (dot) {
throw createException("Unexpected double dot in identifier", codePoint);
} else {
stringBuilder.appendCodePoint(nextCodePoint());
dot = true;
}
} else {
break;
}
}
if (dot) {
throw createException("Identifier must not end with dot", codePoint);
}
return stringBuilder.toString();
}
private boolean isIdentifierChar(int codePoint) {
return (codePoint >= '0' && codePoint <= '9')
|| (codePoint >= 'a' && codePoint <= 'z')
|| (codePoint >= 'A' && codePoint <= 'Z');
}
public void chompWhitespace() {
while (Character.isWhitespace(peekCodePoint())) {
nextCodePoint();
}
}
private int peekCodePoint() {
if (peek == EMPTY) {
peek = nextCodePoint();
}
return peek;
}
private int nextCodePoint() {
if (peek != EMPTY) {
int codePoint = peek;
peek = EMPTY;
return codePoint;
}
if (codePointIterator.hasNext()) {
index++;
return codePointIterator.nextInt();
} else {
return -1;
}
}
public ParseException createException(String message, int codePoint) {
return new ParseException(message, index, codePoint);
}
}
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public static class ParseException extends Exception {
private final int index;
private final int codePoint;
public ParseException(String message, int index, int codePoint) {
super(message);
this.index = index;
this.codePoint = codePoint;
}
@Override
public String getMessage() {
String message = super.getMessage();
StringBuilder stringBuilder = new StringBuilder(30 + message.length())
.append("Parse error at index ")
.append(index)
.append(" \"");
if (codePoint == -1) {
stringBuilder.append("EOF");
} else {
stringBuilder.appendCodePoint(codePoint);
}
return stringBuilder
.append("\": ")
.append(message)
.toString();
}
}
}

View File

@@ -0,0 +1,101 @@
package de.siphalor.tweed5.weaver.pojoext.serde;
import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.TweedPojoWeaverBootstrapper;
import de.siphalor.tweed5.weaver.pojoext.serde.api.EntryReadWriteConfig;
import de.siphalor.tweed5.weaver.pojoext.serde.api.ReadWritePojoPostProcessor;
import lombok.*;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.io.StringWriter;
import static org.assertj.core.api.Assertions.assertThat;
class WeaverPojoSerdeExtensionTest {
@Test
@SneakyThrows
void testAnnotated() {
TweedPojoWeaverBootstrapper<AnnotatedConfig> weaverBootstrapper = TweedPojoWeaverBootstrapper.create(AnnotatedConfig.class);
ConfigContainer<AnnotatedConfig> configContainer = weaverBootstrapper.weave();
configContainer.initialize();
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class);
assertThat(readWriteExtension).isNotNull();
AnnotatedConfig config = new AnnotatedConfig(123, "test", new TestClass(987));
StringWriter stringWriter = new StringWriter();
HjsonWriter hjsonWriter = new HjsonWriter(stringWriter, new HjsonWriter.Options());
readWriteExtension.write(hjsonWriter, config, configContainer.rootEntry(), readWriteExtension.createReadWriteContextExtensionsData());
assertThat(stringWriter).hasToString("{\n\tanInt: 123\n\ttext: test\n\ttest: my cool custom writer\n}\n");
HjsonReader reader = new HjsonReader(new HjsonLexer(new StringReader(
"{\n\tanInt: 987\n\ttext: abdef\n\ttest: { inner: 29 }\n}"
)));
assertThat(readWriteExtension.read(
reader,
configContainer.rootEntry(),
readWriteExtension.createReadWriteContextExtensionsData()
)).isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
}
@AutoService(TweedReaderWriterProvider.class)
public static class TestWriterProvider implements TweedReaderWriterProvider {
@Override
public void provideReaderWriters(ProviderContext context) {
context.registerWriterFactory("tweed5.test.dummy", delegates -> new TweedEntryWriter<Object, ConfigEntry<Object>>() {
@Override
public void write(
TweedDataVisitor writer,
Object value,
ConfigEntry<Object> entry,
TweedWriteContext context
) throws TweedDataWriteException {
writer.visitString("my cool custom writer");
}
});
}
}
@PojoWeaving(extensions = ReadWriteExtension.class, postProcessors = ReadWritePojoPostProcessor.class)
@CompoundWeaving
@EntryReadWriteConfig("tweed5.compound")
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public static class AnnotatedConfig {
@EntryReadWriteConfig("tweed5.integer")
public int anInt;
@EntryReadWriteConfig("tweed5.nullable(tweed5.string)")
public String text;
@EntryReadWriteConfig(writer = "tweed5.test.dummy", reader = "tweed5.compound")
@CompoundWeaving
public TestClass test;
}
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public static class TestClass {
@EntryReadWriteConfig("tweed5.integer")
public int inner;
}
}

View File

@@ -0,0 +1,65 @@
package de.siphalor.tweed5.weaver.pojoext.serde.impl;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.util.Arrays;
import java.util.Collections;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
class SerdePojoReaderWriterSpecTest {
@ParameterizedTest
@CsvSource(ignoreLeadingAndTrailingWhitespace = false, value = {
" abc ,abc",
" abc() ,abc",
" abc.123 ,abc.123",
"abc.123 ( ) ,abc.123",
"123.abc,123.abc",
})
@SneakyThrows
void parseSimpleIdentifier(String input, String identifier) {
SerdePojoReaderWriterSpec spec = SerdePojoReaderWriterSpec.parse(input);
assertThat(spec.identifier()).isEqualTo(identifier);
assertThat(spec.arguments()).isEmpty();
}
@Test
@SneakyThrows
void parseNested() {
SerdePojoReaderWriterSpec spec = SerdePojoReaderWriterSpec.parse("abc.def ( 12 ( def, ghi ( ) ), jkl ) ");
assertThat(spec).isEqualTo(new SerdePojoReaderWriterSpec("abc.def", Arrays.asList(
new SerdePojoReaderWriterSpec("12", Arrays.asList(
new SerdePojoReaderWriterSpec("def", Collections.emptyList()),
new SerdePojoReaderWriterSpec("ghi", Collections.emptyList())
)),
new SerdePojoReaderWriterSpec("jkl", Collections.emptyList())
)));
}
@ParameterizedTest
@CsvSource(ignoreLeadingAndTrailingWhitespace = false, nullValues = "EOF", delimiter = ';', value = {
" abc def ;6;d",
"abcäöüdef;4;ä",
"abc.def(;8;EOF",
"'';0;EOF",
",;1;,",
"abc(,);5;,",
"abc..def;5;.",
})
@SneakyThrows
void parseError(String input, int index, String codePoint) {
assertThatThrownBy(() -> SerdePojoReaderWriterSpec.parse(input))
.asInstanceOf(type(SerdePojoReaderWriterSpec.ParseException.class))
.isInstanceOf(SerdePojoReaderWriterSpec.ParseException.class)
.satisfies(
exception -> assertThat(exception.index()).as("index of: " + exception.getMessage()).isEqualTo(index),
exception -> assertThat(exception.codePoint()).as("code point of: " + exception.getMessage())
.isEqualTo(codePoint == null ? -1 : codePoint.codePointAt(0))
);
}
}

View File

@@ -1,6 +1,4 @@
dependencies {
api(project(":tweed5-core"))
api(project(":tweed5-naming-format"))
compileOnly(project(":tweed5-default-extensions"))
compileOnly(project(":tweed5-serde-extension"))
}

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 weaverClasses.stream()
.map(weaverClass -> checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass))
.collect(Collectors.toList());
}
return weavers;
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) {
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);