From f10a23a0f5a2950d3b841be1d132c1e786aa67ce Mon Sep 17 00:00:00 2001 From: Siphalor Date: Mon, 9 Dec 2024 23:35:26 +0100 Subject: [PATCH] [weaver-pojo] Introduce pojo weaver post processors --- .../core/impl/DefaultConfigContainer.java | 17 +- .../ValidationFallbackExtensionImplTest.java | 4 +- .../extension/api/ReadWriteExtension.java | 2 + .../api/TweedReaderWriterProvider.java | 10 +- .../readwrite/TweedEntryReaderWriters.java | 10 +- ...ltTweedEntryReaderWriterImplsProvider.java | 56 ++++-- .../impl/ReadWriteExtensionImpl.java | 8 +- .../impl/TweedEntryReaderWriterImpls.java | 25 ++- .../utils/api/collection/InheritanceMap.java | 181 +++++++++++++++++ .../api/collection/InheritanceMapTest.java | 19 ++ .../build.gradle.kts | 6 + .../serde/api/EntryReadWriteConfig.java | 17 ++ .../serde/api/ReadWritePojoPostProcessor.java | 182 ++++++++++++++++++ .../serde/impl/SerdePojoReaderWriterSpec.java | 170 ++++++++++++++++ .../serde/WeaverPojoSerdeExtensionTest.java | 101 ++++++++++ .../impl/SerdePojoReaderWriterSpecTest.java | 65 +++++++ tweed5-weaver-pojo/build.gradle.kts | 2 - .../pojo/api/annotation/CompoundWeaving.java | 2 +- .../pojo/api/annotation/PojoWeaving.java | 3 + .../weaver/pojo/api/weaving/Annotations.java | 140 ++++++++++++++ .../pojo/api/weaving/CompoundPojoWeaver.java | 36 ++-- .../pojo/api/weaving/WeavingContext.java | 34 ++-- .../TweedPojoWeavingPostProcessor.java | 8 + .../entry/StaticPojoCompoundConfigEntry.java | 8 +- .../impl/weaving/PojoClassIntrospector.java | 4 +- .../weaving/TweedPojoWeaverBootstrapper.java | 51 ++++- .../api/weaving/CompoundPojoWeaverTest.java | 14 +- 27 files changed, 1078 insertions(+), 97 deletions(-) create mode 100644 tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/InheritanceMap.java create mode 100644 tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/InheritanceMapTest.java create mode 100644 tweed5-weaver-pojo-serde-extension/build.gradle.kts create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/EntryReadWriteConfig.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpec.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/WeaverPojoSerdeExtensionTest.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpecTest.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/DefaultConfigContainer.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/DefaultConfigContainer.java index 25a28b7..8e6ebfa 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/DefaultConfigContainer.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/DefaultConfigContainer.java @@ -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 implements ConfigContainer { @Getter private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP; - private final ClassToInstanceMap extensions = new ClassToInstanceMap<>(); + private final InheritanceMap extensions = new InheritanceMap<>(TweedExtension.class); private ConfigEntry rootEntry; private PatchworkClass entryExtensionsDataPatchworkClass; private Map, RegisteredExtensionDataImpl> registeredEntryDataExtensions; @Override public @Nullable E extension(Class extensionClass) { - return extensions.get(extensionClass); + try { + return extensions.getSingleInstance(extensionClass); + } catch (InheritanceMap.NonUniqueResultException e) { + return null; + } } @Override @@ -39,8 +43,7 @@ public class DefaultConfigContainer implements ConfigContainer { 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 implements ConfigContainer { @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 implements ConfigContainer { } for (TweedExtension additionalExtension : additionalExtensions) { - extensions.put(additionalExtension); + extensions.putIfAbsent(additionalExtension); } extensionsToSetup = new ArrayList<>(additionalExtensions); additionalExtensions.clear(); diff --git a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java index f0f4ce5..c1cb2f2 100644 --- a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java +++ b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java @@ -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()); } }); diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java index 7dc3c3c..dfc2b8d 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java @@ -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 read(TweedDataReader reader, ConfigEntry entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryReadException; diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/TweedReaderWriterProvider.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/TweedReaderWriterProvider.java index ada4da4..1d5dc70 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/TweedReaderWriterProvider.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/TweedReaderWriterProvider.java @@ -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.
* 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> readerFactory); - void registerWriterFactory(String id, ReaderWriterFactory> writerFactory); - default void registerReaderWriterFactory(String id, ReaderWriterFactory> readerWriterFactory) { - registerReaderFactory(id, readerWriterFactory); - registerWriterFactory(id, readerWriterFactory); - } + void registerReaderFactory(String id, ReaderWriterFactory> readerFactory); + void registerWriterFactory(String id, ReaderWriterFactory> writerFactory); } /** diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java index 3908cc4..056b0bc 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java @@ -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 > TweedEntryReaderWriter nullableReaderWriter(TweedEntryReaderWriter delegate) { - return new TweedEntryReaderWriterImpls.NullableReaderWriter<>(delegate); + public static > TweedEntryReader nullableReader(TweedEntryReader delegate) { + return new TweedEntryReaderWriterImpls.NullableReader<>(delegate); + } + + public static > TweedEntryWriter nullableWriter(TweedEntryWriter delegate) { + return new TweedEntryReaderWriterImpls.NullableWriter<>(delegate); } public static > TweedEntryReaderWriter> coherentCollectionReaderWriter() { diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/DefaultTweedEntryReaderWriterImplsProvider.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/DefaultTweedEntryReaderWriterImplsProvider.java index 763a294..50a4dcd 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/DefaultTweedEntryReaderWriterImplsProvider.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/DefaultTweedEntryReaderWriterImplsProvider.java @@ -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> booleanReaderFactory = new StaticReaderWriterFactory<>(booleanReaderWriter()); + StaticReaderWriterFactory> 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> integerReaderFactory = + new StaticReaderWriterFactory<>(intReaderWriter()); + StaticReaderWriterFactory> 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]); }); } } diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java index 4ed47b9..15e8a9c 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java @@ -31,6 +31,7 @@ import java.util.Map; @AutoService(ReadWriteExtension.class) public class ReadWriteExtensionImpl implements ReadWriteExtension { + private RegisteredExtensionData readerWriterDefinitionExtension; private RegisteredExtensionData readWriteEntryDataExtension; private DefaultMiddlewareContainer> entryReaderMiddlewareContainer; private DefaultMiddlewareContainer> 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 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 { diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java index 0d8b49e..eeab26c 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java @@ -32,16 +32,22 @@ public class TweedEntryReaderWriterImpls { public static final TweedEntryReaderWriter> NOOP_READER_WRITER = new NoopReaderWriter(); @RequiredArgsConstructor - public static class NullableReaderWriter> implements TweedEntryReaderWriter { - private final TweedEntryReaderWriter delegate; + public static class NullableReader> implements TweedEntryReader { + private final TweedEntryReader 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> implements TweedEntryWriter { + private final TweedEntryWriter 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 subEntry = (ConfigEntry) compoundEntries.get(key); TweedEntryReader> subEntryReaderChain = ReadWriteExtensionImpl.getReaderChain(subEntry); - - Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context); - entry.set(compoundValue, key, subEntryValue); + 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,10 +166,12 @@ public class TweedEntryReaderWriterImpls { String key = e.getKey(); ConfigEntry subEntry = e.getValue(); - writer.visitMapEntryKey(key); - TweedEntryWriter> subEntryWriterChain = ReadWriteExtensionImpl.getWriterChain(subEntry); - subEntryWriterChain.write(writer, entry.get(value, key), subEntry, context); + + if (subEntryWriterChain != null) { + writer.visitMapEntryKey(key); + subEntryWriterChain.write(writer, entry.get(value, key), subEntry, context); + } } writer.visitMapEnd(); diff --git a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/InheritanceMap.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/InheritanceMap.java new file mode 100644 index 0000000..545867f --- /dev/null +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/InheritanceMap.java @@ -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 { + private static final InheritanceMap EMPTY = unmodifiable(new InheritanceMap<>(Object.class)); + + private final Class baseClass; + private final Map>> instanceToClasses; + private final Map, Collection> classToInstances; + + public static InheritanceMap empty() { + return (InheritanceMap) EMPTY; + } + public static InheritanceMap unmodifiable(InheritanceMap map) { + return new Unmodifiable<>(map); + } + + public InheritanceMap(Class baseClass) { + this(baseClass, new IdentityHashMap<>(), new HashMap<>()); + } + + public int size() { + return instanceToClasses.size(); + } + + public boolean isEmpty() { + return instanceToClasses.isEmpty(); + } + + public boolean containsAnyInstanceForClass(Class clazz) { + return !classToInstances.getOrDefault(clazz, Collections.emptyList()).isEmpty(); + } + + public boolean containsSingleInstanceForClass(Class clazz) { + return classToInstances.getOrDefault(clazz, Collections.emptyList()).size() == 1; + } + + public boolean containsInstance(T instance) { + return instanceToClasses.containsKey(instance); + } + + public Collection getAllInstances(Class clazz) { + return (Collection) classToInstances.getOrDefault(clazz, Collections.emptyList()); + } + + public V getSingleInstance(Class clazz) throws NonUniqueResultException { + Collection 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 existingInstances = classToInstances.getOrDefault(instance.getClass(), Collections.emptyList()); + if (existingInstances.isEmpty()) { + putInternal(instance); + return true; + } else { + return false; + } + } + + public V removeInstance(V instance) { + if (!instanceToClasses.containsKey(instance)) { + return null; + } + Collection> classes = instanceToClasses.getOrDefault(instance, Collections.emptyList()); + for (Class implemented : classes) { + classToInstances.getOrDefault(implemented, Collections.emptyList()).remove(instance); + } + instanceToClasses.remove(instance); + return instance; + } + + public void clear() { + instanceToClasses.clear(); + classToInstances.clear(); + } + + public Set values() { + return instanceToClasses.keySet(); + } + + private void putInternal(T instance) { + Collection> classes = findClasses((Class) instance.getClass()); + + instanceToClasses.put(instance, classes); + for (Class implementedClass : classes) { + classToInstances.computeIfAbsent(implementedClass, c -> new ArrayList<>()).add(instance); + } + } + + private Collection> findClasses(Class clazz) { + List> classes = new ArrayList<>(); + + Class superClass = clazz; + while (superClass != Object.class && superClass != baseClass && baseClass.isAssignableFrom(superClass)) { + classes.add((Class) superClass); + + if (baseClass == Object.class || baseClass.isInterface()) { + classes.addAll(findOnlyInterfaces((Class) superClass)); + } + + superClass = superClass.getSuperclass(); + } + + return classes; + } + + private Collection> findOnlyInterfaces(Class clazz) { + List> classes = new ArrayList<>(); + + for (Class implemented : clazz.getInterfaces()) { + if (baseClass != implemented && baseClass.isAssignableFrom(implemented)) { + classes.add((Class) implemented); + classes.addAll(findOnlyInterfaces((Class) implemented)); + } + } + + return classes; + } + + public static class NonUniqueResultException extends Exception { + public NonUniqueResultException(String message) { + super(message); + } + } + + private static class Unmodifiable extends InheritanceMap { + public Unmodifiable(InheritanceMap 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 removeInstance(V instance) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/InheritanceMapTest.java b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/InheritanceMapTest.java new file mode 100644 index 0000000..182486a --- /dev/null +++ b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/InheritanceMapTest.java @@ -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 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); + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo-serde-extension/build.gradle.kts b/tweed5-weaver-pojo-serde-extension/build.gradle.kts new file mode 100644 index 0000000..2835b69 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/build.gradle.kts @@ -0,0 +1,6 @@ +dependencies { + api(project(":tweed5-weaver-pojo")) + api(project(":tweed5-serde-extension")) + + testImplementation(project(":tweed5-serde-hjson")) +} \ No newline at end of file diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/EntryReadWriteConfig.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/EntryReadWriteConfig.java new file mode 100644 index 0000000..2253bbd --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/EntryReadWriteConfig.java @@ -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 = [ "(" ( "," )* ")" ] } + */ +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EntryReadWriteConfig { + String value() default ""; + String writer() default ""; + String reader() default ""; +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java new file mode 100644 index 0000000..356fff7 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoPostProcessor.java @@ -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>> readerFactories = new HashMap<>(); + private final Map>> writerFactories = new HashMap<>(); + + public ReadWritePojoPostProcessor() { + loadProviders(); + } + + private void loadProviders() { + ServiceLoader serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class); + + for (TweedReaderWriterProvider readerWriterProvider : serviceLoader) { + TweedReaderWriterProvider.ProviderContext providerContext = new TweedReaderWriterProvider.ProviderContext() { + @Override + public void registerReaderFactory( + String id, + TweedReaderWriterProvider.ReaderWriterFactory> 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> 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>)(Object) TweedEntryReader.class, readerFactories, readerSpec, context); + //noinspection unchecked + TweedEntryWriter writer = writerSpec == null + ? TweedEntryReaderWriterImpls.NOOP_READER_WRITER + : resolveReaderWriterFromSpec((Class>)(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 resolveReaderWriterFromSpec( + Class baseClass, + Map> 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 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 loadClassIfExists(Class 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; + } + } +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpec.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpec.java new file mode 100644 index 0000000..a5fc31d --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpec.java @@ -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 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 parseSpecList(Lexer lexer) throws ParseException { + List 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(); + } + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/WeaverPojoSerdeExtensionTest.java b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/WeaverPojoSerdeExtensionTest.java new file mode 100644 index 0000000..47861ba --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/WeaverPojoSerdeExtensionTest.java @@ -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 weaverBootstrapper = TweedPojoWeaverBootstrapper.create(AnnotatedConfig.class); + + ConfigContainer 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>() { + @Override + public void write( + TweedDataVisitor writer, + Object value, + ConfigEntry 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; + } +} diff --git a/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpecTest.java b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpecTest.java new file mode 100644 index 0000000..5a2598a --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/SerdePojoReaderWriterSpecTest.java @@ -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)) + ); + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo/build.gradle.kts b/tweed5-weaver-pojo/build.gradle.kts index 3b417e3..667bd31 100644 --- a/tweed5-weaver-pojo/build.gradle.kts +++ b/tweed5-weaver-pojo/build.gradle.kts @@ -1,6 +1,4 @@ dependencies { api(project(":tweed5-core")) api(project(":tweed5-naming-format")) - compileOnly(project(":tweed5-default-extensions")) - compileOnly(project(":tweed5-serde-extension")) } \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java index eaa25c3..9379d29 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java @@ -11,7 +11,7 @@ import java.lang.annotation.Target; * Marks this class as a class that should be woven as a {@link de.siphalor.tweed5.core.api.entry.CompoundConfigEntry}. */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.FIELD}) public @interface CompoundWeaving { /** * The naming format to use for this POJO. diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/PojoWeaving.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/PojoWeaving.java index 4033e1c..1c7c1bc 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/PojoWeaving.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/PojoWeaving.java @@ -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[] postProcessors() default {}; + Class[] extensions() default {}; } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java new file mode 100644 index 0000000..1e57f3c --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/Annotations.java @@ -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 ELEMENT_TYPE_ORDER = Arrays.asList( + ElementType.TYPE_USE, + ElementType.FIELD, + ElementType.CONSTRUCTOR, + ElementType.METHOD, + ElementType.LOCAL_VARIABLE, + ElementType.TYPE_PARAMETER, + ElementType.TYPE, + ElementType.ANNOTATION_TYPE, + ElementType.PACKAGE + ); + private final Map elements = new EnumMap<>(ElementType.class); + + public void addAnnotationsFrom(ElementType elementType, AnnotatedElement element) { + elements.put(elementType, element); + } + + @Nullable + public T getAnnotation(Class annotationClass) { + for (ElementType elementType : ELEMENT_TYPE_ORDER) { + AnnotatedElement annotatedElement = elements.get(elementType); + if (annotatedElement != null) { + T annotation = annotatedElement.getAnnotation(annotationClass); + if (annotation != null) { + return annotation; + } + } + } + return null; + } + + @Nullable + public T getAnnotation(ElementType elementType, Class annotationType) { + AnnotatedElement annotatedElement = elements.get(elementType); + if (annotatedElement == null) { + return null; + } + return annotatedElement.getAnnotation(annotationType); + } + + @NotNull + public T[] getAnnotationHierarchy(Class annotationClass) { + List hierarchy = new ArrayList<>(elements.size()); + for (ElementType elementType : ELEMENT_TYPE_ORDER) { + AnnotatedElement element = elements.get(elementType); + if (element != null) { + T annotation = element.getAnnotation(annotationClass); + if (annotation != null) { + hierarchy.add(annotation); + } + } + } + //noinspection unchecked + return hierarchy.toArray((T[]) Array.newInstance(annotationClass, hierarchy.size())); + } + + @NotNull + public T[] getAnnotations(Class annotationClass) { + for (ElementType elementType : ELEMENT_TYPE_ORDER) { + AnnotatedElement annotatedElement = elements.get(elementType); + if (annotatedElement != null) { + T[] annotations = annotatedElement.getAnnotationsByType(annotationClass); + if (annotations.length != 0) { + return annotations; + } + } + } + //noinspection unchecked + return (T[]) Array.newInstance(annotationClass, 0); + } + + @NotNull + public T[] getAnnotations(ElementType elementType, Class annotationType) { + AnnotatedElement annotatedElement = elements.get(elementType); + if (annotatedElement == null) { + //noinspection unchecked + return (T[]) Array.newInstance(annotationType, 0); + } + return annotatedElement.getAnnotationsByType(annotationType); + } + + @NotNull + public T[][] getAnnotationsHierachy(Class annotationClass) { + List hierarchy = new ArrayList<>(ELEMENT_TYPE_ORDER.size()); + for (ElementType elementType : ELEMENT_TYPE_ORDER) { + AnnotatedElement annotatedElement = elements.get(elementType); + if (annotatedElement != null) { + T[] annotations = annotatedElement.getAnnotationsByType(annotationClass); + if (annotations.length != 0) { + hierarchy.add(annotations); + } + } + } + //noinspection unchecked + return hierarchy.toArray((T[][]) Array.newInstance(annotationClass, 0, 0)); + } + + @NotNull + public Annotation[] getAllAnnotations() { + Map, Annotation[]> annotations = new HashMap<>(); + for (ElementType elementType : ELEMENT_TYPE_ORDER) { + AnnotatedElement annotatedElement = elements.get(elementType); + if (annotatedElement != null) { + for (Annotation annotation : annotatedElement.getAnnotations()) { + annotations.putIfAbsent(annotation.annotationType(), new Annotation[]{annotation}); + + Repeatable repeatable = annotation.annotationType().getAnnotation(Repeatable.class); + if (repeatable != null) { + annotations.put(repeatable.value(), annotatedElement.getAnnotationsByType(repeatable.value())); + } + } + } + } + + if (annotations.isEmpty()) { + return new Annotation[0]; + } else if (annotations.size() == 1) { + return annotations.values().iterator().next(); + } else { + return annotations.values().stream().flatMap(Arrays::stream).toArray(Annotation[]::new); + } + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java index cf70cb1..db04966 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java @@ -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 ConfigEntry weaveEntry(Class 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 createAdditionalDataFromAnnotations(Annotation[] annotations) { - if (annotations.length == 0) { - return TypedMultimap.empty(); - } - TypedMultimap 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; } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java index 7ad9091..556e696 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java @@ -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 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 additionalData; + private Annotations annotations; public WeavingContext build() { return new WeavingContext( parent, - extensionsData, weavingFunction, + configContainer, path, - additionalData + extensionsData, + annotations ); } } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java new file mode 100644 index 0000000..be2823d --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java @@ -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); +} \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java index 69e84a3..5029fbc 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java @@ -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 extends BaseConfigEntry implements WeavableCompoundConfigEntry { private final MethodHandle noArgsConstructor; - private final Map subEntries = new HashMap<>(); - private final Map> subConfigEntries = new HashMap<>(); + private final Map subEntries = new LinkedHashMap<>(); + private final Map> subConfigEntries = new LinkedHashMap<>(); public StaticPojoCompoundConfigEntry(@NotNull Class valueClass, @NotNull MethodHandle noArgsConstructor) { super(valueClass); @@ -67,7 +67,7 @@ public class StaticPojoCompoundConfigEntry extends BaseConfigEntry 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); } diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoClassIntrospector.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoClassIntrospector.java index c7df317..fc31ab2 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoClassIntrospector.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoClassIntrospector.java @@ -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 properties() { if (this.properties == null) { - this.properties = new HashMap<>(); + this.properties = new LinkedHashMap<>(); Class currentClass = clazz; while (currentClass != null) { appendClassProperties(currentClass); diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java index fad0de5..d8b5e24 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java @@ -8,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 { private final Class pojoClass; private final ConfigContainer configContainer; private final Collection weavers; + private final Collection postProcessors; private PatchworkClass contextExtensionsDataClass; public static TweedPojoWeaverBootstrapper create(Class pojoClass) { @@ -37,11 +46,14 @@ public class TweedPojoWeaverBootstrapper { //noinspection unchecked ConfigContainer configContainer = (ConfigContainer) createConfigContainer((Class>) rootWeavingConfig.container()); + Collection weavers = loadWeavers(Arrays.asList(rootWeavingConfig.weavers())); + Collection postProcessors = loadPostProcessors(Arrays.asList(rootWeavingConfig.postProcessors())); + Collection 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 loadExtensions(Collection> extensionClasses) { @@ -53,11 +65,15 @@ public class TweedPojoWeaverBootstrapper { } private static Collection loadWeavers(Collection> weaverClasses) { - List weavers = new ArrayList<>(); - for (Class 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 loadPostProcessors(Collection> postProcessorClasses) { + return postProcessorClasses.stream() + .map(postProcessorClass -> checkImplementsAndInstantiate(TweedPojoWeavingPostProcessor.class, postProcessorClass)) + .collect(Collectors.toList()); } private static ConfigContainer createConfigContainer(Class> containerClass) { @@ -197,9 +213,13 @@ public class TweedPojoWeaverBootstrapper { 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 { for (TweedPojoWeaver weaver : weavers) { ConfigEntry 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 { 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 implements RegisteredExtensionData { private MethodHandle setter; diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java index bb72096..0513255 100644 --- a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java @@ -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 ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { ConfigEntry entry = compoundWeaver.weaveEntry(valueClass, context); @@ -45,7 +49,9 @@ class CompoundPojoWeaverTest { return configEntry; } } - }) + }, mock(ConfigContainer.class)) + .extensionsData(new ExtensionsDataMock(null)) + .annotations(annotations) .build(); ConfigEntry resultEntry = compoundWeaver.weaveEntry(Compound.class, weavingContext);