diff --git a/settings.gradle.kts b/settings.gradle.kts index 24cc10d..188916e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,5 +7,6 @@ include("tweed5-patchwork") include("tweed5-serde-api") include("tweed5-serde-extension") include("tweed5-serde-hjson") +include("tweed5-utils") include("tweed5-weaver-pojo") include("tweed5-weaver-pojo-serde-extension") diff --git a/tweed5-core/build.gradle.kts b/tweed5-core/build.gradle.kts index 87361d5..5ccbb5b 100644 --- a/tweed5-core/build.gradle.kts +++ b/tweed5-core/build.gradle.kts @@ -1,3 +1,4 @@ dependencies { api(project(":tweed5-patchwork")) + api(project(":tweed5-utils")) } \ No newline at end of file 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 eb2eb00..7de32ee 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 @@ -4,6 +4,7 @@ import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; 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 org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.Map; @@ -15,10 +16,6 @@ import java.util.Map; * @see ConfigContainerSetupPhase */ public interface ConfigContainer { - ConfigContainerSetupPhase setupPhase(); - default boolean isReady() { - return setupPhase() == ConfigContainerSetupPhase.READY; - } default void registerExtensions(TweedExtension... extensions) { for (TweedExtension extension : extensions) { @@ -28,6 +25,10 @@ public interface ConfigContainer { void registerExtension(TweedExtension extension); + @Nullable + E extension(Class extensionClass); + Collection extensions(); + void finishExtensionSetup(); void attachAndSealTree(ConfigEntry rootEntry); @@ -37,7 +38,5 @@ public interface ConfigContainer { void initialize(); ConfigEntry rootEntry(); - - Collection extensions(); Map, ? extends RegisteredExtensionData> entryDataExtensions(); } 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 69bb323..25a28b7 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/DefaultConfigContainer.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/DefaultConfigContainer.java @@ -9,33 +9,37 @@ import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator; import de.siphalor.tweed5.patchwork.impl.PatchworkClass; import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator; import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart; +import de.siphalor.tweed5.utils.api.collection.ClassToInstanceMap; import lombok.Getter; import lombok.Setter; +import org.jetbrains.annotations.Nullable; import java.lang.invoke.MethodHandle; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; public class DefaultConfigContainer implements ConfigContainer { @Getter private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP; - private final Map, TweedExtension> extensions = new HashMap<>(); + private final ClassToInstanceMap extensions = new ClassToInstanceMap<>(); private ConfigEntry rootEntry; private PatchworkClass entryExtensionsDataPatchworkClass; private Map, RegisteredExtensionDataImpl> registeredEntryDataExtensions; + @Override + public @Nullable E extension(Class extensionClass) { + return extensions.get(extensionClass); + } + @Override public Collection extensions() { - return extensions.values(); + return Collections.unmodifiableCollection(extensions.values()); } @Override public void registerExtension(TweedExtension extension) { requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP); - TweedExtension previous = extensions.put(extension.getClass(), extension); + TweedExtension previous = extensions.put(extension); if (previous != null) { throw new IllegalArgumentException("Extension " + extension.getClass().getName() + " is already registered"); } @@ -65,7 +69,7 @@ public class DefaultConfigContainer implements ConfigContainer { @Override public void registerExtension(TweedExtension extension) { - if (!extensions.containsKey(extension.getClass())) { + if (!extensions.containsClass(extension.getClass())) { additionalExtensions.add(extension); } } @@ -78,7 +82,7 @@ public class DefaultConfigContainer implements ConfigContainer { } for (TweedExtension additionalExtension : additionalExtensions) { - extensions.put(additionalExtension.getClass(), additionalExtension); + extensions.put(additionalExtension); } extensionsToSetup = new ArrayList<>(additionalExtensions); additionalExtensions.clear(); diff --git a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java new file mode 100644 index 0000000..8029192 --- /dev/null +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMap.java @@ -0,0 +1,94 @@ +package de.siphalor.tweed5.utils.api.collection; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +@SuppressWarnings("unchecked") +@EqualsAndHashCode +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class ClassToInstanceMap implements Iterable { + private final Map, T> delegate; + + public static ClassToInstanceMap backedBy(Map, T> delegate) { + return new ClassToInstanceMap<>(delegate); + } + + public ClassToInstanceMap() { + this(new HashMap<>()); + } + + public int size() { + return delegate.size(); + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + public boolean containsClass(Class key) { + return delegate.containsKey(key); + } + + public boolean containsValue(T value) { + return delegate.containsValue(value); + } + + public V get(Class key) { + return (V) delegate.get(key); + } + + public V put(@NotNull V value) { + return (V) delegate.put((Class) value.getClass(), value); + } + + public V remove(Class key) { + return (V) delegate.remove(key); + } + + public void clear() { + delegate.clear(); + } + + public Set> classes() { + return delegate.keySet(); + } + + public Set values() { + return new AbstractSet() { + @Override + public @NotNull Iterator iterator() { + Iterator, T>> entryIterator = delegate.entrySet().iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return entryIterator.hasNext(); + } + + @Override + public T next() { + return entryIterator.next().getValue(); + } + + @Override + public void remove() { + entryIterator.remove(); + } + }; + } + + @Override + public int size() { + return delegate.size(); + } + }; + } + + @Override + public @NotNull Iterator iterator() { + return values().iterator(); + } +} diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstancesMultimap.java similarity index 91% rename from tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java rename to tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstancesMultimap.java index 9fbd77d..7bfc9ea 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/collection/TypedMultimap.java +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ClassToInstancesMultimap.java @@ -1,4 +1,4 @@ -package de.siphalor.tweed5.core.api.collection; +package de.siphalor.tweed5.utils.api.collection; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; @@ -10,18 +10,18 @@ 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)); +public class ClassToInstancesMultimap implements Collection { + private static final ClassToInstancesMultimap EMPTY = unmodifiable(new ClassToInstancesMultimap<>(Collections.emptyMap(), ArrayList::new)); protected final Map, Collection> delegate; protected final Supplier> collectionSupplier; - public static TypedMultimap unmodifiable(TypedMultimap map) { + public static ClassToInstancesMultimap unmodifiable(ClassToInstancesMultimap map) { return new Unmodifiable<>(map.delegate, map.collectionSupplier); } - public static TypedMultimap empty() { - return (TypedMultimap) EMPTY; + public static ClassToInstancesMultimap empty() { + return (ClassToInstancesMultimap) EMPTY; } public int size() { @@ -169,7 +169,7 @@ public class TypedMultimap implements Collection { delegate.clear(); } - protected static class Unmodifiable extends TypedMultimap { + protected static class Unmodifiable extends ClassToInstancesMultimap { public Unmodifiable( Map, Collection> delegate, Supplier> collectionSupplier diff --git a/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMapTest.java b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMapTest.java new file mode 100644 index 0000000..7253ed1 --- /dev/null +++ b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ClassToInstanceMapTest.java @@ -0,0 +1,152 @@ +package de.siphalor.tweed5.utils.api.collection; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +class ClassToInstanceMapTest { + + @Test + void size() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + assertThat(map.size()).isZero(); + map.put(1234); + assertThat(map.size()).isEqualTo(1); + map.put(456); + assertThat(map.size()).isEqualTo(1); + map.put(789L); + assertThat(map.size()).isEqualTo(2); + } + + @Test + void isEmpty() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + assertThat(map.isEmpty()).isTrue(); + map.put(123L); + assertThat(map.isEmpty()).isFalse(); + map.remove(Long.class); + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void containsClass() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + map.put(123L); + map.put("abc"); + assertThat(map.containsClass(Long.class)).isTrue(); + assertThat(map.containsClass(String.class)).isTrue(); + assertThat(map.containsClass(Integer.class)).isFalse(); + } + + @Test + void containsValue() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + map.put(123.45D); + map.put("test"); + assertThat(map.containsValue(123)).isFalse(); + assertThat(map.containsValue(123.4D)).isFalse(); + assertThat(map.containsValue(123.45D)).isTrue(); + assertThat(map.containsValue("TEST")).isFalse(); + assertThat(map.containsValue("test")).isTrue(); + } + + @Test + void get() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + assertThat(map.get(Integer.class)).isNull(); + map.put(123); + map.put(456L); + assertThat(map.get(Float.class)).isNull(); + assertThat(map.get(Integer.class)).isEqualTo(123); + assertThat(map.get(Long.class)).isEqualTo(456L); + } + + @Test + void put() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + map.put(123); + assertThat(map.size()).isEqualTo(1); + map.put(456); + assertThat(map.size()).isEqualTo(1); + map.put(123L); + assertThat(map.size()).isEqualTo(2); + map.put(456); + assertThat(map.size()).isEqualTo(2); + } + + @Test + void remove() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + map.put(123); + map.put("abcdefg"); + assertThat(map.size()).isEqualTo(2); + map.remove(Long.class); + assertThat(map.size()).isEqualTo(2); + map.remove(String.class); + assertThat(map.size()).isEqualTo(1); + map.remove(Integer.class); + assertThat(map.size()).isZero(); + } + + @Test + void clear() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + map.put(123); + map.put("abcdefg"); + assertThat(map.size()).isEqualTo(2); + map.clear(); + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void classes() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + assertThat(map.classes()).isEmpty(); + map.put(123); + assertThat(map.classes()).containsExactlyInAnyOrder(Integer.class); + map.put(456); + assertThat(map.classes()).containsExactlyInAnyOrder(Integer.class); + map.put("heyho"); + assertThat(map.classes()).containsExactlyInAnyOrder(Integer.class, String.class); + } + + @Test + void values() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + assertThat(map.values()).isEmpty(); + map.put(123); + assertThat(map.values()).containsExactlyInAnyOrder(123); + map.put(123L); + assertThat(map.values()).containsExactlyInAnyOrder(123, 123L); + map.put(456); + assertThat(map.values()).containsExactlyInAnyOrder(456, 123L); + } + + @Test + void iterator() { + ClassToInstanceMap map = new ClassToInstanceMap<>(); + assertThat(map.iterator()).isExhausted(); + map.put(123); + Iterator iterator = map.iterator(); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo(123); + assertThat(iterator).isExhausted(); + + map.put(123L); + iterator = map.iterator(); + assertThat(iterator).hasNext(); + Object first = iterator.next(); + assertThat(first).satisfiesAnyOf(value -> assertThat(value).isEqualTo(123), value -> assertThat(value).isEqualTo(123L)); + assertThat(iterator).hasNext(); + assertThat(iterator.next()) + .as("must be different from the first value") + .isNotEqualTo(first) + .satisfiesAnyOf(value -> assertThat(value).isEqualTo(123), value -> assertThat(value).isEqualTo(123L)); + iterator.remove(); + assertThat(iterator).isExhausted(); + + assertThat(map.size()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ClassToInstancesMultimapTest.java similarity index 72% rename from tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java rename to tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ClassToInstancesMultimapTest.java index 41f9df4..f5cc19d 100644 --- a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/TypedMultimapTest.java +++ b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ClassToInstancesMultimapTest.java @@ -1,6 +1,5 @@ -package de.siphalor.tweed5.weaver.pojo.api; +package de.siphalor.tweed5.utils.api.collection; -import de.siphalor.tweed5.core.api.collection.TypedMultimap; import org.junit.jupiter.api.Test; import java.util.*; @@ -9,11 +8,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @SuppressWarnings("java:S5838") // Since we're testing collections methods here, AssertJ's shorthands are not applicable -class TypedMultimapTest { +class ClassToInstancesMultimapTest { @Test void size() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); assertThat(map).isEmpty(); map.add("abc"); assertThat(map.size()).isEqualTo(1); @@ -27,7 +26,7 @@ class TypedMultimapTest { @Test void isEmpty() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); assertThat(map.isEmpty()).isTrue(); map.add("def"); assertThat(map.isEmpty()).isFalse(); @@ -37,7 +36,7 @@ class TypedMultimapTest { @Test void contains() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); assertThat(map.contains(123)).isFalse(); map.add(456); assertThat(map.contains(123)).isFalse(); @@ -47,14 +46,14 @@ class TypedMultimapTest { @Test void classes() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, "abc", "def", "ghi", 789L)); assertThat(map.classes()).containsExactlyInAnyOrder(Integer.class, String.class, Long.class); } @Test void iterator() { - TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new LinkedHashMap<>(), ArrayList::new); map.add("abc"); map.add(123); map.add("def"); @@ -78,7 +77,7 @@ class TypedMultimapTest { @Test void toArray() { - TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new LinkedHashMap<>(), ArrayList::new); map.add("abc"); map.add(123); map.add("def"); @@ -89,7 +88,7 @@ class TypedMultimapTest { @Test void toArrayProvided() { - TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new LinkedHashMap<>(), ArrayList::new); map.add(12); map.add(34L); map.add(56); @@ -100,7 +99,7 @@ class TypedMultimapTest { @Test void add() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), HashSet::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), HashSet::new); assertThat(map).isEmpty(); map.add(123); assertThat(map).hasSize(1); @@ -114,7 +113,7 @@ class TypedMultimapTest { @Test void remove() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, "abc", "def")); assertThat(map).hasSize(4); map.remove("def"); @@ -125,7 +124,7 @@ class TypedMultimapTest { @Test void getAll() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, "abc", "def")); assertThat(map.getAll(Integer.class)).containsExactly(123, 456); assertThat(map.getAll(String.class)).containsExactly("abc", "def"); @@ -134,7 +133,7 @@ class TypedMultimapTest { @Test void removeAll() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, 789, "abc", "def")); map.removeAll(Arrays.asList(456, "def")); assertThat(map).hasSize(3); @@ -146,7 +145,7 @@ class TypedMultimapTest { @Test void containsAll() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, 789, "abc", "def")); assertThat(map.containsAll(Arrays.asList(456, "def"))).isTrue(); assertThat(map.containsAll(Arrays.asList(123, 789))).isTrue(); @@ -155,7 +154,7 @@ class TypedMultimapTest { @Test void addAll() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, 789, "abc", "def")); assertThat(map).hasSize(5); assertThat(map.getAll(Integer.class)).containsExactlyElementsOf(Arrays.asList(123, 456, 789)); @@ -165,7 +164,7 @@ class TypedMultimapTest { @Test void removeAllByClass() { - TypedMultimap map = new TypedMultimap<>(new HashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new HashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L)); map.removeAll(Integer.class); assertThat(map).hasSize(3); @@ -177,7 +176,7 @@ class TypedMultimapTest { @Test void retainAll() { - TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new LinkedHashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L)); map.retainAll(Arrays.asList("abc", 456)); assertThat(map).hasSize(2); @@ -188,7 +187,7 @@ class TypedMultimapTest { @Test void clear() { - TypedMultimap map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new); + ClassToInstancesMultimap map = new ClassToInstancesMultimap<>(new LinkedHashMap<>(), ArrayList::new); map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L)); map.clear(); assertThat(map.isEmpty()).isTrue();