From 002f59ebd0123923a40ad2ac9ec4cb85614e00c7 Mon Sep 17 00:00:00 2001 From: Siphalor Date: Sun, 20 Oct 2024 21:30:00 +0200 Subject: [PATCH] [weaver-pojo] Implement first prototype of POJO weaving --- build.gradle.kts | 12 +- gradle.properties | 10 +- lombok.config | 3 +- settings.gradle.kts | 1 + .../core/api/collection/TypedMultimap.java | 224 +++++++++++++++++ .../core/api/container/ConfigContainer.java | 12 + .../container/ConfigContainerSetupPhase.java | 1 - .../entry/BaseConfigEntry.java} | 5 +- .../core/impl/DefaultConfigContainer.java | 10 +- .../CoherentCollectionConfigEntryImpl.java | 7 +- .../ReflectiveCompoundConfigEntryImpl.java | 7 +- .../impl/entry/SimpleConfigEntryImpl.java | 3 +- .../StaticMapCompoundConfigEntryImpl.java | 7 +- .../tweed5/namingformat/api/NamingFormat.java | 7 + .../api/NamingFormatCollector.java | 29 +++ .../api/NamingFormatProvider.java | 9 + .../impl/DefaultNamingFormatProvider.java | 22 ++ .../api/TweedReaderWriterProvider.java | 48 ++++ ...ltTweedEntryReaderWriterImplsProvider.java | 30 +++ tweed5-weaver-pojo/build.gradle.kts | 6 + .../pojo/api/annotation/CompoundWeaving.java | 24 ++ .../pojo/api/annotation/PojoWeaving.java | 26 ++ .../entry/WeavableCompoundConfigEntry.java | 49 ++++ .../pojo/api/weaving/CompoundPojoWeaver.java | 199 +++++++++++++++ .../pojo/api/weaving/TrivialPojoWeaver.java | 17 ++ .../pojo/api/weaving/TweedPojoWeaver.java | 14 ++ .../api/weaving/TweedPojoWeavingFunction.java | 29 +++ .../pojo/api/weaving/WeavingContext.java | 67 +++++ .../entry/StaticPojoCompoundConfigEntry.java | 119 +++++++++ .../tweed5/weaver/pojo/impl/package-info.java | 5 + .../impl/weaving/PojoClassIntrospector.java | 231 +++++++++++++++++ .../impl/weaving/PojoWeavingException.java | 11 + .../weaving/TweedPojoWeaverBootstrapper.java | 234 ++++++++++++++++++ .../compound/CompoundWeavingConfig.java | 15 ++ .../compound/CompoundWeavingConfigImpl.java | 27 ++ .../weaver/pojo/api/TypedMultimapTest.java | 197 +++++++++++++++ .../api/weaving/CompoundPojoWeaverTest.java | 122 +++++++++ .../pojo/impl/PojoClassIntrospectorTest.java | 233 +++++++++++++++++ .../TweedPojoWeaverBootstrapperTest.java | 71 ++++++ .../pojo/test/ConfigEntryAssertions.java | 32 +++ 40 files changed, 2144 insertions(+), 31 deletions(-) create mode 100644 tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java rename tweed5-core/src/main/java/de/siphalor/tweed5/core/{impl/entry/BaseConfigEntryImpl.java => api/entry/BaseConfigEntry.java} (81%) create mode 100644 tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatCollector.java create mode 100644 tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatProvider.java create mode 100644 tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/impl/DefaultNamingFormatProvider.java create mode 100644 tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/TweedReaderWriterProvider.java create mode 100644 tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/DefaultTweedEntryReaderWriterImplsProvider.java create mode 100644 tweed5-weaver-pojo/build.gradle.kts create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/PojoWeaving.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/package-info.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoClassIntrospector.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoWeavingException.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfig.java create mode 100644 tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfigImpl.java create mode 100644 tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java create mode 100644 tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java create mode 100644 tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/PojoClassIntrospectorTest.java create mode 100644 tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java create mode 100644 tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java diff --git a/build.gradle.kts b/build.gradle.kts index 9509b4c..ebee5e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,13 +25,19 @@ allprojects { testCompileOnly(lombok) testAnnotationProcessor(lombok) - compileOnly("com.google.auto.service:auto-service-annotations:${properties["auto_service.version"]}") - annotationProcessor("com.google.auto.service:auto-service:${properties["auto_service.version"]}") + val autoServiceAnnotations = "com.google.auto.service:auto-service-annotations:${properties["auto_service.version"]}" + val autoService = "com.google.auto.service:auto-service:${properties["auto_service.version"]}" + compileOnly(autoServiceAnnotations) + annotationProcessor(autoService) + testCompileOnly(autoServiceAnnotations) + testAnnotationProcessor(autoService) implementation("org.jetbrains:annotations:${properties["jetbrains_annotations.version"]}") - testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation(platform("org.junit:junit-bom:${properties["junit.version"]}")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core:${properties["mockito.version"]}") + testImplementation("org.assertj:assertj-core:${properties["assertj.version"]}") } tasks.test { diff --git a/gradle.properties b/gradle.properties index 553d356..c9c1735 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,8 @@ asm.version = 9.7 -jetbrains_annotations.version = 24.1.0 -lombok.version = 1.18.32 -auto_service.version = 1.1.1 \ No newline at end of file +jetbrains_annotations.version = 26.0.1 +lombok.version = 1.18.34 +auto_service.version = 1.1.1 + +junit.version = 5.11.2 +mockito.version = 5.14.2 +assertj.version = 3.26.3 \ No newline at end of file diff --git a/lombok.config b/lombok.config index 18f74be..f778720 100644 --- a/lombok.config +++ b/lombok.config @@ -1 +1,2 @@ -lombok.accessors.fluent = true \ No newline at end of file +lombok.accessors.fluent = true +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e9df496..d89b388 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,3 +7,4 @@ include("tweed5-patchwork") include("tweed5-serde-api") include("tweed5-serde-extension") include("tweed5-serde-hjson") +include("tweed5-weaver-pojo") diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java new file mode 100644 index 0000000..c6a4fdb --- /dev/null +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java @@ -0,0 +1,224 @@ +package de.siphalor.tweed5.core.api.collection; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@SuppressWarnings("unchecked") +@RequiredArgsConstructor +public class TypedMultimap implements Collection { + private static final TypedMultimap EMPTY = unmodifiable(new TypedMultimap<>(Collections.emptyMap(), ArrayList::new)); + + protected final Map, Collection> delegate; + protected final Supplier> collectionSupplier; + + public static TypedMultimap unmodifiable(TypedMultimap map) { + return new Unmodifiable<>(map.delegate, map.collectionSupplier); + } + + public static TypedMultimap empty() { + return (TypedMultimap) EMPTY; + } + + public int size() { + return (int) delegate.values().stream().mapToLong(Collection::size).sum(); + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(@NotNull Object o) { + return delegate.getOrDefault(o.getClass(), Collections.emptyList()).contains(o); + } + + public Set> classes() { + return delegate.keySet(); + } + + @Override + public @NotNull Iterator iterator() { + return new Iterator() { + private final Iterator, Collection>> classIterator = delegate.entrySet().iterator(); + private Iterator listIterator; + private boolean keptElement; + private boolean keptAnyElementInList; + + @Override + public boolean hasNext() { + return classIterator.hasNext() || (listIterator != null && listIterator.hasNext()); + } + + @Override + public T next() { + if (keptElement) { + keptAnyElementInList = true; + } + if (listIterator == null || !listIterator.hasNext()) { + if (!keptAnyElementInList) { + classIterator.remove(); + } + listIterator = classIterator.next().getValue().iterator(); + keptAnyElementInList = false; + } + keptElement = true; + return listIterator.next(); + } + + @Override + public void remove() { + if (listIterator == null) { + throw new IllegalStateException("Iterator has not been called"); + } + keptElement = false; + listIterator.remove(); + } + }; + } + + @Override + @NotNull + public Object @NotNull [] toArray() { + return delegate.values().stream().flatMap(Collection::stream).toArray(); + } + + @Override + @NotNull + public S @NotNull [] toArray(@NotNull S @NotNull [] array) { + Class clazz = array.getClass().getComponentType(); + return delegate.values().stream() + .flatMap(Collection::stream) + .toArray(size -> (S[]) Array.newInstance(clazz, size)); + } + + @Override + public boolean add(@NotNull T value) { + return delegate.computeIfAbsent(((Class) value.getClass()), clazz -> collectionSupplier.get()).add(value); + } + + @Override + public boolean remove(@NotNull Object value) { + Collection values = delegate.get(value.getClass()); + if (values == null) { + return false; + } + if (values.remove(value)) { + if (values.isEmpty()) { + delegate.remove(value.getClass()); + } + return true; + } + return false; + } + + @NotNull + public Collection getAll(Class clazz) { + return (Collection) Collections.unmodifiableCollection(delegate.getOrDefault(clazz, Collections.emptyList())); + } + + @NotNull + public Collection removeAll(Class clazz) { + Collection removed = delegate.remove(clazz); + return removed == null ? Collections.emptyList() : Collections.unmodifiableCollection(removed); + } + + @Override + public boolean containsAll(@NotNull Collection values) { + for (Object value : values) { + if (!contains(value)) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(@NotNull Collection values) { + boolean changed = false; + for (T value : values) { + changed = add(value) || changed; + } + return changed; + } + + @Override + public boolean removeAll(@NotNull Collection values) { + boolean changed = false; + for (Object value : values) { + changed = remove(value) || changed; + } + return changed; + } + + @Override + public boolean retainAll(@NotNull Collection values) { + Map, ? extends List> valuesByClass = values.stream() + .collect(Collectors.groupingBy(Object::getClass)); + delegate.putAll((Map, List>) valuesByClass); + delegate.keySet().removeIf(key -> !valuesByClass.containsKey(key)); + return true; + } + + @Override + public void clear() { + delegate.clear(); + } + + protected static class Unmodifiable extends TypedMultimap { + public Unmodifiable( + Map, Collection> delegate, + Supplier> collectionSupplier + ) { + super(delegate, collectionSupplier); + } + + @Override + public @NotNull Iterator iterator() { + return delegate.values().stream().flatMap(Collection::stream).iterator(); + } + + @Override + public boolean add(@NotNull T value) { + throw createUnsupportedOperationException(); + } + + @Override + public boolean remove(@NotNull Object value) { + throw createUnsupportedOperationException(); + } + + @Override + public @NotNull Collection removeAll(Class clazz) { + throw createUnsupportedOperationException(); + } + + @Override + public boolean addAll(@NotNull Collection values) { + throw createUnsupportedOperationException(); + } + + @Override + public boolean removeAll(@NotNull Collection values) { + throw createUnsupportedOperationException(); + } + + @Override + public boolean retainAll(@NotNull Collection values) { + throw createUnsupportedOperationException(); + } + + @Override + public void clear() { + throw createUnsupportedOperationException(); + } + + protected UnsupportedOperationException createUnsupportedOperationException() { + return new UnsupportedOperationException("Map is unmodifiable"); + } + } +} diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java index 9c34b4d..eb2eb00 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java @@ -8,12 +8,24 @@ import de.siphalor.tweed5.core.api.extension.TweedExtension; import java.util.Collection; import java.util.Map; +/** + * The main wrapper for a config tree.
+ * Holds certain global metadata like registered extensions and manages the initialization phases. + * @param The class that the config tree represents + * @see ConfigContainerSetupPhase + */ public interface ConfigContainer { ConfigContainerSetupPhase setupPhase(); default boolean isReady() { return setupPhase() == ConfigContainerSetupPhase.READY; } + default void registerExtensions(TweedExtension... extensions) { + for (TweedExtension extension : extensions) { + registerExtension(extension); + } + } + void registerExtension(TweedExtension extension); void finishExtensionSetup(); diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainerSetupPhase.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainerSetupPhase.java index f784395..c9ca07f 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainerSetupPhase.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainerSetupPhase.java @@ -3,7 +3,6 @@ package de.siphalor.tweed5.core.api.container; public enum ConfigContainerSetupPhase { EXTENSIONS_SETUP, TREE_SETUP, - SEALING_TREE, TREE_SEALED, READY, } diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/BaseConfigEntryImpl.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/entry/BaseConfigEntry.java similarity index 81% rename from tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/BaseConfigEntryImpl.java rename to tweed5-core/src/main/java/de/siphalor/tweed5/core/api/entry/BaseConfigEntry.java index 7cd8c14..2bfb493 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/BaseConfigEntryImpl.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/entry/BaseConfigEntry.java @@ -1,7 +1,6 @@ -package de.siphalor.tweed5.core.impl.entry; +package de.siphalor.tweed5.core.api.entry; import de.siphalor.tweed5.core.api.container.ConfigContainer; -import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -9,7 +8,7 @@ import org.jetbrains.annotations.NotNull; @RequiredArgsConstructor @Getter -abstract class BaseConfigEntryImpl implements ConfigEntry { +public abstract class BaseConfigEntry implements ConfigEntry { @NotNull private final Class valueClass; 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 1b95795..69bb323 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 @@ -110,16 +110,18 @@ public class DefaultConfigContainer implements ConfigContainer { } private void finishEntrySetup() { - setupPhase = ConfigContainerSetupPhase.SEALING_TREE; - - rootEntry.visitInOrder(entry -> entry.seal(DefaultConfigContainer.this)); + rootEntry.visitInOrder(entry -> { + if (!entry.sealed()) { + entry.seal(DefaultConfigContainer.this); + } + }); setupPhase = ConfigContainerSetupPhase.TREE_SEALED; } @Override public EntryExtensionsData createExtensionsData() { - requireSetupPhase(ConfigContainerSetupPhase.SEALING_TREE); + requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP); try { return (EntryExtensionsData) entryExtensionsDataPatchworkClass.constructor().invoke(); diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/CoherentCollectionConfigEntryImpl.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/CoherentCollectionConfigEntryImpl.java index ff08501..479d583 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/CoherentCollectionConfigEntryImpl.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/CoherentCollectionConfigEntryImpl.java @@ -1,15 +1,12 @@ package de.siphalor.tweed5.core.impl.entry; -import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry; -import de.siphalor.tweed5.core.api.entry.ConfigEntry; -import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; -import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; +import de.siphalor.tweed5.core.api.entry.*; import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.function.IntFunction; -public class CoherentCollectionConfigEntryImpl> extends BaseConfigEntryImpl implements CoherentCollectionConfigEntry { +public class CoherentCollectionConfigEntryImpl> extends BaseConfigEntry implements CoherentCollectionConfigEntry { private final IntFunction collectionConstructor; private ConfigEntry elementEntry; diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java index d323cd4..68a4b24 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java @@ -1,9 +1,6 @@ package de.siphalor.tweed5.core.impl.entry; -import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; -import de.siphalor.tweed5.core.api.entry.ConfigEntry; -import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; -import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; +import de.siphalor.tweed5.core.api.entry.*; import lombok.Getter; import lombok.Value; import org.jetbrains.annotations.NotNull; @@ -16,7 +13,7 @@ import java.util.Map; import java.util.stream.Collectors; @Getter -public class ReflectiveCompoundConfigEntryImpl extends BaseConfigEntryImpl implements CompoundConfigEntry { +public class ReflectiveCompoundConfigEntryImpl extends BaseConfigEntry implements CompoundConfigEntry { private final Constructor noArgsConstructor; private final Map compoundEntries; diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/SimpleConfigEntryImpl.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/SimpleConfigEntryImpl.java index 6262cc3..ba1d5fe 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/SimpleConfigEntryImpl.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/SimpleConfigEntryImpl.java @@ -1,11 +1,12 @@ package de.siphalor.tweed5.core.impl.entry; +import de.siphalor.tweed5.core.api.entry.BaseConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry; import org.jetbrains.annotations.NotNull; -public class SimpleConfigEntryImpl extends BaseConfigEntryImpl implements SimpleConfigEntry { +public class SimpleConfigEntryImpl extends BaseConfigEntry implements SimpleConfigEntry { public SimpleConfigEntryImpl(Class valueClass) { super(valueClass); } diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/StaticMapCompoundConfigEntryImpl.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/StaticMapCompoundConfigEntryImpl.java index 6484579..2af283f 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/StaticMapCompoundConfigEntryImpl.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/StaticMapCompoundConfigEntryImpl.java @@ -1,16 +1,13 @@ package de.siphalor.tweed5.core.impl.entry; -import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; -import de.siphalor.tweed5.core.api.entry.ConfigEntry; -import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; -import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; +import de.siphalor.tweed5.core.api.entry.*; import org.jetbrains.annotations.NotNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.IntFunction; -public class StaticMapCompoundConfigEntryImpl> extends BaseConfigEntryImpl implements CompoundConfigEntry { +public class StaticMapCompoundConfigEntryImpl> extends BaseConfigEntry implements CompoundConfigEntry { private final IntFunction mapConstructor; private final Map> compoundEntries = new LinkedHashMap<>(); diff --git a/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormat.java b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormat.java index 9579c66..4623946 100644 --- a/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormat.java +++ b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormat.java @@ -5,4 +5,11 @@ public interface NamingFormat { String joinToName(String[] words); String name(); + + static String convert(String name, NamingFormat from, NamingFormat to) { + if (from == to) { + return name; + } + return to.joinToName(from.splitIntoWords(name)); + } } diff --git a/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatCollector.java b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatCollector.java new file mode 100644 index 0000000..a258f7b --- /dev/null +++ b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatCollector.java @@ -0,0 +1,29 @@ +package de.siphalor.tweed5.namingformat.api; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +public class NamingFormatCollector { + private final Map namingFormats = new HashMap<>(); + + public void setupFormats() { + ServiceLoader serviceLoader = ServiceLoader.load(NamingFormatProvider.class); + Context context = new Context(); + for (NamingFormatProvider provider : serviceLoader) { + provider.provideNamingFormats(context); + } + } + + public Map namingFormats() { + return Collections.unmodifiableMap(namingFormats); + } + + private class Context implements NamingFormatProvider.ProvidingContext { + @Override + public void registerNamingFormat(String id, NamingFormat namingFormat) { + namingFormats.put(id, namingFormat); + } + } +} diff --git a/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatProvider.java b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatProvider.java new file mode 100644 index 0000000..df24b1a --- /dev/null +++ b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/api/NamingFormatProvider.java @@ -0,0 +1,9 @@ +package de.siphalor.tweed5.namingformat.api; + +public interface NamingFormatProvider { + void provideNamingFormats(ProvidingContext context); + + interface ProvidingContext { + void registerNamingFormat(String id, NamingFormat namingFormat); + } +} diff --git a/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/impl/DefaultNamingFormatProvider.java b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/impl/DefaultNamingFormatProvider.java new file mode 100644 index 0000000..1e80927 --- /dev/null +++ b/tweed5-naming-format/src/main/java/de/siphalor/tweed5/namingformat/impl/DefaultNamingFormatProvider.java @@ -0,0 +1,22 @@ +package de.siphalor.tweed5.namingformat.impl; + +import com.google.auto.service.AutoService; +import de.siphalor.tweed5.namingformat.api.NamingFormatProvider; + +import static de.siphalor.tweed5.namingformat.api.NamingFormats.*; + +@AutoService(NamingFormatProvider.class) +public class DefaultNamingFormatProvider implements NamingFormatProvider { + @Override + public void provideNamingFormats(ProvidingContext context) { + context.registerNamingFormat("camel_case", camelCase()); + context.registerNamingFormat("pascal_case", pascalCase()); + context.registerNamingFormat("kebab_case", kebabCase()); + context.registerNamingFormat("upper_kebab_case", upperKebabCase()); + context.registerNamingFormat("snake_case", snakeCase()); + context.registerNamingFormat("upper_snake_case", upperKebabCase()); + context.registerNamingFormat("space_case", spaceCase()); + context.registerNamingFormat("upper_space_case", upperSpaceCase()); + context.registerNamingFormat("title_case", titleCase()); + } +} 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 new file mode 100644 index 0000000..ada4da4 --- /dev/null +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/TweedReaderWriterProvider.java @@ -0,0 +1,48 @@ +package de.siphalor.tweed5.data.extension.api; + +import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter; +import lombok.RequiredArgsConstructor; + +/** + * An interface that allows to register {@link TweedEntryReader}s and {@link TweedEntryWriter}s. + * Implementing classes should be Java services, e.g. using {@link com.google.auto.service.AutoService}. + */ +public interface TweedReaderWriterProvider { + void provideReaderWriters(ProviderContext context); + + /** + * 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") + */ + 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); + } + } + + /** + * A factory that creates a new reader or writer using delegate readers/writers as its arguments. + * @param + */ + @FunctionalInterface + interface ReaderWriterFactory { + T create(T... delegateReaderWriters); + } + + @RequiredArgsConstructor + final class StaticReaderWriterFactory implements ReaderWriterFactory { + private final T readerWriter; + + @SafeVarargs + @Override + public final T create(T... delegateReaderWriters) { + if (delegateReaderWriters.length != 0) { + throw new IllegalArgumentException("Reader writer factory must not be passed any delegates as arguments"); + } + return readerWriter; + } + } +} 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 new file mode 100644 index 0000000..763a294 --- /dev/null +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/DefaultTweedEntryReaderWriterImplsProvider.java @@ -0,0 +1,30 @@ +package de.siphalor.tweed5.data.extension.impl; + +import com.google.auto.service.AutoService; +import de.siphalor.tweed5.data.extension.api.TweedReaderWriterProvider; + +import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*; + +@AutoService(TweedReaderWriterProvider.class) +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())); + + context.registerReaderWriterFactory("nullable", delegateReaderWriters -> { + if (delegateReaderWriters.length != 1) { + throw new IllegalArgumentException("Nullable reader writer requires a single delegate argument, got " + delegateReaderWriters.length); + } + return nullableReaderWriter(delegateReaderWriters[0]); + }); + } +} diff --git a/tweed5-weaver-pojo/build.gradle.kts b/tweed5-weaver-pojo/build.gradle.kts new file mode 100644 index 0000000..3b417e3 --- /dev/null +++ b/tweed5-weaver-pojo/build.gradle.kts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..eaa25c3 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/CompoundWeaving.java @@ -0,0 +1,24 @@ +package de.siphalor.tweed5.weaver.pojo.api.annotation; + +import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +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) +public @interface CompoundWeaving { + /** + * The naming format to use for this POJO. + * Use {@link de.siphalor.tweed5.namingformat.api.NamingFormatProvider} to define naming formats. + * @see de.siphalor.tweed5.namingformat.impl.DefaultNamingFormatProvider + */ + String namingFormat() default ""; + + Class entryClass() default WeavableCompoundConfigEntry.class; +} 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 new file mode 100644 index 0000000..4033e1c --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/PojoWeaving.java @@ -0,0 +1,26 @@ +package de.siphalor.tweed5.weaver.pojo.api.annotation; + +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.extension.TweedExtension; +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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PojoWeaving { + Class container() default DefaultConfigContainer.class; + + Class[] weavers() default { + CompoundPojoWeaver.class, + TrivialPojoWeaver.class, + }; + + Class[] extensions() default {}; +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java new file mode 100644 index 0000000..0244d0a --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java @@ -0,0 +1,49 @@ +package de.siphalor.tweed5.weaver.pojo.api.entry; + +import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.jetbrains.annotations.NotNull; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * {@inheritDoc} + *
+ * A constructor taking the value {@link Class} and a {@link MethodHandle} that allows to instantiate the value Class with no arguments. + */ +public interface WeavableCompoundConfigEntry extends CompoundConfigEntry { + static > C instantiate( + Class weavableClass, Class valueClass, MethodHandle constructorHandle + ) throws PojoWeavingException { + try { + Constructor weavableEntryConstructor = weavableClass.getConstructor(Class.class, MethodHandle.class); + return weavableEntryConstructor.newInstance(valueClass, constructorHandle); + } catch (NoSuchMethodException e) { + throw new PojoWeavingException( + "Class " + weavableClass.getName() + " must have constructor with value class and value constructor", + e + ); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new PojoWeavingException( + "Failed to instantiate class for weavable compound entry " + weavableClass.getName(), + e + ); + } + } + + void registerSubEntry(SubEntry subEntry); + + @Value + @RequiredArgsConstructor + class SubEntry { + @NotNull String name; + @NotNull ConfigEntry configEntry; + @NotNull MethodHandle getter; + @NotNull MethodHandle setter; + } +} 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 new file mode 100644 index 0000000..cf70cb1 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java @@ -0,0 +1,199 @@ +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; +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.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.invoke.MethodHandle; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +/** + * A weaver that weaves classes with the {@link CompoundWeaving} annotation as compound entries. + */ +public class CompoundPojoWeaver implements TweedPojoWeaver { + private static final CompoundWeavingConfig DEFAULT_WEAVING_CONFIG = CompoundWeavingConfigImpl.builder() + .compoundSourceNamingFormat(NamingFormats.camelCase()) + .compoundTargetNamingFormat(NamingFormats.camelCase()) + .compoundEntryClass(StaticPojoCompoundConfigEntry.class) + .build(); + + private final NamingFormatCollector namingFormatCollector = new NamingFormatCollector(); + private RegisteredExtensionData weavingConfigAccess; + + public void setup(SetupContext context) { + namingFormatCollector.setupFormats(); + + this.weavingConfigAccess = context.registerWeavingContextExtensionData(CompoundWeavingConfig.class); + } + + @Override + public @Nullable ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { + if (!valueClass.isAnnotationPresent(CompoundWeaving.class)) { + return null; + } + try { + CompoundWeavingConfig weavingConfig = getOrCreateWeavingConfig(valueClass, context); + WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy(); + weavingConfigAccess.set(newExtensionsData, weavingConfig); + + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueClass); + + WeavableCompoundConfigEntry compoundEntry = instantiateCompoundEntry(introspector, weavingConfig); + + Map properties = introspector.properties(); + properties.forEach((name, property) -> { + if (shouldIncludeCompoundPropertyInWeaving(property)) { + compoundEntry.registerSubEntry(weaveCompoundSubEntry(property, newExtensionsData, context)); + } + }); + + return compoundEntry; + } catch (Exception e) { + throw new PojoWeavingException("Exception occurred trying to weave compound for class " + valueClass.getName(), e); + } + } + + private CompoundWeavingConfig getOrCreateWeavingConfig(Class valueClass, WeavingContext context) { + CompoundWeavingConfig parent; + if (context.extensionsData().isPatchworkPartSet(CompoundWeavingConfig.class)) { + parent = (CompoundWeavingConfig) context.extensionsData(); + } else { + parent = DEFAULT_WEAVING_CONFIG; + } + + CompoundWeavingConfig local = getWeavingConfigFromClassAnnotation(valueClass); + if (local == null) { + return parent; + } + + return CompoundWeavingConfigImpl.withOverrides(parent, local); + } + + private WeavingContext createSubContextForProperty( + PojoClassIntrospector.Property property, + String name, + WeavingContext.ExtensionsData newExtensionsData, + WeavingContext parentContext + ) { + return parentContext.subContextBuilder(name) + .additionalData(createAdditionalDataFromAnnotations(property.field().getAnnotations())) + .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); + } + + @Nullable + private CompoundWeavingConfig getWeavingConfigFromClassAnnotation(Class clazz) { + CompoundWeaving annotation = clazz.getAnnotation(CompoundWeaving.class); + if (annotation == null) { + return null; + } + + CompoundWeavingConfigImpl.CompoundWeavingConfigImplBuilder builder = CompoundWeavingConfigImpl.builder(); + builder.compoundSourceNamingFormat(NamingFormats.camelCase()); + if (!annotation.namingFormat().isEmpty()) { + builder.compoundTargetNamingFormat(getNamingFormatById(annotation.namingFormat())); + } + if (annotation.entryClass() != WeavableCompoundConfigEntry.class) { + builder.compoundEntryClass(annotation.entryClass()); + } + + return builder.build(); + } + + + @SuppressWarnings("unchecked") + private WeavableCompoundConfigEntry instantiateCompoundEntry( + PojoClassIntrospector classIntrospector, + CompoundWeavingConfig weavingConfig + ) { + MethodHandle valueConstructor = classIntrospector.noArgsConstructor(); + if (valueConstructor == null) { + throw new PojoWeavingException("Class " + classIntrospector.type().getName() + " must have public no args constructor"); + } + + //noinspection rawtypes + Class annotationEntryClass = weavingConfig.compoundEntryClass(); + @NotNull + Class> weavableEntryClass = (Class>) ( + annotationEntryClass != null + ? annotationEntryClass + : StaticPojoCompoundConfigEntry.class + ); + return WeavableCompoundConfigEntry.instantiate( + weavableEntryClass, + (Class) classIntrospector.type(), + valueConstructor + ); + } + + private boolean shouldIncludeCompoundPropertyInWeaving(PojoClassIntrospector.Property property) { + return property.getter() != null && (property.setter() != null || property.isFinal()); + } + + private @NotNull WeavableCompoundConfigEntry.SubEntry weaveCompoundSubEntry( + PojoClassIntrospector.Property property, + WeavingContext.ExtensionsData newExtensionsData, + WeavingContext parentContext + ) { + String name = convertName(property.field().getName(), (CompoundWeavingConfig) newExtensionsData); + WeavingContext subContext = createSubContextForProperty(property, name, newExtensionsData, parentContext); + + ConfigEntry subEntry; + if (property.isFinal()) { + // TODO + throw new UnsupportedOperationException("Final config entries are not supported in weaving yet."); + } else { + subEntry = subContext.weaveEntry(property.field().getType(), subContext); + } + + return new StaticPojoCompoundConfigEntry.SubEntry( + name, + subEntry, + property.getter(), + property.setter() + ); + } + + private @NotNull String convertName(String name, CompoundWeavingConfig weavingConfig) { + return NamingFormat.convert( + name, + weavingConfig.compoundSourceNamingFormat(), + weavingConfig.compoundTargetNamingFormat() + ); + } + + private @NotNull NamingFormat getNamingFormatById(String id) { + NamingFormat namingFormat = namingFormatCollector.namingFormats().get(id); + if (namingFormat == null) { + throw new PojoWeavingException( + "Naming format \"" + id + "\" is not recognized. Available formats are: " + + namingFormatCollector.namingFormats().keySet() + ); + } + return namingFormat; + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java new file mode 100644 index 0000000..8ef13b7 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TrivialPojoWeaver.java @@ -0,0 +1,17 @@ +package de.siphalor.tweed5.weaver.pojo.api.weaving; + +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; +import org.jetbrains.annotations.Nullable; + +public class TrivialPojoWeaver implements TweedPojoWeaver { + @Override + public void setup(SetupContext context) { + // nothing to set up here + } + + @Override + public @Nullable ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { + return new SimpleConfigEntryImpl<>(valueClass); + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java new file mode 100644 index 0000000..ed1a375 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java @@ -0,0 +1,14 @@ +package de.siphalor.tweed5.weaver.pojo.api.weaving; + +import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; +import org.jetbrains.annotations.ApiStatus; + +public interface TweedPojoWeaver extends TweedPojoWeavingFunction { + + @ApiStatus.OverrideOnly + void setup(SetupContext context); + + interface SetupContext { + RegisteredExtensionData registerWeavingContextExtensionData(Class dataClass); + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java new file mode 100644 index 0000000..b54c1f4 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeavingFunction.java @@ -0,0 +1,29 @@ +package de.siphalor.tweed5.weaver.pojo.api.weaving; + +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@FunctionalInterface +public interface TweedPojoWeavingFunction { + /** + * Weaves a {@link ConfigEntry} for the given value class and context. + * The returned config entry must be sealed. + * @return The resulting, sealed config entry or {@code null}, if the weaving function is not applicable to the given parameters. + */ + @Nullable + ConfigEntry weaveEntry(Class valueClass, WeavingContext context); + + @FunctionalInterface + interface NonNull extends TweedPojoWeavingFunction { + /** + * {@inheritDoc} + *
+ * The function must ensure that the resulting entry is not null, e.g., by trowing a {@link RuntimeException}. + * @return The resulting, sealed config entry. + * @throws RuntimeException when a valid config entry could not be resolved. + */ + @Override + @NotNull ConfigEntry weaveEntry(Class valueClass, WeavingContext context); + } +} 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 new file mode 100644 index 0000000..7ad9091 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/WeavingContext.java @@ -0,0 +1,67 @@ +package de.siphalor.tweed5.weaver.pojo.api.weaving; + +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; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +@Value +public class WeavingContext implements TweedPojoWeavingFunction.NonNull { + @Nullable + WeavingContext parent; + ExtensionsData extensionsData; + @Getter(AccessLevel.NONE) + TweedPojoWeavingFunction.NonNull weavingFunction; + String[] path; + TypedMultimap additionalData; + + public static Builder builder() { + return new Builder(null, new String[0]); + } + + public static Builder builder(String baseName) { + return new Builder(null, 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); + } + + @Override + public @NotNull ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { + return weavingFunction.weaveEntry(valueClass, context); + } + + public interface ExtensionsData extends Patchwork {} + + @Accessors(fluent = true, chain = true) + @Setter + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class Builder { + @Nullable + private final WeavingContext parent; + private final String[] path; + private ExtensionsData extensionsData; + private TweedPojoWeavingFunction.NonNull weavingFunction; + private TypedMultimap additionalData; + + public WeavingContext build() { + return new WeavingContext( + parent, + extensionsData, + weavingFunction, + path, + additionalData + ); + } + } +} 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 new file mode 100644 index 0000000..69e84a3 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java @@ -0,0 +1,119 @@ +package de.siphalor.tweed5.weaver.pojo.impl.entry; + +import de.siphalor.tweed5.core.api.entry.BaseConfigEntry; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; +import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; +import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; +import org.jetbrains.annotations.NotNull; + +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.HashMap; +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<>(); + + public StaticPojoCompoundConfigEntry(@NotNull Class valueClass, @NotNull MethodHandle noArgsConstructor) { + super(valueClass); + this.noArgsConstructor = noArgsConstructor; + } + + public void registerSubEntry(SubEntry subEntry) { + requireUnsealed(); + + subEntries.put(subEntry.name(), subEntry); + subConfigEntries.put(subEntry.name(), subEntry.configEntry()); + } + + @Override + public Map> subEntries() { + return Collections.unmodifiableMap(subConfigEntries); + } + + @Override + public void set(T compoundValue, String key, V value) { + SubEntry subEntry = subEntries.get(key); + if (subEntry == null) { + throw new IllegalArgumentException("Unknown config entry: " + key); + } + + try { + subEntry.setter().invoke(compoundValue, value); + } catch (Throwable e) { + throw new IllegalStateException("Failed to set value for config entry \"" + key + "\"", e); + } + } + + @Override + public V get(T compoundValue, String key) { + SubEntry subEntry = subEntries.get(key); + if (subEntry == null) { + throw new IllegalArgumentException("Unknown config entry: " + key); + } + + try { + //noinspection unchecked + return (V) subEntry.getter().invoke(compoundValue); + } catch (Throwable e) { + throw new IllegalStateException("Failed to get value for config entry \"" + key + "\"", e); + } + } + + @Override + public T instantiateCompoundValue() { + try { + //noinspection unchecked + return (T) noArgsConstructor.invokeExact(); + } catch (Throwable e) { + throw new IllegalStateException("Failed to instantiate compound class", e); + } + } + + @Override + public void visitInOrder(ConfigEntryVisitor visitor) { + if (visitor.enterCompoundEntry(this)) { + subConfigEntries.forEach((key, entry) -> { + if (visitor.enterCompoundSubEntry(key)) { + entry.visitInOrder(visitor); + visitor.leaveCompoundSubEntry(key); + } + }); + visitor.leaveCompoundEntry(this); + } + } + + @Override + public void visitInOrder(ConfigEntryValueVisitor visitor, T value) { + if (visitor.enterCompoundEntry(this, value)) { + subEntries.forEach((key, entry) -> { + if (visitor.enterCompoundSubEntry(key)) { + try { + Object subValue = entry.getter().invokeExact(value); + //noinspection unchecked + visitor.visitEntry((ConfigEntry) entry.configEntry(), subValue); + } catch (Throwable e) { + throw new RuntimeException("Failed to get compound sub entry value \"" + key + "\""); + } + } + }); + } + } + + @Override + public @NotNull T deepCopy(@NotNull T value) { + T copy = instantiateCompoundValue(); + for (SubEntry subEntry : subEntries.values()) { + try { + Object subValue = subEntry.getter().invokeExact(value); + subEntry.setter().invoke(copy, subValue); + } catch (Throwable e) { + throw new RuntimeException("Failed to copy value of sub entry \"" + subEntry.name() + "\"", e); + } + } + return copy; + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/package-info.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/package-info.java new file mode 100644 index 0000000..fdba406 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/package-info.java @@ -0,0 +1,5 @@ +@ApiStatus.Internal + +package de.siphalor.tweed5.weaver.pojo.impl; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file 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 new file mode 100644 index 0000000..1960312 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoClassIntrospector.java @@ -0,0 +1,231 @@ +package de.siphalor.tweed5.weaver.pojo.impl.weaving; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +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.Map; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class PojoClassIntrospector { + private final Class clazz; + private final MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + + private Map properties; + + public static PojoClassIntrospector forClass(Class clazz) { + if ((clazz.getModifiers() & Modifier.PUBLIC) == 0) { + throw new IllegalStateException("Class " + clazz.getName() + " must be public"); + } + return new PojoClassIntrospector(clazz); + } + + public Class type() { + return clazz; + } + + public @Nullable MethodHandle noArgsConstructor() { + try { + return lookup.findConstructor(clazz, MethodType.methodType(void.class)); + } catch (NoSuchMethodException | IllegalAccessException | SecurityException e) { + return null; + } + } + + public Map properties() { + if (this.properties == null) { + this.properties = new HashMap<>(); + Class currentClass = clazz; + while (currentClass != null) { + appendClassProperties(currentClass); + currentClass = currentClass.getSuperclass(); + } + } + + return Collections.unmodifiableMap(this.properties); + } + + private void appendClassProperties(Class targetClass) { + try { + Field[] fields = targetClass.getDeclaredFields(); + for (Field field : fields) { + if (shouldIgnoreField(field)) { + continue; + } + + if (!properties.containsKey(field.getName())) { + Property property = introspectProperty(field); + properties.put(property.field.getName(), property); + } else { + // TODO: logging + } + } + } catch (Exception e) { + // TODO: logging + } + } + + private boolean shouldIgnoreField(Field field) { + return (field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0; + } + + private Property introspectProperty(Field field) { + int modifiers = field.getModifiers(); + + return Property.builder() + .field(field) + .isFinal((modifiers & Modifier.FINAL) != 0) + .getter(findGetter(field)) + .setter(findSetter(field)) + .type(field.getGenericType()) + .build(); + } + + @Nullable + private MethodHandle findGetter(Field field) { + String fieldName = field.getName(); + // fluid getters + MethodHandle method = findMethod( + clazz, + new MethodDescriptor(fieldName, MethodType.methodType(field.getType())) + ); + if (method != null) { + return method; + } + // boolean getters + if (field.getType() == Boolean.class || field.getType() == Boolean.TYPE) { + method = findMethod( + clazz, + new MethodDescriptor("is" + firstToUpper(fieldName), MethodType.methodType(field.getType())) + ); + if (method != null) { + return method; + } + } + // classic getters + method = findMethod( + clazz, + new MethodDescriptor("get" + firstToUpper(fieldName), MethodType.methodType(field.getType())) + ); + if (method != null) { + return method; + } + + // public field access + int modifiers = field.getModifiers(); + if ((modifiers & Modifier.PUBLIC) != 0) { + return findFieldGetter(field); + } + return null; + } + + @Nullable + private MethodHandle findSetter(Field field) { + String fieldName = field.getName(); + + String classicSetterName = "set" + firstToUpper(fieldName); + MethodHandle method = findFirstMethod( + clazz, + // fluid + new MethodDescriptor(fieldName, MethodType.methodType(Void.TYPE, field.getType())), + // fluid + chain + new MethodDescriptor(fieldName, MethodType.methodType(field.getDeclaringClass(), field.getType())), + // classic + new MethodDescriptor(classicSetterName, MethodType.methodType(Void.TYPE, field.getType())), + // classic + chain + new MethodDescriptor( + classicSetterName, + MethodType.methodType(field.getDeclaringClass(), field.getType()) + ) + ); + if (method != null) { + return method; + } + + // public field access + int modifiers = field.getModifiers(); + if ((modifiers & Modifier.PUBLIC) != 0) { + return findFieldSetter(field); + } + return null; + } + + @Nullable + private MethodHandle findFirstMethod(Class targetClass, MethodDescriptor... methodDescriptors) { + for (MethodDescriptor methodDescriptor : methodDescriptors) { + MethodHandle method = findMethod(targetClass, methodDescriptor); + if (method != null) { + return method; + } + } + return null; + } + + @Nullable + private MethodHandle findMethod(Class targetClass, MethodDescriptor methodDescriptor) { + try { + return lookup.findVirtual(targetClass, methodDescriptor.name(), methodDescriptor.methodType()); + } catch (NoSuchMethodException e) { + return null; + } catch (IllegalAccessException e) { + // TODO: logging + return null; + } + } + + @Nullable + private MethodHandle findFieldGetter(Field field) { + try { + return lookup.findGetter(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (NoSuchFieldException e) { + return null; + } catch (IllegalAccessException e) { + // TODO: logging + return null; + } + } + + @Nullable + private MethodHandle findFieldSetter(Field field) { + try { + return lookup.findSetter(field.getDeclaringClass(), field.getName(), field.getType()); + } catch (NoSuchFieldException e) { + return null; + } catch (IllegalAccessException e) { + // TODO: logging + return null; + } + } + + private static String firstToUpper(String text) { + return Character.toUpperCase(text.charAt(0)) + text.substring(1); + } + + @Value + private static class MethodDescriptor { + String name; + MethodType methodType; + } + + @Value + @Builder + public static class Property { + Field field; + boolean isFinal; + Type type; + @Nullable + MethodHandle getter; + @Nullable + MethodHandle setter; + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoWeavingException.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoWeavingException.java new file mode 100644 index 0000000..8583e83 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/PojoWeavingException.java @@ -0,0 +1,11 @@ +package de.siphalor.tweed5.weaver.pojo.impl.weaving; + +public class PojoWeavingException extends RuntimeException { + public PojoWeavingException(String message, Throwable cause) { + super(message, cause); + } + + public PojoWeavingException(String message) { + super(message); + } +} 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 new file mode 100644 index 0000000..fad0de5 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java @@ -0,0 +1,234 @@ +package de.siphalor.tweed5.weaver.pojo.impl.weaving; + +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; +import de.siphalor.tweed5.core.api.extension.TweedExtension; +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.weaver.pojo.api.annotation.PojoWeaving; +import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver; +import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext; +import lombok.*; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +/** + * 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}. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class TweedPojoWeaverBootstrapper { + private final Class pojoClass; + private final ConfigContainer configContainer; + private final Collection weavers; + private PatchworkClass contextExtensionsDataClass; + + public static TweedPojoWeaverBootstrapper create(Class pojoClass) { + PojoWeaving rootWeavingConfig = expectAnnotation(pojoClass, PojoWeaving.class); + + //noinspection unchecked + ConfigContainer configContainer = (ConfigContainer) createConfigContainer((Class>) rootWeavingConfig.container()); + + 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()))); + } + + private static Collection loadExtensions(Collection> extensionClasses) { + try { + return loadSingleServices(extensionClasses); + } catch (Exception e) { + throw new PojoWeavingException("Failed to load Tweed extensions", e); + } + } + + private static Collection loadWeavers(Collection> weaverClasses) { + List weavers = new ArrayList<>(); + for (Class weaverClass : weaverClasses) { + weavers.add(checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass)); + } + return weavers; + } + + private static ConfigContainer createConfigContainer(Class> containerClass) { + try { + return checkImplementsAndInstantiate(ConfigContainer.class, containerClass); + } catch (Exception e) { + throw new PojoWeavingException("Failed to instantiate config container"); + } + } + + + private static Collection loadSingleServices(Collection> serviceClasses) { + Collection services = new ArrayList<>(serviceClasses.size()); + for (Class serviceClass : serviceClasses) { + try { + services.add(loadSingleService(serviceClass)); + } catch (Exception e) { + throw new PojoWeavingException("Failed to instantiate single service " + serviceClass.getName(), e); + } + } + return services; + } + + private static S loadSingleService(Class serviceClass) { + try { + ServiceLoader loader = ServiceLoader.load(serviceClass); + Iterator iterator = loader.iterator(); + if (!iterator.hasNext()) { + throw new PojoWeavingException("Could not find any service for class " + serviceClass.getName()); + } + S service = iterator.next(); + + if (iterator.hasNext()) { + throw new PojoWeavingException( + "Found multiple services for class " + serviceClass.getName() + ": " + + createInstanceDebugStringFromIterator(loader.iterator()) + ); + } + + return service; + } catch (ServiceConfigurationError e) { + throw new PojoWeavingException("Failed to load service " + serviceClass.getName(), e); + } + } + + private static String createInstanceDebugStringFromIterator(Iterator iterator) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("[ "); + while (iterator.hasNext()) { + stringBuilder.append(createInstanceDebugDescriptor(iterator.next())); + stringBuilder.append(", "); + } + stringBuilder.append(" ]"); + return stringBuilder.toString(); + } + + private static String createInstanceDebugDescriptor(@Nullable Object object) { + if (object == null) { + return "null"; + } else { + return object.getClass().getName() + "@" + System.identityHashCode(object); + } + } + + private static A expectAnnotation(Class clazz, Class annotationClass) { + A annotation = clazz.getAnnotation(annotationClass); + if (annotation == null) { + throw new PojoWeavingException("Annotation " + annotationClass.getName() + " must be defined on class " + clazz); + } else { + return annotation; + } + } + + private static T checkImplementsAndInstantiate(Class superClass, Class clazz) { + if (!superClass.isAssignableFrom(clazz)) { + throw new PojoWeavingException("Class " + clazz.getName() + " must extend/implement " + superClass.getName()); + } + return instantiate(clazz); + } + + private static T instantiate(Class clazz) { + try { + Constructor constructor = clazz.getConstructor(); + return constructor.newInstance(); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException e) { + throw new PojoWeavingException("Failed to instantiate class " + clazz.getName(), e); + } + } + + public ConfigContainer weave() { + setupWeavers(); + WeavingContext weavingContext = createWeavingContext(); + + ConfigEntry rootEntry = this.weaveEntry(pojoClass, weavingContext); + configContainer.attachAndSealTree(rootEntry); + + return configContainer; + } + + private void setupWeavers() { + Map, RegisteredExtensionDataImpl> registeredExtensions = new HashMap<>(); + + TweedPojoWeaver.SetupContext setupContext = new TweedPojoWeaver.SetupContext() { + @Override + public RegisteredExtensionData registerWeavingContextExtensionData( + Class dataClass + ) { + RegisteredExtensionDataImpl registeredExtension = new RegisteredExtensionDataImpl<>(); + registeredExtensions.put(dataClass, registeredExtension); + return registeredExtension; + } + }; + + for (TweedPojoWeaver weaver : weavers) { + weaver.setup(setupContext); + } + + PatchworkClassCreator weavingContextCreator = PatchworkClassCreator.builder() + .classPackage(this.getClass().getPackage().getName() + ".generated") + .classPrefix("WeavingContext$") + .patchworkInterface(WeavingContext.ExtensionsData.class) + .build(); + + try { + this.contextExtensionsDataClass = weavingContextCreator.createClass(registeredExtensions.keySet()); + + for (PatchworkClassPart part : this.contextExtensionsDataClass.parts()) { + RegisteredExtensionDataImpl registeredExtension = registeredExtensions.get(part.partInterface()); + registeredExtension.setter(part.fieldSetter()); + } + } catch (PatchworkClassGenerator.GenerationException e) { + throw new PojoWeavingException("Failed to create weaving context extensions data"); + } + } + + private WeavingContext createWeavingContext() { + try { + WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke(); + return WeavingContext.builder() + .extensionsData(extensionsData) + .weavingFunction(this::weaveEntry) + .build(); + } catch (Throwable e) { + throw new PojoWeavingException("Failed to create weaving context's extension data"); + } + } + + private ConfigEntry weaveEntry(Class dataClass, WeavingContext context) { + for (TweedPojoWeaver weaver : weavers) { + ConfigEntry configEntry = weaver.weaveEntry(dataClass, context); + if (configEntry != null) { + configEntry.seal(configContainer); + return configEntry; + } + } + + throw new PojoWeavingException("Failed to weave " + dataClass.getName() + ": No matching weavers found"); + } + + @Setter + private static class RegisteredExtensionDataImpl implements RegisteredExtensionData { + private MethodHandle setter; + + @Override + public void set(WeavingContext.ExtensionsData patchwork, E extension) { + try { + setter.invokeWithArguments(patchwork, extension); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfig.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfig.java new file mode 100644 index 0000000..71fd835 --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfig.java @@ -0,0 +1,15 @@ +package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound; + +import de.siphalor.tweed5.namingformat.api.NamingFormat; +import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; +import org.jetbrains.annotations.Nullable; + +public interface CompoundWeavingConfig { + NamingFormat compoundSourceNamingFormat(); + + NamingFormat compoundTargetNamingFormat(); + + @SuppressWarnings("rawtypes") + @Nullable + Class compoundEntryClass(); +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfigImpl.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfigImpl.java new file mode 100644 index 0000000..b8d03fc --- /dev/null +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/compound/CompoundWeavingConfigImpl.java @@ -0,0 +1,27 @@ +package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound; + +import de.siphalor.tweed5.namingformat.api.NamingFormat; +import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; +import lombok.Builder; +import lombok.Value; +import org.jetbrains.annotations.Nullable; + +@Builder +@Value +public class CompoundWeavingConfigImpl implements CompoundWeavingConfig { + private static final CompoundWeavingConfigImpl EMPTY = CompoundWeavingConfigImpl.builder().build(); + + NamingFormat compoundSourceNamingFormat; + NamingFormat compoundTargetNamingFormat; + @SuppressWarnings("rawtypes") + @Nullable + Class compoundEntryClass; + + public static CompoundWeavingConfigImpl withOverrides(CompoundWeavingConfig self, CompoundWeavingConfig overrides) { + return CompoundWeavingConfigImpl.builder() + .compoundSourceNamingFormat(overrides.compoundSourceNamingFormat() != null ? overrides.compoundSourceNamingFormat() : self.compoundSourceNamingFormat()) + .compoundTargetNamingFormat(overrides.compoundTargetNamingFormat() != null ? overrides.compoundTargetNamingFormat() : self.compoundTargetNamingFormat()) + .compoundEntryClass(overrides.compoundEntryClass() != null ? overrides.compoundEntryClass() : self.compoundEntryClass()) + .build(); + } +} diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java new file mode 100644 index 0000000..ae7f812 --- /dev/null +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java @@ -0,0 +1,197 @@ +package de.siphalor.tweed5.weaver.pojo.api; + +import de.siphalor.tweed5.core.api.collection.TypedMultimap; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class TypedMultimapTest { + + @Test + void size() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + assertEquals(0, map.size()); + map.add("abc"); + assertEquals(1, map.size()); + map.add(456); + assertEquals(2, map.size()); + map.add("def"); + assertEquals(3, map.size()); + map.remove(456); + assertEquals(2, map.size()); + } + + @Test + void isEmpty() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + assertTrue(map.isEmpty()); + map.add("def"); + assertFalse(map.isEmpty()); + map.remove("def"); + assertTrue(map.isEmpty()); + } + + @Test + void contains() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + assertFalse(map.contains(123)); + map.add(456); + assertFalse(map.contains(123)); + map.add(123); + assertTrue(map.contains(123)); + } + + @Test + void classes() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, "abc", "def", "ghi", 789L)); + assertEquals(new HashSet<>(Arrays.asList(Integer.class, String.class, Long.class)), map.classes()); + } + + @Test + void iterator() { + TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + map.add("abc"); + map.add(123); + map.add("def"); + map.add(456); + + Iterator iterator = map.iterator(); + assertThrows(IllegalStateException.class, iterator::remove); + assertTrue(iterator.hasNext()); + assertEquals("abc", iterator.next()); + iterator.remove(); + assertTrue(iterator.hasNext()); + assertEquals("def", iterator.next()); + iterator.remove(); + assertTrue(iterator.hasNext()); + assertEquals(123, iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals(456, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void toArray() { + TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + map.add("abc"); + map.add(123); + map.add("def"); + map.add(456); + + Object[] array = map.toArray(); + assertArrayEquals(new Object[] { "abc", "def", 123, 456 }, array); + } + + @Test + void toArrayProvided() { + TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + map.add(12); + map.add(34L); + map.add(56); + map.add(78L); + + @NotNull Number[] array = map.toArray(new Number[0]); + assertArrayEquals(new Object[] { 12, 56, 34L, 78L }, array); + } + + @Test + void add() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), HashSet::new); + assertTrue(map.isEmpty()); + map.add(123); + assertEquals(1, map.size()); + map.add("abc"); + assertEquals(2, map.size()); + map.add(123); + assertEquals(2, map.size()); + map.add("abc"); + assertEquals(2, map.size()); + } + + @Test + void remove() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, "abc", "def")); + assertEquals(4, map.size()); + map.remove("def"); + assertEquals(3, map.size()); + map.remove("abc"); + assertEquals(2, map.size()); + } + + @Test + void getAll() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, "abc", "def")); + assertEquals(Arrays.asList(123, 456), map.getAll(Integer.class)); + assertEquals(Arrays.asList("abc", "def"), map.getAll(String.class)); + assertEquals(Collections.emptyList(), map.getAll(Long.class)); + } + + @Test + void removeAll() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, 789, "abc", "def")); + map.removeAll(Arrays.asList(456, "def")); + assertEquals(3, map.size()); + assertEquals(Arrays.asList(123, 789), map.getAll(Integer.class)); + assertEquals(Collections.singletonList("abc"), map.getAll(String.class)); + map.removeAll(Arrays.asList(123, 789)); + assertArrayEquals(new Object[] { "abc" }, map.toArray()); + } + + @Test + void containsAll() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, 789, "abc", "def")); + assertTrue(map.containsAll(Arrays.asList(456, "def"))); + assertTrue(map.containsAll(Arrays.asList(123, 789))); + assertFalse(map.containsAll(Arrays.asList(404, 789))); + } + + @Test + void addAll() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, 789, "abc", "def")); + assertEquals(5, map.size()); + assertEquals(Arrays.asList(123, 456, 789), map.getAll(Integer.class)); + map.addAll(Arrays.asList(123L, 456L)); + assertEquals(Arrays.asList(123L, 456L), map.getAll(Long.class)); + } + + @Test + void removeAllByClass() { + TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L)); + map.removeAll(Integer.class); + assertEquals(3, map.size()); + map.removeAll(Long.class); + assertEquals(2, map.size()); + map.removeAll(String.class); + assertTrue(map.isEmpty()); + } + + @Test + void retainAll() { + TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L)); + map.retainAll(Arrays.asList("abc", 456)); + assertEquals(2, map.size()); + assertArrayEquals(new Object[] { 456, "abc" }, map.toArray()); + map.retainAll(Collections.emptyList()); + assertTrue(map.isEmpty()); + } + + @Test + void clear() { + TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L)); + map.clear(); + assertTrue(map.isEmpty()); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..bb72096 --- /dev/null +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java @@ -0,0 +1,122 @@ +package de.siphalor.tweed5.weaver.pojo.api.weaving; + +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry; +import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; +import de.siphalor.tweed5.namingformat.api.NamingFormat; +import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving; +import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; +import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import 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; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unused") +class CompoundPojoWeaverTest { + + @Test + void weave() { + CompoundPojoWeaver compoundWeaver = new CompoundPojoWeaver(); + compoundWeaver.setup(new TweedPojoWeaver.SetupContext() { + @Override + public RegisteredExtensionData registerWeavingContextExtensionData(Class dataClass) { + return (patchwork, extension) -> ((ExtensionsDataMock) patchwork).weavingConfig = (CompoundWeavingConfig) extension; + } + }); + + WeavingContext weavingContext = WeavingContext.builder() + .extensionsData(new ExtensionsDataMock(null)) + .weavingFunction(new TweedPojoWeavingFunction.NonNull() { + @Override + public @NotNull ConfigEntry weaveEntry(Class valueClass, WeavingContext context) { + ConfigEntry entry = compoundWeaver.weaveEntry(valueClass, context); + if (entry != null) { + return entry; + } else { + //noinspection unchecked + ConfigEntry configEntry = mock((Class>) (Class) SimpleConfigEntry.class); + when(configEntry.valueClass()).thenReturn(valueClass); + return configEntry; + } + } + }) + .build(); + + ConfigEntry resultEntry = compoundWeaver.weaveEntry(Compound.class, weavingContext); + + assertThat(resultEntry).satisfies(isCompoundEntryForClassWith(Compound.class, compoundEntry -> assertThat(compoundEntry.subEntries()) + .hasEntrySatisfying("an-integer", isSimpleEntryForClass(int.class)) + .hasEntrySatisfying("inner-compound", isCompoundEntryForClassWith(InnerCompound1.class, innerCompound1 -> assertThat(innerCompound1.subEntries()) + .hasEntrySatisfying("a-parameter", isSimpleEntryForClass(String.class)) + .hasEntrySatisfying("inner-compound", isCompoundEntryForClassWith(InnerCompound2.class, innerCompound2 -> assertThat(innerCompound2.subEntries()) + .hasEntrySatisfying("inner_value", isSimpleEntryForClass(InnerValue.class)) + .hasSize(1) + )) + .hasSize(2) + )) + .hasSize(2) + )); + } + + @CompoundWeaving(namingFormat = "kebab_case") + public static class Compound { + public int anInteger; + public InnerCompound1 innerCompound; + } + + @CompoundWeaving + public static class InnerCompound1 { + public String aParameter; + public InnerCompound2 innerCompound; + } + + @CompoundWeaving(namingFormat = "snake_case") + public static class InnerCompound2 { + public InnerValue innerValue; + } + + public static class InnerValue { + public Integer value; + } + + @AllArgsConstructor + private static class ExtensionsDataMock implements WeavingContext.ExtensionsData, CompoundWeavingConfig { + private CompoundWeavingConfig weavingConfig; + + @Override + public boolean isPatchworkPartDefined(Class patchworkInterface) { + return patchworkInterface == CompoundWeavingConfig.class; + } + + @Override + public boolean isPatchworkPartSet(Class patchworkInterface) { + return weavingConfig != null; + } + + @Override + public WeavingContext.ExtensionsData copy() { + return new ExtensionsDataMock(weavingConfig); + } + + @Override + public NamingFormat compoundSourceNamingFormat() { + return weavingConfig.compoundSourceNamingFormat(); + } + + @Override + public NamingFormat compoundTargetNamingFormat() { + return weavingConfig.compoundTargetNamingFormat(); + } + + @Override + public @Nullable Class compoundEntryClass() { + return weavingConfig.compoundEntryClass(); + } + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/PojoClassIntrospectorTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/PojoClassIntrospectorTest.java new file mode 100644 index 0000000..33d62bd --- /dev/null +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/PojoClassIntrospectorTest.java @@ -0,0 +1,233 @@ +package de.siphalor.tweed5.weaver.pojo.impl; + +import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@SuppressWarnings({"LombokSetterMayBeUsed", "LombokGetterMayBeUsed"}) +class PojoClassIntrospectorTest { + @Test + void propertiesClassicAccessors() { + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(ClassicPojo.class); + + ClassicPojo instance = new ClassicPojo(123); + + Map result = assertDoesNotThrow(introspector::properties); + assertThat(result).hasSize(4) + .hasEntrySatisfying("integer", property -> { + assertThat(property.field().getName()).isEqualTo("integer"); + assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class); + assertThat(property.type()).isEqualTo(Integer.TYPE); + assertThat(property.isFinal()).isTrue(); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNull(); + + assertThatNoException() + .isThrownBy(() -> assertThat((int) property.getter().invokeExact(instance)).isEqualTo(123)); + }) + .hasEntrySatisfying("str", property -> { + assertThat(property.field().getName()).isEqualTo("str"); + assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class); + assertThat(property.type()).isEqualTo(String.class); + assertThat(property.isFinal()).isFalse(); + assertThat(property.getter()).isNull(); + assertThat(property.setter()).isNotNull(); + + assertThatNoException().isThrownBy(() -> property.setter().invoke(instance, "Hello")); + assertThat(instance.str).isEqualTo("Hello"); + }) + .hasEntrySatisfying("bool", property -> { + assertThat(property.field().getName()).isEqualTo("bool"); + assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class); + assertThat(property.type()).isEqualTo(Boolean.TYPE); + assertThat(property.isFinal()).isFalse(); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNotNull(); + + assertThatNoException() + .isThrownBy(() -> property.setter().invoke(instance, true)); + assertThatNoException() + .isThrownBy(() -> assertThat((boolean) property.getter().invokeExact(instance)).isTrue()); + }) + .hasEntrySatisfying("boolObj", property -> { + assertThat(property.field().getName()).isEqualTo("boolObj"); + assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class); + assertThat(property.type()).isEqualTo(Boolean.class); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNotNull(); + + assertThatNoException() + .isThrownBy(() -> property.setter().invoke(instance, true)); + assertThatNoException() + .isThrownBy(() -> assertThat((Boolean) property.getter().invokeExact(instance)).isTrue()); + }); + } + + @Test + void propertiesFluidAndChainedAccessors() { + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(FluidChainedPojo.class); + + FluidChainedPojo instance = new FluidChainedPojo(123); + + Map result = assertDoesNotThrow(introspector::properties); + assertThat(result).hasSize(3) + .hasEntrySatisfying("integer", property -> { + assertThat(property.field().getName()).isEqualTo("integer"); + assertThat(property.field().getDeclaringClass()).isEqualTo(FluidChainedPojo.class); + assertThat(property.type()).isEqualTo(Integer.TYPE); + assertThat(property.isFinal()).isTrue(); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNull(); + + assertThatNoException() + .isThrownBy(() -> assertThat((int) property.getter().invokeExact(instance)).isEqualTo(123)); + }) + .hasEntrySatisfying("str", property -> { + assertThat(property.field().getName()).isEqualTo("str"); + assertThat(property.field().getDeclaringClass()).isEqualTo(FluidChainedPojo.class); + assertThat(property.type()).isEqualTo(String.class); + assertThat(property.isFinal()).isFalse(); + assertThat(property.getter()).isNull(); + assertThat(property.setter()).isNotNull(); + + assertThatNoException().isThrownBy(() -> property.setter().invoke(instance, "Hello")); + assertThat(instance.str).isEqualTo("Hello"); + }) + .hasEntrySatisfying("bool", property -> { + assertThat(property.field().getName()).isEqualTo("bool"); + assertThat(property.field().getDeclaringClass()).isEqualTo(FluidChainedPojo.class); + assertThat(property.type()).isEqualTo(Boolean.TYPE); + assertThat(property.isFinal()).isFalse(); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNotNull(); + + assertThatNoException() + .isThrownBy(() -> property.setter().invoke(instance, true)); + assertThatNoException() + .isThrownBy(() -> assertThat((boolean) property.getter().invokeExact(instance)).isTrue()); + }); + } + + @Test + void propertiesDirectAccess() { + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(DirectAccessPojo.class); + + DirectAccessPojo instance = new DirectAccessPojo(123); + + Map result = assertDoesNotThrow(introspector::properties); + assertThat(result).hasSize(2) + .hasEntrySatisfying("integer", property -> { + assertThat(property.field().getName()).isEqualTo("integer"); + assertThat(property.field().getDeclaringClass()).isEqualTo(DirectAccessPojo.class); + assertThat(property.type()).isEqualTo(Integer.TYPE); + assertThat(property.isFinal()).isTrue(); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNull(); + + assertThatNoException() + .isThrownBy(() -> assertThat(property.getter().invoke(instance)).isEqualTo(123)); + }) + .hasEntrySatisfying("str", property -> { + assertThat(property.field().getName()).isEqualTo("str"); + assertThat(property.field().getDeclaringClass()).isEqualTo(DirectAccessPojo.class); + assertThat(property.type()).isEqualTo(String.class); + assertThat(property.isFinal()).isFalse(); + assertThat(property.getter()).isNotNull(); + assertThat(property.setter()).isNotNull(); + + assertThatNoException() + .isThrownBy(() -> property.setter().invoke(instance, "abcd")); + assertThat(instance.str).isEqualTo("abcd"); + }); + } + + @Test + void noArgsConstructorNone() { + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(ClassicPojo.class); + assertThat(introspector.noArgsConstructor()).isNull(); + } + + @Test + void noArgsConstructor() { + PojoClassIntrospector introspector = PojoClassIntrospector.forClass(NoArgs.class); + + assertThat(introspector.noArgsConstructor()) + .isNotNull() + .satisfies(constructor -> assertThat(constructor.invoke()).isInstanceOf(NoArgs.class)); + } + + @SuppressWarnings("unused") + @RequiredArgsConstructor + public static class ClassicPojo { + final int integer; + String str; + boolean bool; + Boolean boolObj; + + public int getInteger() { + return integer; + } + + public void setStr(String str) { + this.str = str; + } + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public Boolean isBoolObj() { + return boolObj; + } + + public void setBoolObj(Boolean boolObj) { + this.boolObj = boolObj; + } + } + + @SuppressWarnings("unused") + @RequiredArgsConstructor + public static class FluidChainedPojo { + final int integer; + String str; + boolean bool; + + public int integer() { + return integer; + } + + public FluidChainedPojo str(String value) { + this.str = value; + return this; + } + + public boolean bool() { + return bool; + } + + public FluidChainedPojo bool(boolean value) { + this.bool = value; + return this; + } + } + + @SuppressWarnings("unused") + @RequiredArgsConstructor + public static class DirectAccessPojo { + public final int integer; + public String str; + } + + public static class NoArgs { + public String noop; + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java new file mode 100644 index 0000000..5143063 --- /dev/null +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java @@ -0,0 +1,71 @@ +package de.siphalor.tweed5.weaver.pojo.impl.weaving; + +import com.google.auto.service.AutoService; +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.extension.TweedExtension; +import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving; +import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving; +import lombok.Data; +import org.junit.jupiter.api.Test; + +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; + +@SuppressWarnings("unused") +class TweedPojoWeaverBootstrapperTest { + @Test + void defaultWeaving() { + TweedPojoWeaverBootstrapper bootstrapper = TweedPojoWeaverBootstrapper.create(DefaultWeaving.class); + ConfigContainer configContainer = bootstrapper.weave(); + + assertThat(configContainer.rootEntry()).satisfies(isCompoundEntryForClassWith(DefaultWeaving.class, rootCompound -> + assertThat(rootCompound.subEntries()) + .hasEntrySatisfying("primitiveInteger", isSimpleEntryForClass(int.class)) + .hasEntrySatisfying("boxedDouble", isSimpleEntryForClass(Double.class)) + .hasEntrySatisfying("value", isSimpleEntryForClass(InnerValue.class)) + .hasEntrySatisfying("compound", isCompoundEntryForClassWith(InnerCompound.class, innerCompound -> + assertThat(innerCompound.subEntries()) + .hasEntrySatisfying("string", isSimpleEntryForClass(String.class)) + .hasSize(1))) + .hasSize(4) + )); + + configContainer.initialize(); + + assertThat(configContainer.extensions()) + .satisfiesOnlyOnce(extension -> assertThat(extension).isInstanceOf(DummyExtension.class)) + .hasSize(1); + } + + @AutoService(DummyExtension.class) + public static class DummyExtension implements TweedExtension { + @Override + public String getId() { + return "dummy"; + } + } + + @PojoWeaving(extensions = {DummyExtension.class}) + @CompoundWeaving(namingFormat = "camel_case") + @Data + public static class DefaultWeaving { + int primitiveInteger; + Double boxedDouble; + InnerValue value; + + InnerCompound compound; + } + + @CompoundWeaving + @Data + public static class InnerCompound { + String string; + } + + @Data + public static class InnerValue { + int something; + boolean somethingElse; + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java new file mode 100644 index 0000000..e52d1c2 --- /dev/null +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/test/ConfigEntryAssertions.java @@ -0,0 +1,32 @@ +package de.siphalor.tweed5.weaver.pojo.test; + +import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry; + +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; + +public class ConfigEntryAssertions { + public static Consumer isSimpleEntryForClass(Class valueClass) { + return object -> assertThat(object) + .asInstanceOf(type(SimpleConfigEntry.class)) + .extracting(ConfigEntry::valueClass) + .isEqualTo(valueClass); + } + + @SuppressWarnings("unchecked") + public static Consumer isCompoundEntryForClassWith( + Class compoundClass, + Consumer> condition + ) { + return object -> assertThat(object) + .asInstanceOf(type(CompoundConfigEntry.class)) + .satisfies(compoundEntry -> { + assertThat(compoundEntry.valueClass()).isEqualTo(compoundClass); + condition.accept(compoundEntry); + }); + } +}