From c9a609d457b3c9e6c619cdc656e323d5dab6063c Mon Sep 17 00:00:00 2001 From: Siphalor Date: Sun, 27 Jul 2025 01:18:32 +0200 Subject: [PATCH] [attributes] Introduce attributes extensions --- buildSrc/settings.gradle.kts | 2 +- settings.gradle.kts | 2 + tweed5-attributes-extension/build.gradle.kts | 10 + .../api/AttributesExtension.java | 38 ++ .../api/AttributesRelatedExtension.java | 5 + .../attributesextension/api/package-info.java | 4 + .../AttributesReadWriteFilterExtension.java | 13 + .../api/serde/filter/package-info.java | 4 + .../impl/AttributesExtensionImpl.java | 224 ++++++++++ .../impl/package-info.java | 6 + ...ttributesReadWriteFilterExtensionImpl.java | 404 ++++++++++++++++++ .../impl/serde/filter/package-info.java | 6 + ...butesReadWriteFilterExtensionImplTest.java | 183 ++++++++ .../core/api/extension/TweedExtension.java | 3 + .../core/impl/DefaultConfigContainer.java | 4 + .../impl/sort/AcyclicGraphSorterTest.java | 9 +- .../tweed5/data/hjson/HjsonWriter.java | 6 +- .../tweed5/utils/api/UniqueSymbol.java | 13 + .../collection/ImmutableArrayBackedMap.java | 195 +++++++++ .../collection/ImmutableArrayBackedSet.java | 169 ++++++++ .../ImmutableArrayBackedMapTest.java | 194 +++++++++ .../build.gradle.kts | 11 + .../pojoext/attributes/api/Attribute.java | 11 + .../attributes/api/AttributeDefault.java | 11 + .../attributes/api/AttributeDefaults.java | 12 + .../pojoext/attributes/api/Attributes.java | 12 + .../api/AttributesPojoWeavingProcessor.java | 75 ++++ .../pojoext/attributes/api/package-info.java | 4 + 28 files changed, 1627 insertions(+), 3 deletions(-) create mode 100644 tweed5-attributes-extension/build.gradle.kts create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesExtension.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesRelatedExtension.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/package-info.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/AttributesReadWriteFilterExtension.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/package-info.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/AttributesExtensionImpl.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/package-info.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java create mode 100644 tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/package-info.java create mode 100644 tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java create mode 100644 tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/UniqueSymbol.java create mode 100644 tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMap.java create mode 100644 tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedSet.java create mode 100644 tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMapTest.java create mode 100644 tweed5-weaver-pojo-attributes-extension/build.gradle.kts create mode 100644 tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attribute.java create mode 100644 tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefault.java create mode 100644 tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefaults.java create mode 100644 tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attributes.java create mode 100644 tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributesPojoWeavingProcessor.java create mode 100644 tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/package-info.java diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 92420d2..b4881a3 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("dev.panuszewski.typesafe-conventions") version "0.6.0" + id("dev.panuszewski.typesafe-conventions") version "0.7.3" } rootProject.name = "tweed5-conventions" diff --git a/settings.gradle.kts b/settings.gradle.kts index 89d0a38..55974c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ rootProject.name = "tweed5" include("test-utils") include("tweed5-annotation-inheritance") +include("tweed5-attributes-extension") include("tweed5-construct") include("tweed5-core") include("tweed5-default-extensions") @@ -13,4 +14,5 @@ include("tweed5-serde-hjson") include("tweed5-type-utils") include("tweed5-utils") include("tweed5-weaver-pojo") +include("tweed5-weaver-pojo-attributes-extension") include("tweed5-weaver-pojo-serde-extension") diff --git a/tweed5-attributes-extension/build.gradle.kts b/tweed5-attributes-extension/build.gradle.kts new file mode 100644 index 0000000..4659332 --- /dev/null +++ b/tweed5-attributes-extension/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("de.siphalor.tweed5.base-module") +} + +dependencies { + implementation(project(":tweed5-core")) + compileOnly(project(":tweed5-serde-extension")) + testImplementation(project(":tweed5-serde-extension")) + testImplementation(project(":tweed5-serde-hjson")) +} diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesExtension.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesExtension.java new file mode 100644 index 0000000..052ed1e --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesExtension.java @@ -0,0 +1,38 @@ +package de.siphalor.tweed5.attributesextension.api; + +import de.siphalor.tweed5.attributesextension.impl.AttributesExtensionImpl; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.extension.TweedExtension; +import org.jspecify.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +public interface AttributesExtension extends TweedExtension { + Class DEFAULT = AttributesExtensionImpl.class; + + static > Consumer attribute(String key, String value) { + return entry -> entry.container().extension(AttributesExtension.class) + .orElseThrow(() -> new IllegalStateException("No attributes extension registered")) + .setAttribute(entry, key, value); + } + + static > Consumer attributeDefault(String key, String value) { + return entry -> entry.container().extension(AttributesExtension.class) + .orElseThrow(() -> new IllegalStateException("No attributes extension registered")) + .setAttributeDefault(entry, key, value); + } + + default void setAttribute(ConfigEntry entry, String key, String value) { + setAttribute(entry, key, Collections.singletonList(value)); + } + void setAttribute(ConfigEntry entry, String key, List values); + default void setAttributeDefault(ConfigEntry entry, String key, String value) { + setAttributeDefault(entry, key, Collections.singletonList(value)); + } + void setAttributeDefault(ConfigEntry entry, String key, List values); + + List getAttributeValues(ConfigEntry entry, String key); + @Nullable String getAttributeValue(ConfigEntry entry, String key); +} diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesRelatedExtension.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesRelatedExtension.java new file mode 100644 index 0000000..3104edc --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/AttributesRelatedExtension.java @@ -0,0 +1,5 @@ +package de.siphalor.tweed5.attributesextension.api; + +public interface AttributesRelatedExtension { + default void afterAttributesInitialized() {} +} diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/package-info.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/package-info.java new file mode 100644 index 0000000..9790eca --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.attributesextension.api; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/AttributesReadWriteFilterExtension.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/AttributesReadWriteFilterExtension.java new file mode 100644 index 0000000..4051487 --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/AttributesReadWriteFilterExtension.java @@ -0,0 +1,13 @@ +package de.siphalor.tweed5.attributesextension.api.serde.filter; + +import de.siphalor.tweed5.attributesextension.impl.serde.filter.AttributesReadWriteFilterExtensionImpl; +import de.siphalor.tweed5.core.api.extension.TweedExtension; +import de.siphalor.tweed5.patchwork.api.Patchwork; + +public interface AttributesReadWriteFilterExtension extends TweedExtension { + Class DEFAULT = AttributesReadWriteFilterExtensionImpl.class; + + void markAttributeForFiltering(String key); + + void addFilter(Patchwork contextExtensionsData, String key, String value); +} diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/package-info.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/package-info.java new file mode 100644 index 0000000..1bf697b --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/api/serde/filter/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.attributesextension.api.serde.filter; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/AttributesExtensionImpl.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/AttributesExtensionImpl.java new file mode 100644 index 0000000..93e65f2 --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/AttributesExtensionImpl.java @@ -0,0 +1,224 @@ +package de.siphalor.tweed5.attributesextension.impl; + +import de.siphalor.tweed5.attributesextension.api.AttributesExtension; +import de.siphalor.tweed5.attributesextension.api.AttributesRelatedExtension; +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; +import de.siphalor.tweed5.core.api.extension.TweedExtension; +import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; +import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess; +import de.siphalor.tweed5.utils.api.collection.ImmutableArrayBackedMap; +import lombok.Data; +import lombok.var; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class AttributesExtensionImpl implements AttributesExtension { + private final ConfigContainer configContainer; + private final PatchworkPartAccess dataAccess; + private boolean initialized; + + public AttributesExtensionImpl(ConfigContainer configContainer, TweedExtensionSetupContext setupContext) { + this.configContainer = configContainer; + this.dataAccess = setupContext.registerEntryExtensionData(CustomEntryData.class); + } + + @Override + public String getId() { + return "attributes"; + } + + @Override + public void setAttribute(ConfigEntry entry, String key, List values) { + requireEditable(); + + var attributes = getOrCreateEditableAttributes(entry); + + attributes.compute(key, (k, existingValues) -> { + if (existingValues == null) { + return new ArrayList<>(values); + } else { + existingValues.addAll(values); + return existingValues; + } + }); + } + + @Override + public void setAttributeDefault(ConfigEntry entry, String key, List values) { + requireEditable(); + + var attributeDefaults = getOrCreateEditableAttributeDefaults(entry); + + attributeDefaults.compute(key, (k, existingValues) -> { + if (existingValues == null) { + return new ArrayList<>(values); + } else { + existingValues.addAll(values); + return existingValues; + } + }); + } + + private void requireEditable() { + if (initialized) { + throw new IllegalStateException("Attributes are only editable until the config has been initialized"); + } + } + + private Map> getOrCreateEditableAttributes(ConfigEntry entry) { + CustomEntryData data = getOrCreateCustomEntryData(entry); + var attributes = data.attributes(); + if (attributes == null) { + attributes = new HashMap<>(); + data.attributes(attributes); + } + return attributes; + } + + private Map> getOrCreateEditableAttributeDefaults(ConfigEntry entry) { + CustomEntryData data = getOrCreateCustomEntryData(entry); + var attributeDefaults = data.attributeDefaults(); + if (attributeDefaults == null) { + attributeDefaults = new HashMap<>(); + data.attributeDefaults(attributeDefaults); + } + return attributeDefaults; + } + + private CustomEntryData getOrCreateCustomEntryData(ConfigEntry entry) { + CustomEntryData customEntryData = entry.extensionsData().get(dataAccess); + if (customEntryData == null) { + customEntryData = new CustomEntryData(); + entry.extensionsData().set(dataAccess, customEntryData); + } + return customEntryData; + } + + @Override + public void initialize() { + configContainer.rootEntry().visitInOrder(new ConfigEntryVisitor() { + private final Deque>> defaults = new ArrayDeque<>( + Collections.singletonList(Collections.emptyMap()) + ); + + @Override + public boolean enterCompoundEntry(ConfigEntry entry) { + enterEntry(entry); + return true; + } + + @Override + public boolean enterCollectionEntry(ConfigEntry entry) { + enterEntry(entry); + return true; + } + + private void enterEntry(ConfigEntry entry) { + var data = entry.extensionsData().get(dataAccess); + var currentDefaults = defaults.getFirst(); + if (data == null) { + defaults.push(currentDefaults); + return; + } + var entryDefaults = data.attributeDefaults(); + data.attributeDefaults(null); + if (entryDefaults == null || entryDefaults.isEmpty()) { + defaults.push(currentDefaults); + return; + } + + defaults.push(mergeMapsAndSeal(currentDefaults, entryDefaults)); + + visitEntry(entry); + } + + @Override + public void leaveCompoundEntry(ConfigEntry entry) { + defaults.pop(); + } + + @Override + public void leaveCollectionEntry(ConfigEntry entry) { + defaults.pop(); + } + + @Override + public void visitEntry(ConfigEntry entry) { + var data = getOrCreateCustomEntryData(entry); + var currentDefaults = defaults.getFirst(); + if (data.attributes() == null || data.attributes().isEmpty()) { + data.attributes(currentDefaults); + } else { + data.attributes(mergeMapsAndSeal(currentDefaults, data.attributes())); + } + } + + private Map> mergeMapsAndSeal( + Map> base, + Map> overrides + ) { + if (overrides.isEmpty()) { + return ImmutableArrayBackedMap.ofEntries(base.entrySet()); + } else if (base.isEmpty()) { + return ImmutableArrayBackedMap.ofEntries(overrides.entrySet()); + } + + List>> entries = new ArrayList<>(base.size() + overrides.size()); + overrides.forEach((key, value) -> + entries.add(new AbstractMap.SimpleEntry<>(key, Collections.unmodifiableList(value))) + ); + base.forEach((key, value) -> { + if (!overrides.containsKey(key)) { + entries.add(new AbstractMap.SimpleEntry<>(key, Collections.unmodifiableList(value))); + } + }); + return ImmutableArrayBackedMap.ofEntries(entries); + } + }); + + initialized = true; + + for (TweedExtension extension : configContainer.extensions()) { + if (extension instanceof AttributesRelatedExtension) { + ((AttributesRelatedExtension) extension).afterAttributesInitialized(); + } + } + } + + @Override + public List getAttributeValues(ConfigEntry entry, String key) { + requireInitialized(); + CustomEntryData data = entry.extensionsData().get(dataAccess); + return Optional.ofNullable(data) + .map(CustomEntryData::attributes) + .map(attributes -> attributes.get(key)) + .orElse(Collections.emptyList()); + } + + @Override + public @Nullable String getAttributeValue(ConfigEntry entry, String key) { + requireInitialized(); + CustomEntryData data = entry.extensionsData().get(dataAccess); + return Optional.ofNullable(data) + .map(CustomEntryData::attributes) + .map(attributes -> attributes.get(key)) + .map(values -> values.isEmpty() ? null : values.get(values.size() - 1)) + .orElse(null); + } + + private void requireInitialized() { + if (!initialized) { + throw new IllegalStateException("Attributes are only available after the config has been initialized"); + } + } + + @Data + private static class CustomEntryData { + private @Nullable Map> attributes; + private @Nullable Map> attributeDefaults; + } +} diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/package-info.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/package-info.java new file mode 100644 index 0000000..308f489 --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/package-info.java @@ -0,0 +1,6 @@ +@ApiStatus.Internal +@NullMarked +package de.siphalor.tweed5.attributesextension.impl; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java new file mode 100644 index 0000000..834a40f --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java @@ -0,0 +1,404 @@ +package de.siphalor.tweed5.attributesextension.impl.serde.filter; + +import de.siphalor.tweed5.attributesextension.api.AttributesExtension; +import de.siphalor.tweed5.attributesextension.api.AttributesRelatedExtension; +import de.siphalor.tweed5.attributesextension.api.serde.filter.AttributesReadWriteFilterExtension; +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; +import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; +import de.siphalor.tweed5.core.api.middleware.Middleware; +import de.siphalor.tweed5.data.extension.api.TweedEntryReadException; +import de.siphalor.tweed5.data.extension.api.TweedEntryReader; +import de.siphalor.tweed5.data.extension.api.TweedEntryWriter; +import de.siphalor.tweed5.data.extension.api.TweedReadContext; +import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext; +import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension; +import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters; +import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls; +import de.siphalor.tweed5.dataapi.api.DelegatingTweedDataVisitor; +import de.siphalor.tweed5.dataapi.api.TweedDataReader; +import de.siphalor.tweed5.dataapi.api.TweedDataUnsupportedValueException; +import de.siphalor.tweed5.dataapi.api.TweedDataVisitor; +import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration; +import de.siphalor.tweed5.patchwork.api.Patchwork; +import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess; +import de.siphalor.tweed5.utils.api.UniqueSymbol; +import lombok.*; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class AttributesReadWriteFilterExtensionImpl + implements AttributesReadWriteFilterExtension, AttributesRelatedExtension, ReadWriteRelatedExtension { + private static final String ID = "attributes-serde-filter"; + private static final Set MIDDLEWARES_MUST_COME_BEFORE = new HashSet<>(Arrays.asList( + Middleware.DEFAULT_START, + "validation" + )); + private static final Set MIDDLEWARES_MUST_COME_AFTER = Collections.emptySet(); + private static final UniqueSymbol TWEED_DATA_NOTHING_VALUE = new UniqueSymbol("nothing (skip value)"); + + private final ConfigContainer configContainer; + private @Nullable AttributesExtension attributesExtension; + private final Set filterableAttributes = new HashSet<>(); + private final PatchworkPartAccess entryDataAccess; + private @Nullable PatchworkPartAccess readWriteContextDataAccess; + + public AttributesReadWriteFilterExtensionImpl(ConfigContainer configContainer, TweedExtensionSetupContext setupContext) { + this.configContainer = configContainer; + + entryDataAccess = setupContext.registerEntryExtensionData(EntryCustomData.class); + } + + @Override + public String getId() { + return ID; + } + + @Override + public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) { + readWriteContextDataAccess = context.registerReadWriteContextExtensionData(ReadWriteContextCustomData.class); + context.registerReaderMiddleware(new ReaderMiddleware()); + context.registerWriterMiddleware(new WriterMiddleware()); + } + + @Override + public void markAttributeForFiltering(String key) { + requireUninitialized(); + filterableAttributes.add(key); + } + + @Override + public void afterAttributesInitialized() { + attributesExtension = configContainer.extension(AttributesExtension.class) + .orElseThrow(() -> new IllegalStateException( + "You must register a " + AttributesExtension.class.getSimpleName() + + " before initializing the " + AttributesReadWriteFilterExtension.class.getSimpleName() + )); + + configContainer.rootEntry().visitInOrder(new ConfigEntryVisitor() { + private final Deque>> attributesCollectors = new ArrayDeque<>(); + + @Override + public void visitEntry(ConfigEntry entry) { + Map> currentAttributesCollector = attributesCollectors.peekFirst(); + if (currentAttributesCollector != null) { + for (String filterableAttribute : filterableAttributes) { + List values = attributesExtension.getAttributeValues(entry, filterableAttribute); + if (!values.isEmpty()) { + currentAttributesCollector.computeIfAbsent(filterableAttribute, k -> new HashSet<>()).addAll(values); + } + } + } + } + + @Override + public boolean enterCollectionEntry(ConfigEntry entry) { + attributesCollectors.push(new HashMap<>()); + visitEntry(entry); + return true; + } + + @Override + public void leaveCollectionEntry(ConfigEntry entry) { + leaveContainerEntry(entry); + } + + @Override + public boolean enterCompoundEntry(ConfigEntry entry) { + attributesCollectors.push(new HashMap<>()); + visitEntry(entry); + return true; + } + + @Override + public void leaveCompoundEntry(ConfigEntry entry) { + leaveContainerEntry(entry); + } + + private void leaveContainerEntry(ConfigEntry entry) { + Map> entryAttributesCollector = attributesCollectors.pop(); + entry.extensionsData().set(entryDataAccess, new EntryCustomData(entryAttributesCollector)); + + Map> outerAttributesCollector = attributesCollectors.peekFirst(); + if (outerAttributesCollector != null) { + entryAttributesCollector.forEach((key, value) -> + outerAttributesCollector.computeIfAbsent(key, k -> new HashSet<>()).addAll(value) + ); + } + } + }); + } + + @Override + public void addFilter(Patchwork contextExtensionsData, String key, String value) { + requireInitialized(); + + var contextCustomData = getOrCreateReadWriteContextCustomData(contextExtensionsData); + addFilterToRWContextData(key, value, contextCustomData); + } + + private ReadWriteContextCustomData getOrCreateReadWriteContextCustomData(Patchwork patchwork) { + assert readWriteContextDataAccess != null; + + ReadWriteContextCustomData readWriteContextCustomData = patchwork.get(readWriteContextDataAccess); + if (readWriteContextCustomData == null) { + readWriteContextCustomData = new ReadWriteContextCustomData(); + patchwork.set(readWriteContextDataAccess, readWriteContextCustomData); + } + + return readWriteContextCustomData; + } + + private void addFilterToRWContextData(String key, String value, ReadWriteContextCustomData contextCustomData) { + if (filterableAttributes.contains(key)) { + contextCustomData.attributeFilters().computeIfAbsent(key, k -> new HashSet<>()).add(value); + } else { + throw new IllegalArgumentException("The attribute " + key + " has not been marked for filtering"); + } + } + + private void requireUninitialized() { + if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) >= 0) { + throw new IllegalStateException( + "Attribute optimization is only editable until the config has been initialized" + ); + } + } + + private void requireInitialized() { + if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) < 0) { + throw new IllegalStateException("Config container must already be initialized"); + } + } + + private class ReaderMiddleware implements Middleware> { + @Override + public String id() { + return ID; + } + + @Override + public Set mustComeBefore() { + return MIDDLEWARES_MUST_COME_BEFORE; + } + + @Override + public Set mustComeAfter() { + return MIDDLEWARES_MUST_COME_AFTER; + } + + @Override + public TweedEntryReader process(TweedEntryReader inner) { + assert readWriteContextDataAccess != null; + //noinspection unchecked + TweedEntryReader> innerCasted + = (TweedEntryReader>) inner; + + return new TweedEntryReader<@Nullable Object, ConfigEntry>() { + @Override + public @Nullable Object read( + TweedDataReader reader, + ConfigEntry entry, + TweedReadContext context + ) throws TweedEntryReadException { + ReadWriteContextCustomData contextData = context.extensionsData().get(readWriteContextDataAccess); + if (contextData == null || doFiltersMatch(entry, contextData)) { + return innerCasted.read(reader, entry, context); + } + TweedEntryReaderWriterImpls.NOOP_READER_WRITER.read(reader, entry, context); + // TODO: this should result in a noop instead of a null value + return null; + } + }; + } + } + + private class WriterMiddleware implements Middleware> { + @Override + public String id() { + return ID; + } + + @Override + public Set mustComeBefore() { + return MIDDLEWARES_MUST_COME_BEFORE; + } + + @Override + public Set mustComeAfter() { + return MIDDLEWARES_MUST_COME_AFTER; + } + + @Override + public TweedEntryWriter process(TweedEntryWriter inner) { + assert readWriteContextDataAccess != null; + //noinspection unchecked + TweedEntryWriter> innerCasted + = (TweedEntryWriter>) inner; + + return (TweedEntryWriter<@Nullable Object, @NonNull ConfigEntry<@Nullable Object>>) + (writer, value, entry, context) -> { + ReadWriteContextCustomData contextData = context.extensionsData() + .get(readWriteContextDataAccess); + if (contextData == null || contextData.attributeFilters().isEmpty()) { + innerCasted.write(writer, value, entry, context); + return; + } + + if (!contextData.writerInstalled()) { + writer = new MapEntryKeyDeferringWriter(writer); + contextData.writerInstalled(true); + } + + if (doFiltersMatch(entry, contextData)) { + innerCasted.write(writer, value, entry, context); + } else { + try { + writer.visitValue(TWEED_DATA_NOTHING_VALUE); + } catch (TweedDataUnsupportedValueException ignored) {} + } + }; + } + } + + private boolean doFiltersMatch(ConfigEntry entry, ReadWriteContextCustomData contextData) { + assert attributesExtension != null; + + EntryCustomData entryCustomData = entry.extensionsData().get(entryDataAccess); + if (entryCustomData == null) { + for (Map.Entry> attributeFilter : contextData.attributeFilters().entrySet()) { + List values = attributesExtension.getAttributeValues(entry, attributeFilter.getKey()); + //noinspection SlowListContainsAll + if (!values.containsAll(attributeFilter.getValue())) { + return false; + } + } + return true; + } + for (Map.Entry> attributeFilter : contextData.attributeFilters().entrySet()) { + Set values = entryCustomData.optimizedAttributes() + .getOrDefault(attributeFilter.getKey(), Collections.emptySet()); + + if (!values.containsAll(attributeFilter.getValue())) { + return false; + } + } + return true; + } + + private static class MapEntryKeyDeferringWriter extends DelegatingTweedDataVisitor { + private final Deque mapContext = new ArrayDeque<>(); + private final Deque preDecorationQueue = new ArrayDeque<>(); + private final Deque postDecorationQueue = new ArrayDeque<>(); + private @Nullable String mapEntryKey; + + protected MapEntryKeyDeferringWriter(TweedDataVisitor delegate) { + super(delegate); + mapContext.push(false); + } + + @Override + public void visitMapStart() { + beforeValueWrite(); + mapContext.push(true); + delegate.visitMapStart(); + } + + @Override + public void visitMapEntryKey(String key) { + if (mapEntryKey != null) { + throw new IllegalStateException("The map entry key has already been visited"); + } else { + mapEntryKey = key; + } + } + + @Override + public void visitMapEnd() { + if (mapEntryKey != null) { + throw new IllegalArgumentException("Reached end of map while waiting for value for key " + mapEntryKey); + } + + TweedDataDecoration decoration; + while ((decoration = preDecorationQueue.pollFirst()) != null) { + super.visitDecoration(decoration); + } + + super.visitMapEnd(); + mapContext.pop(); + } + + @Override + public void visitListStart() { + beforeValueWrite(); + mapContext.push(false); + delegate.visitListStart(); + } + + @Override + public void visitListEnd() { + super.visitListEnd(); + mapContext.pop(); + } + + @Override + public void visitValue(@Nullable Object value) throws TweedDataUnsupportedValueException { + if (value == TWEED_DATA_NOTHING_VALUE) { + preDecorationQueue.clear(); + postDecorationQueue.clear(); + mapEntryKey = null; + return; + } + super.visitValue(value); + } + + @Override + public void visitDecoration(TweedDataDecoration decoration) { + if (Boolean.TRUE.equals(mapContext.peekFirst())) { + if (mapEntryKey == null) { + preDecorationQueue.addLast(decoration); + } else { + postDecorationQueue.addLast(decoration); + } + } else { + super.visitDecoration(decoration); + } + } + + @Override + protected void beforeValueWrite() { + super.beforeValueWrite(); + + if (mapEntryKey != null) { + TweedDataDecoration decoration; + while ((decoration = preDecorationQueue.pollFirst()) != null) { + super.visitDecoration(decoration); + } + + super.visitMapEntryKey(mapEntryKey); + mapEntryKey = null; + + while ((decoration = postDecorationQueue.pollFirst()) != null) { + super.visitDecoration(decoration); + } + } + } + } + + @Getter + @RequiredArgsConstructor + private static class EntryCustomData { + private final Map> optimizedAttributes; + } + + @Getter + @Setter + private static class ReadWriteContextCustomData { + private final Map> attributeFilters = new HashMap<>(); + private boolean writerInstalled; + } +} diff --git a/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/package-info.java b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/package-info.java new file mode 100644 index 0000000..45b5601 --- /dev/null +++ b/tweed5-attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/package-info.java @@ -0,0 +1,6 @@ +@ApiStatus.Internal +@NullMarked +package de.siphalor.tweed5.attributesextension.impl.serde.filter; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java b/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java new file mode 100644 index 0000000..a0ea8e9 --- /dev/null +++ b/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java @@ -0,0 +1,183 @@ +package de.siphalor.tweed5.attributesextension.impl.serde.filter; + +import de.siphalor.tweed5.attributesextension.api.AttributesExtension; +import de.siphalor.tweed5.attributesextension.api.serde.filter.AttributesReadWriteFilterExtension; +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; +import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry; +import de.siphalor.tweed5.core.impl.DefaultConfigContainer; +import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; +import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl; +import de.siphalor.tweed5.data.extension.api.ReadWriteExtension; +import de.siphalor.tweed5.data.hjson.HjsonLexer; +import de.siphalor.tweed5.data.hjson.HjsonReader; +import de.siphalor.tweed5.data.hjson.HjsonWriter; +import org.jspecify.annotations.NullUnmarked; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static de.siphalor.tweed5.attributesextension.api.AttributesExtension.attribute; +import static de.siphalor.tweed5.attributesextension.api.AttributesExtension.attributeDefault; +import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.*; +import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.compoundReaderWriter; +import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.stringReaderWriter; +import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.map; + +@NullUnmarked +class AttributesReadWriteFilterExtensionImplTest { + ConfigContainer> configContainer; + CompoundConfigEntry> rootEntry; + SimpleConfigEntry firstEntry; + SimpleConfigEntry secondEntry; + CompoundConfigEntry> nestedEntry; + SimpleConfigEntry nestedFirstEntry; + SimpleConfigEntry nestedSecondEntry; + AttributesReadWriteFilterExtension attributesReadWriteFilterExtension; + + @BeforeEach + void setUp() { + configContainer = new DefaultConfigContainer<>(); + configContainer.registerExtension(AttributesReadWriteFilterExtensionImpl.class); + configContainer.registerExtension(ReadWriteExtension.class); + configContainer.registerExtension(AttributesExtension.class); + configContainer.finishExtensionSetup(); + + nestedFirstEntry = new SimpleConfigEntryImpl<>(configContainer, String.class) + .apply(entryReaderWriter(stringReaderWriter())) + .apply(attribute("type", "a")) + .apply(attribute("sync", "true")); + nestedSecondEntry = new SimpleConfigEntryImpl<>(configContainer, String.class) + .apply(entryReaderWriter(stringReaderWriter())); + //noinspection unchecked + nestedEntry = new StaticMapCompoundConfigEntryImpl<>( + configContainer, + (Class>) (Class) Map.class, + HashMap::new, + sequencedMap(List.of(entry("first", nestedFirstEntry), entry("second", nestedSecondEntry))) + ) + .apply(entryReaderWriter(compoundReaderWriter())) + .apply(attributeDefault("type", "c")); + + firstEntry = new SimpleConfigEntryImpl<>(configContainer, String.class) + .apply(entryReaderWriter(stringReaderWriter())) + .apply(attribute("type", "a")) + .apply(attribute("sync", "true")); + secondEntry = new SimpleConfigEntryImpl<>(configContainer, String.class) + .apply(entryReaderWriter(stringReaderWriter())); + + //noinspection unchecked + rootEntry = new StaticMapCompoundConfigEntryImpl<>( + configContainer, + (Class>) (Class) Map.class, + HashMap::new, + sequencedMap(List.of( + entry("first", firstEntry), + entry("second", secondEntry), + entry("nested", nestedEntry) + )) + ) + .apply(entryReaderWriter(compoundReaderWriter())) + .apply(attributeDefault("type", "b")); + + configContainer.attachTree(rootEntry); + + attributesReadWriteFilterExtension = configContainer.extension(AttributesReadWriteFilterExtension.class) + .orElseThrow(); + + attributesReadWriteFilterExtension.markAttributeForFiltering("type"); + attributesReadWriteFilterExtension.markAttributeForFiltering("sync"); + + configContainer.initialize(); + } + + @ParameterizedTest + @CsvSource(quoteCharacter = '`', textBlock = """ + a,`{ + \tfirst: 1st + \tnested: { + \t\tfirst: n 1st + \t} + } + ` + b,`{ + \tsecond: 2nd + } + ` + c,`{ + \tnested: { + \t\tsecond: n 2nd + \t} + } + ` + """ + ) + void writeWithType(String type, String serialized) { + var writer = new StringWriter(); + configContainer.rootEntry().apply(write( + new HjsonWriter(writer, new HjsonWriter.Options()), + Map.of("first", "1st", "second", "2nd", "nested", Map.of("first", "n 1st", "second", "n 2nd")), + patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "type" , type) + )); + + assertThat(writer.toString()).isEqualTo(serialized); + } + + @Test + void writeWithSync() { + var writer = new StringWriter(); + configContainer.rootEntry().apply(write( + new HjsonWriter(writer, new HjsonWriter.Options()), + Map.of("first", "1st", "second", "2nd", "nested", Map.of("first", "n 1st", "second", "n 2nd")), + patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "sync" , "true") + )); + + assertThat(writer.toString()).isEqualTo(""" + { + \tfirst: 1st + \tnested: { + \t\tfirst: n 1st + \t} + } + """); + } + + @Test + void readWithType() { + HjsonReader reader = new HjsonReader(new HjsonLexer(new StringReader(""" + { + first: 1st + second: 2nd + nested: { + first: n 1st + second: n 2nd + } + } + """))); + Map readValue = configContainer.rootEntry().call(read( + reader, + patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "type", "a") + )); + + assertThat(readValue) + .containsEntry("first", "1st") + .containsEntry("second", null) + .hasEntrySatisfying( + "nested", nested -> assertThat(nested) + .asInstanceOf(map(String.class, Object.class)) + .containsEntry("first", "n 1st") + .containsEntry("second", null) + ); + } +} diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/extension/TweedExtension.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/extension/TweedExtension.java index 256e9a6..a574169 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/extension/TweedExtension.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/extension/TweedExtension.java @@ -15,6 +15,9 @@ public interface TweedExtension { default void extensionsFinalized() { } + default void initialize() { + } + default void initEntry(ConfigEntry configEntry) { } } 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 1116464..7bc9642 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 @@ -217,6 +217,10 @@ public class DefaultConfigContainer implements ConfigContainer { public void initialize() { requireSetupPhase(ConfigContainerSetupPhase.TREE_ATTACHED); + for (TweedExtension extension : extensions()) { + extension.initialize(); + } + assert rootEntry != null; rootEntry.visitInOrder(entry -> { for (TweedExtension extension : extensions()) { diff --git a/tweed5-core/src/test/java/de/siphalor/tweed5/core/impl/sort/AcyclicGraphSorterTest.java b/tweed5-core/src/test/java/de/siphalor/tweed5/core/impl/sort/AcyclicGraphSorterTest.java index 2ed4986..4d631a0 100644 --- a/tweed5-core/src/test/java/de/siphalor/tweed5/core/impl/sort/AcyclicGraphSorterTest.java +++ b/tweed5-core/src/test/java/de/siphalor/tweed5/core/impl/sort/AcyclicGraphSorterTest.java @@ -9,6 +9,13 @@ import static org.junit.jupiter.api.Assertions.*; class AcyclicGraphSorterTest { + @Test + void trivialSort() { + AcyclicGraphSorter sorter = new AcyclicGraphSorter(2); + sorter.addEdge(0, 1); + assertArrayEquals(new int[]{ 0, 1 }, assertDoesNotThrow(sorter::sort)); + } + @Test void sort1() { AcyclicGraphSorter sorter = new AcyclicGraphSorter(4); @@ -73,4 +80,4 @@ class AcyclicGraphSorterTest { AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort); assertEquals(Arrays.asList(0, 1), exception.cycleIndeces()); } -} \ No newline at end of file +} diff --git a/tweed5-serde-hjson/src/main/java/de/siphalor/tweed5/data/hjson/HjsonWriter.java b/tweed5-serde-hjson/src/main/java/de/siphalor/tweed5/data/hjson/HjsonWriter.java index 4a44b84..9a49375 100644 --- a/tweed5-serde-hjson/src/main/java/de/siphalor/tweed5/data/hjson/HjsonWriter.java +++ b/tweed5-serde-hjson/src/main/java/de/siphalor/tweed5/data/hjson/HjsonWriter.java @@ -15,6 +15,7 @@ import java.util.regex.Pattern; public class HjsonWriter implements TweedDataVisitor { private static final int PREFILL_INDENT = 10; private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\\n|\\r\\n"); + private static final Pattern NUMBER_PATTERN = Pattern.compile("^-?\\d+(?:\\.\\d*)?(?:[eE][+-]?\\d+)?$"); private final Writer writer; private final Options options; @@ -101,8 +102,11 @@ public class HjsonWriter implements TweedDataVisitor { if (value.isEmpty() || "true".equals(value) || "false".equals(value) || "null".equals(value)) { return HjsonStringType.INLINE_DOUBLE_QUOTE; } + if (NUMBER_PATTERN.matcher(value).matches()) { + return HjsonStringType.INLINE_DOUBLE_QUOTE; + } int firstCodePoint = value.codePointAt(0); - if (Character.isDigit(firstCodePoint) || Character.isWhitespace(firstCodePoint)) { + if (Character.isWhitespace(firstCodePoint)) { return HjsonStringType.INLINE_DOUBLE_QUOTE; } int lastCodePoint = value.codePointBefore(value.length()); diff --git a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/UniqueSymbol.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/UniqueSymbol.java new file mode 100644 index 0000000..55df900 --- /dev/null +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/UniqueSymbol.java @@ -0,0 +1,13 @@ +package de.siphalor.tweed5.utils.api; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class UniqueSymbol { + private final String displayName; + + @Override + public String toString() { + return "UniqueSymbol@" + System.identityHashCode(this) + "{" + displayName + "}"; + } +} diff --git a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMap.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMap.java new file mode 100644 index 0000000..cb1172f --- /dev/null +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMap.java @@ -0,0 +1,195 @@ +package de.siphalor.tweed5.utils.api.collection; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.stream.IntStream; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class ImmutableArrayBackedMap implements SortedMap { + private final K[] keys; + private final V[] values; + + @SuppressWarnings("unchecked") + public static , V> SortedMap ofEntries(Collection> entries) { + if (entries.isEmpty()) { + return Collections.emptySortedMap(); + } + + Entry any = entries.iterator().next(); + int size = entries.size(); + K[] keys = (K[]) Array.newInstance(any.getKey().getClass(), size); + V[] values = (V[]) Array.newInstance(any.getValue().getClass(), size); + + int i = 0; + Iterator> iterator = entries.stream().sorted(Entry.comparingByKey()).iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + i++; + } + + return new ImmutableArrayBackedMap<>(keys, values); + } + + @Override + public @Nullable Comparator comparator() { + return null; + } + + @Override + public @NonNull SortedMap subMap(K fromKey, K toKey) { + int from = findKey(fromKey); + if (from < 0) { + from = -from - 1; + } + if (from == 0) { + return headMap(toKey); + } else if (from >= keys.length) { + return Collections.emptySortedMap(); + } + int to = findKey(toKey, from + 1); + if (to < 0) { + to = -to - 1; + } + if (to == keys.length) { + return this; + } else if (to == from) { + return Collections.emptySortedMap(); + } + return new ImmutableArrayBackedMap<>(Arrays.copyOfRange(keys, from, to), Arrays.copyOfRange(values, from, to)); + } + + @Override + public @NonNull SortedMap headMap(K toKey) { + int to = findKey(toKey); + if (to < 0) { + to = -to - 1; + } + if (to == keys.length) { + return this; + } else if (to == 0) { + return Collections.emptySortedMap(); + } + return new ImmutableArrayBackedMap<>(Arrays.copyOf(keys, to), Arrays.copyOf(values, to)); + } + + @Override + public @NonNull SortedMap tailMap(K fromKey) { + int from = findKey(fromKey); + if (from < 0) { + from = -from - 1; + } + if (from == 0) { + return this; + } else if (from >= keys.length) { + return Collections.emptySortedMap(); + } + return new ImmutableArrayBackedMap<>( + Arrays.copyOfRange(keys, from, keys.length), + Arrays.copyOfRange(values, from, keys.length) + ); + } + + @Override + public K firstKey() { + return keys[0]; + } + + @Override + public K lastKey() { + return keys[keys.length - 1]; + } + + @Override + public int size() { + return keys.length; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(Object key) { + return findKey(key) >= 0; + } + + @Override + public boolean containsValue(Object value) { + return Arrays.binarySearch(values, value) >= 0; + } + + @Override + public @Nullable V get(Object key) { + int index = findKey(key); + if (index < 0) { + return null; + } + return values[index]; + } + + @Override + public @Nullable V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(@NonNull Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() { + return new ImmutableArrayBackedSet<>(keys); + } + + @Override + public Collection values() { + return new AbstractList() { + @Override + public V get(int index) { + return values[index]; + } + + @Override + public int size() { + return values.length; + } + }; + } + + @Override + public Set> entrySet() { + //noinspection unchecked + return new ImmutableArrayBackedSet>( + IntStream.range(0, keys.length) + .mapToObj(index -> new AbstractMap.SimpleEntry<>(keys[index], values[index])) + .toArray(Entry[]::new) + ); + } + + private int findKey(Object key) { + return Arrays.binarySearch(keys, key); + } + + private int findKey(Object key, int from) { + return Arrays.binarySearch(keys, from, keys.length, key); + } +} diff --git a/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedSet.java b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedSet.java new file mode 100644 index 0000000..869a32e --- /dev/null +++ b/tweed5-utils/src/main/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedSet.java @@ -0,0 +1,169 @@ +package de.siphalor.tweed5.utils.api.collection; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.lang.reflect.Array; +import java.util.*; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class ImmutableArrayBackedSet implements SortedSet { + private final T[] values; + + public static > SortedSet of(Collection collection) { + if (collection.isEmpty()) { + return Collections.emptySortedSet(); + } + T first = collection.iterator().next(); + + //noinspection unchecked + return new ImmutableArrayBackedSet<>( + collection.stream().sorted().toArray(length -> (T[]) Array.newInstance(first.getClass(), length)) + ); + } + + @SafeVarargs + public static > SortedSet of(T... values) { + if (values.length == 0) { + return Collections.emptySortedSet(); + } + Arrays.sort(values); + return new ImmutableArrayBackedSet<>(values); + } + + @Override + public @Nullable Comparator comparator() { + return null; + } + + @Override + public @NonNull SortedSet subSet(T fromElement, T toElement) { + int from = Arrays.binarySearch(values, fromElement); + if (from < 0) { + from = -from - 1; + } + if (from == 0) { + return headSet(toElement); + } + int to = Arrays.binarySearch(values, toElement); + if (to < 0) { + to = -to - 1; + } + if (to == values.length) { + return this; + } + return new ImmutableArrayBackedSet<>(Arrays.copyOfRange(values, from, to)); + } + + @Override + public @NonNull SortedSet headSet(T toElement) { + int to = Arrays.binarySearch(values, toElement); + if (to < 0) { + to = -to - 1; + } + if (to == values.length) { + return this; + } + return new ImmutableArrayBackedSet<>(Arrays.copyOfRange(values, 0, to)); + } + + @Override + public @NonNull SortedSet tailSet(T fromElement) { + int from = Arrays.binarySearch(values, fromElement); + if (from < 0) { + from = -from - 1; + } + if (from == 0) { + return this; + } + return new ImmutableArrayBackedSet<>(Arrays.copyOfRange(values, from, values.length)); + } + + @Override + public T first() { + return values[0]; + } + + @Override + public T last() { + return values[values.length - 1]; + } + + @Override + public int size() { + return values.length; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean contains(Object o) { + return Arrays.binarySearch(values, o) >= 0; + } + + @Override + public @NonNull Iterator iterator() { + return Arrays.stream(values).iterator(); + } + + @Override + public @NonNull Object[] toArray() { + return Arrays.copyOf(values, values.length); + } + + @Override + public @NonNull T1[] toArray(@NonNull T1[] a) { + // basically copied from ArrayList#toArray(Object[]) + if (a.length < values.length) { + //noinspection unchecked + return (T1[]) toArray(); + } + //noinspection SuspiciousSystemArraycopy + System.arraycopy(values, 0, a, 0, values.length); + if (a.length > values.length) { + //noinspection DataFlowIssue + a[values.length] = null; + } + return a; + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(@NonNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(@NonNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(@NonNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(@NonNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } +} diff --git a/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMapTest.java b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMapTest.java new file mode 100644 index 0000000..12a4437 --- /dev/null +++ b/tweed5-utils/src/test/java/de/siphalor/tweed5/utils/api/collection/ImmutableArrayBackedMapTest.java @@ -0,0 +1,194 @@ +package de.siphalor.tweed5.utils.api.collection; + +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ImmutableArrayBackedMapTest { + + @Test + void ofEntriesEmpty() { + assertThat(ImmutableArrayBackedMap.ofEntries(Collections.emptyList())).isSameAs(Collections.emptySortedMap()); + } + + @Test + void ofEntries() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10), + Map.entry(3, 30) + )); + assertThat(map).containsExactly(Map.entry(1, 10), Map.entry(2, 20), Map.entry(3, 30)); + } + + @Test + void comparator() { + assertThat(ImmutableArrayBackedMap.ofEntries(List.of(Map.entry(1, 10))).comparator()).isNull(); + } + + @Test + void subMap() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(40, 400), + Map.entry(20, 200), + Map.entry(10, 100), + Map.entry(30, 300) + )); + + assertThat(map.subMap(1, 100)).isSameAs(map); + assertThat(map.subMap(10, 40)).containsExactly(Map.entry(10, 100), Map.entry(20, 200), Map.entry(30, 300)); + assertThat(map.subMap(0, 20)).containsExactly(Map.entry(10, 100)); + assertThat(map.subMap(0, 1)).isEmpty(); + assertThat(map.subMap(41, 100)).isEmpty(); + } + + @Test + void headMap() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(40, 400), + Map.entry(20, 200), + Map.entry(10, 100), + Map.entry(30, 300) + )); + + assertThat(map.headMap(41)).isSameAs(map); + assertThat(map.headMap(40)).containsExactly(Map.entry(10, 100), Map.entry(20, 200), Map.entry(30, 300)); + assertThat(map.headMap(10)).isEmpty(); + assertThat(map.headMap(0)).isEmpty(); + } + + @Test + void tailMap() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(40, 400), + Map.entry(20, 200), + Map.entry(10, 100), + Map.entry(30, 300) + )); + + assertThat(map.tailMap(0)).isSameAs(map); + assertThat(map.tailMap(10)).isSameAs(map); + assertThat(map.tailMap(30)).containsExactly(Map.entry(30, 300), Map.entry(40, 400)); + assertThat(map.tailMap(41)).isEmpty(); + } + + @Test + void firstKey() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThat(map.firstKey()).isEqualTo(1); + } + + @Test + void lastKey() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThat(map.lastKey()).isEqualTo(2); + } + + @Test + void size() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThat(map).hasSize(2); + assertThat(map).isNotEmpty(); + } + + @Test + void containsKey() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThat(map.containsKey(0)).isFalse(); + assertThat(map.containsKey(1)).isTrue(); + assertThat(map.containsKey(2)).isTrue(); + assertThat(map.containsKey(3)).isFalse(); + } + + @Test + void containsValue() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThat(map.containsValue(0)).isFalse(); + assertThat(map.containsValue(10)).isTrue(); + assertThat(map.containsValue(20)).isTrue(); + assertThat(map.containsValue(30)).isFalse(); + } + + @Test + void get() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThat(map.get(0)).isNull(); + assertThat(map.get(1)).isEqualTo(10); + assertThat(map.get(2)).isEqualTo(20); + assertThat(map.get(3)).isNull(); + } + + @Test + void unsupported() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10) + )); + assertThatThrownBy(() -> map.put(5, 50)).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.putAll(Map.of(3, 30, 5, 50))).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.remove(2)).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(map::clear).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void keySet() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 20), + Map.entry(1, 10), + Map.entry(3, 30) + )); + Set keySet = map.keySet(); + assertThat(keySet).containsExactly(1, 2, 3).hasSize(3); + assertThatThrownBy(() -> keySet.add(4)).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> keySet.remove(2)).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(keySet::clear).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void values() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 40), + Map.entry(1, 90), + Map.entry(3, 10) + )); + Collection values = map.values(); + assertThat(values).containsExactly(90, 40, 10).hasSize(3); + assertThatThrownBy(() -> values.add(50)).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> values.remove(10)).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(values::clear).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void entrySet() { + SortedMap map = ImmutableArrayBackedMap.ofEntries(List.of( + Map.entry(2, 40), + Map.entry(1, 90), + Map.entry(3, 10) + )); + Set> entrySet = map.entrySet(); + assertThat(entrySet).containsExactly(Map.entry(1, 90), Map.entry(2, 40), Map.entry(3, 10)).hasSize(3); + assertThatThrownBy(() -> entrySet.add(Map.entry(4, 50))).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> entrySet.remove(Map.entry(1, 90))).isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/tweed5-weaver-pojo-attributes-extension/build.gradle.kts b/tweed5-weaver-pojo-attributes-extension/build.gradle.kts new file mode 100644 index 0000000..84863da --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("de.siphalor.tweed5.base-module") +} + +dependencies { + api(project(":tweed5-weaver-pojo")) + api(project(":tweed5-attributes-extension")) + + testImplementation(project(":tweed5-default-extensions")) + testImplementation(project(":tweed5-serde-hjson")) +} diff --git a/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attribute.java b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attribute.java new file mode 100644 index 0000000..b2cd749 --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attribute.java @@ -0,0 +1,11 @@ +package de.siphalor.tweed5.weaver.pojoext.attributes.api; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE}) +@Repeatable(Attributes.class) +public @interface Attribute { + String key(); + String[] values(); +} diff --git a/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefault.java b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefault.java new file mode 100644 index 0000000..bde2c32 --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefault.java @@ -0,0 +1,11 @@ +package de.siphalor.tweed5.weaver.pojoext.attributes.api; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE}) +@Repeatable(AttributeDefaults.class) +public @interface AttributeDefault { + String key(); + String[] defaultValue(); +} diff --git a/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefaults.java b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefaults.java new file mode 100644 index 0000000..1fe6a90 --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributeDefaults.java @@ -0,0 +1,12 @@ +package de.siphalor.tweed5.weaver.pojoext.attributes.api; + +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, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE}) +public @interface AttributeDefaults { + AttributeDefault[] value(); +} diff --git a/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attributes.java b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attributes.java new file mode 100644 index 0000000..cd9bc78 --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/Attributes.java @@ -0,0 +1,12 @@ +package de.siphalor.tweed5.weaver.pojoext.attributes.api; + +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, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE}) +public @interface Attributes { + Attribute[] value(); +} diff --git a/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributesPojoWeavingProcessor.java b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributesPojoWeavingProcessor.java new file mode 100644 index 0000000..ae36025 --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/AttributesPojoWeavingProcessor.java @@ -0,0 +1,75 @@ +package de.siphalor.tweed5.weaver.pojoext.attributes.api; + +import de.siphalor.tweed5.attributesextension.api.AttributesExtension; +import de.siphalor.tweed5.core.api.container.ConfigContainer; +import de.siphalor.tweed5.core.api.entry.ConfigEntry; +import de.siphalor.tweed5.typeutils.api.type.ActualType; +import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension; +import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext; +import lombok.var; +import org.jetbrains.annotations.ApiStatus; + +import java.util.*; +import java.util.function.Function; + +public class AttributesPojoWeavingProcessor implements TweedPojoWeavingExtension { + AttributesExtension attributesExtension; + + @ApiStatus.Internal + public AttributesPojoWeavingProcessor(ConfigContainer configContainer) { + attributesExtension = configContainer.extension(AttributesExtension.class) + .orElseThrow(() -> new IllegalStateException( + "You must register a " + AttributesExtension.class.getSimpleName() + + " to use the " + getClass().getSimpleName() + )); + } + + @Override + public void setup(SetupContext context) { + + } + + @Override + public void afterWeaveEntry(ActualType valueType, ConfigEntry configEntry, WeavingContext context) { + var attributeAnnotations = context.annotations().getAnnotationsByType(Attribute.class); + var attributes = collectAttributesFromAnnotations(attributeAnnotations, Attribute::key, Attribute::values); + attributes.forEach((key, values) -> attributesExtension.setAttribute(configEntry, key, values)); + + var attributeDefaultAnnotations = context.annotations().getAnnotationsByType(AttributeDefault.class); + var attributeDefaults = collectAttributesFromAnnotations( + attributeDefaultAnnotations, + AttributeDefault::key, + AttributeDefault::defaultValue + ); + attributeDefaults.forEach((key, values) -> attributesExtension.setAttributeDefault(configEntry, key, values)); + } + + private Map> collectAttributesFromAnnotations( + T[] annotations, + Function keyGetter, + Function valueGetter + ) { + if (annotations.length == 0) { + return Collections.emptyMap(); + } + + Map> attributes; + if (annotations.length == 1) { + return Collections.singletonMap( + keyGetter.apply(annotations[0]), + Arrays.asList(valueGetter.apply(annotations[0])) + ); + } else if (annotations.length <= 12) { + attributes = new TreeMap<>(); + } else { + attributes = new HashMap<>(); + } + + for (T annotation : annotations) { + attributes.computeIfAbsent(keyGetter.apply(annotation), k -> new ArrayList<>()) + .addAll(Arrays.asList(valueGetter.apply(annotation))); + } + + return attributes; + } +} diff --git a/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/package-info.java b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/package-info.java new file mode 100644 index 0000000..55cd96b --- /dev/null +++ b/tweed5-weaver-pojo-attributes-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/attributes/api/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.weaver.pojoext.attributes.api; + +import org.jspecify.annotations.NullMarked;