[attributes] Introduce attributes extensions
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.panuszewski.typesafe-conventions") version "0.6.0"
|
id("dev.panuszewski.typesafe-conventions") version "0.7.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "tweed5-conventions"
|
rootProject.name = "tweed5-conventions"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ rootProject.name = "tweed5"
|
|||||||
|
|
||||||
include("test-utils")
|
include("test-utils")
|
||||||
include("tweed5-annotation-inheritance")
|
include("tweed5-annotation-inheritance")
|
||||||
|
include("tweed5-attributes-extension")
|
||||||
include("tweed5-construct")
|
include("tweed5-construct")
|
||||||
include("tweed5-core")
|
include("tweed5-core")
|
||||||
include("tweed5-default-extensions")
|
include("tweed5-default-extensions")
|
||||||
@@ -13,4 +14,5 @@ include("tweed5-serde-hjson")
|
|||||||
include("tweed5-type-utils")
|
include("tweed5-type-utils")
|
||||||
include("tweed5-utils")
|
include("tweed5-utils")
|
||||||
include("tweed5-weaver-pojo")
|
include("tweed5-weaver-pojo")
|
||||||
|
include("tweed5-weaver-pojo-attributes-extension")
|
||||||
include("tweed5-weaver-pojo-serde-extension")
|
include("tweed5-weaver-pojo-serde-extension")
|
||||||
|
|||||||
10
tweed5-attributes-extension/build.gradle.kts
Normal file
10
tweed5-attributes-extension/build.gradle.kts
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
@@ -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<? extends AttributesExtension> DEFAULT = AttributesExtensionImpl.class;
|
||||||
|
|
||||||
|
static <C extends ConfigEntry<?>> Consumer<C> attribute(String key, String value) {
|
||||||
|
return entry -> entry.container().extension(AttributesExtension.class)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("No attributes extension registered"))
|
||||||
|
.setAttribute(entry, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static <C extends ConfigEntry<?>> Consumer<C> 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<String> values);
|
||||||
|
default void setAttributeDefault(ConfigEntry<?> entry, String key, String value) {
|
||||||
|
setAttributeDefault(entry, key, Collections.singletonList(value));
|
||||||
|
}
|
||||||
|
void setAttributeDefault(ConfigEntry<?> entry, String key, List<String> values);
|
||||||
|
|
||||||
|
List<String> getAttributeValues(ConfigEntry<?> entry, String key);
|
||||||
|
@Nullable String getAttributeValue(ConfigEntry<?> entry, String key);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.siphalor.tweed5.attributesextension.api;
|
||||||
|
|
||||||
|
public interface AttributesRelatedExtension {
|
||||||
|
default void afterAttributesInitialized() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@NullMarked
|
||||||
|
package de.siphalor.tweed5.attributesextension.api;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
@@ -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<? extends AttributesReadWriteFilterExtension> DEFAULT = AttributesReadWriteFilterExtensionImpl.class;
|
||||||
|
|
||||||
|
void markAttributeForFiltering(String key);
|
||||||
|
|
||||||
|
void addFilter(Patchwork contextExtensionsData, String key, String value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@NullMarked
|
||||||
|
package de.siphalor.tweed5.attributesextension.api.serde.filter;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
@@ -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<CustomEntryData> 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<String> 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<String> 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<String, List<String>> getOrCreateEditableAttributes(ConfigEntry<?> entry) {
|
||||||
|
CustomEntryData data = getOrCreateCustomEntryData(entry);
|
||||||
|
var attributes = data.attributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
attributes = new HashMap<>();
|
||||||
|
data.attributes(attributes);
|
||||||
|
}
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<String>> 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<Map<String, List<String>>> 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<String, List<String>> mergeMapsAndSeal(
|
||||||
|
Map<String, List<String>> base,
|
||||||
|
Map<String, List<String>> overrides
|
||||||
|
) {
|
||||||
|
if (overrides.isEmpty()) {
|
||||||
|
return ImmutableArrayBackedMap.ofEntries(base.entrySet());
|
||||||
|
} else if (base.isEmpty()) {
|
||||||
|
return ImmutableArrayBackedMap.ofEntries(overrides.entrySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map.Entry<String, List<String>>> 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<String> 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<String, List<String>> attributes;
|
||||||
|
private @Nullable Map<String, List<String>> attributeDefaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@ApiStatus.Internal
|
||||||
|
@NullMarked
|
||||||
|
package de.siphalor.tweed5.attributesextension.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
@@ -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<String> MIDDLEWARES_MUST_COME_BEFORE = new HashSet<>(Arrays.asList(
|
||||||
|
Middleware.DEFAULT_START,
|
||||||
|
"validation"
|
||||||
|
));
|
||||||
|
private static final Set<String> 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<String> filterableAttributes = new HashSet<>();
|
||||||
|
private final PatchworkPartAccess<EntryCustomData> entryDataAccess;
|
||||||
|
private @Nullable PatchworkPartAccess<ReadWriteContextCustomData> 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<Map<String, Set<String>>> attributesCollectors = new ArrayDeque<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEntry(ConfigEntry<?> entry) {
|
||||||
|
Map<String, Set<String>> currentAttributesCollector = attributesCollectors.peekFirst();
|
||||||
|
if (currentAttributesCollector != null) {
|
||||||
|
for (String filterableAttribute : filterableAttributes) {
|
||||||
|
List<String> 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<String, Set<String>> entryAttributesCollector = attributesCollectors.pop();
|
||||||
|
entry.extensionsData().set(entryDataAccess, new EntryCustomData(entryAttributesCollector));
|
||||||
|
|
||||||
|
Map<String, Set<String>> 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<TweedEntryReader<?, ?>> {
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> mustComeBefore() {
|
||||||
|
return MIDDLEWARES_MUST_COME_BEFORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> mustComeAfter() {
|
||||||
|
return MIDDLEWARES_MUST_COME_AFTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
|
||||||
|
assert readWriteContextDataAccess != null;
|
||||||
|
//noinspection unchecked
|
||||||
|
TweedEntryReader<Object, ConfigEntry<Object>> innerCasted
|
||||||
|
= (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
|
||||||
|
|
||||||
|
return new TweedEntryReader<@Nullable Object, ConfigEntry<Object>>() {
|
||||||
|
@Override
|
||||||
|
public @Nullable Object read(
|
||||||
|
TweedDataReader reader,
|
||||||
|
ConfigEntry<Object> 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<TweedEntryWriter<?, ?>> {
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> mustComeBefore() {
|
||||||
|
return MIDDLEWARES_MUST_COME_BEFORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> mustComeAfter() {
|
||||||
|
return MIDDLEWARES_MUST_COME_AFTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
|
||||||
|
assert readWriteContextDataAccess != null;
|
||||||
|
//noinspection unchecked
|
||||||
|
TweedEntryWriter<Object, ConfigEntry<Object>> innerCasted
|
||||||
|
= (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) 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<String, Set<String>> attributeFilter : contextData.attributeFilters().entrySet()) {
|
||||||
|
List<String> values = attributesExtension.getAttributeValues(entry, attributeFilter.getKey());
|
||||||
|
//noinspection SlowListContainsAll
|
||||||
|
if (!values.containsAll(attributeFilter.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Set<String>> attributeFilter : contextData.attributeFilters().entrySet()) {
|
||||||
|
Set<String> 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<Boolean> mapContext = new ArrayDeque<>();
|
||||||
|
private final Deque<TweedDataDecoration> preDecorationQueue = new ArrayDeque<>();
|
||||||
|
private final Deque<TweedDataDecoration> 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<String, Set<String>> optimizedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private static class ReadWriteContextCustomData {
|
||||||
|
private final Map<String, Set<String>> attributeFilters = new HashMap<>();
|
||||||
|
private boolean writerInstalled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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<Map<String, Object>> configContainer;
|
||||||
|
CompoundConfigEntry<Map<String, Object>> rootEntry;
|
||||||
|
SimpleConfigEntry<String> firstEntry;
|
||||||
|
SimpleConfigEntry<String> secondEntry;
|
||||||
|
CompoundConfigEntry<Map<String, Object>> nestedEntry;
|
||||||
|
SimpleConfigEntry<String> nestedFirstEntry;
|
||||||
|
SimpleConfigEntry<String> 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<Map<String, Object>>) (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<Map<String, Object>>) (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<String, Object> 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ public interface TweedExtension {
|
|||||||
default void extensionsFinalized() {
|
default void extensionsFinalized() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void initialize() {
|
||||||
|
}
|
||||||
|
|
||||||
default void initEntry(ConfigEntry<?> configEntry) {
|
default void initEntry(ConfigEntry<?> configEntry) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
|
|||||||
public void initialize() {
|
public void initialize() {
|
||||||
requireSetupPhase(ConfigContainerSetupPhase.TREE_ATTACHED);
|
requireSetupPhase(ConfigContainerSetupPhase.TREE_ATTACHED);
|
||||||
|
|
||||||
|
for (TweedExtension extension : extensions()) {
|
||||||
|
extension.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
assert rootEntry != null;
|
assert rootEntry != null;
|
||||||
rootEntry.visitInOrder(entry -> {
|
rootEntry.visitInOrder(entry -> {
|
||||||
for (TweedExtension extension : extensions()) {
|
for (TweedExtension extension : extensions()) {
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
class AcyclicGraphSorterTest {
|
class AcyclicGraphSorterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void trivialSort() {
|
||||||
|
AcyclicGraphSorter sorter = new AcyclicGraphSorter(2);
|
||||||
|
sorter.addEdge(0, 1);
|
||||||
|
assertArrayEquals(new int[]{ 0, 1 }, assertDoesNotThrow(sorter::sort));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sort1() {
|
void sort1() {
|
||||||
AcyclicGraphSorter sorter = new AcyclicGraphSorter(4);
|
AcyclicGraphSorter sorter = new AcyclicGraphSorter(4);
|
||||||
@@ -73,4 +80,4 @@ class AcyclicGraphSorterTest {
|
|||||||
AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort);
|
AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort);
|
||||||
assertEquals(Arrays.asList(0, 1), exception.cycleIndeces());
|
assertEquals(Arrays.asList(0, 1), exception.cycleIndeces());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import java.util.regex.Pattern;
|
|||||||
public class HjsonWriter implements TweedDataVisitor {
|
public class HjsonWriter implements TweedDataVisitor {
|
||||||
private static final int PREFILL_INDENT = 10;
|
private static final int PREFILL_INDENT = 10;
|
||||||
private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\\n|\\r\\n");
|
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 Writer writer;
|
||||||
private final Options options;
|
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)) {
|
if (value.isEmpty() || "true".equals(value) || "false".equals(value) || "null".equals(value)) {
|
||||||
return HjsonStringType.INLINE_DOUBLE_QUOTE;
|
return HjsonStringType.INLINE_DOUBLE_QUOTE;
|
||||||
}
|
}
|
||||||
|
if (NUMBER_PATTERN.matcher(value).matches()) {
|
||||||
|
return HjsonStringType.INLINE_DOUBLE_QUOTE;
|
||||||
|
}
|
||||||
int firstCodePoint = value.codePointAt(0);
|
int firstCodePoint = value.codePointAt(0);
|
||||||
if (Character.isDigit(firstCodePoint) || Character.isWhitespace(firstCodePoint)) {
|
if (Character.isWhitespace(firstCodePoint)) {
|
||||||
return HjsonStringType.INLINE_DOUBLE_QUOTE;
|
return HjsonStringType.INLINE_DOUBLE_QUOTE;
|
||||||
}
|
}
|
||||||
int lastCodePoint = value.codePointBefore(value.length());
|
int lastCodePoint = value.codePointBefore(value.length());
|
||||||
|
|||||||
@@ -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 + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<K, V> implements SortedMap<K, V> {
|
||||||
|
private final K[] keys;
|
||||||
|
private final V[] values;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <K extends Comparable<K>, V> SortedMap<K, V> ofEntries(Collection<Map.Entry<K, V>> entries) {
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
return Collections.emptySortedMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry<K, V> 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<Entry<K, V>> iterator = entries.stream().sorted(Entry.comparingByKey()).iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Entry<K, V> entry = iterator.next();
|
||||||
|
keys[i] = entry.getKey();
|
||||||
|
values[i] = entry.getValue();
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImmutableArrayBackedMap<>(keys, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Comparator<? super K> comparator() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull SortedMap<K, V> 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<K, V> 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<K, V> 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<? extends K, ? extends V> m) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<K> keySet() {
|
||||||
|
return new ImmutableArrayBackedSet<>(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<V> values() {
|
||||||
|
return new AbstractList<V>() {
|
||||||
|
@Override
|
||||||
|
public V get(int index) {
|
||||||
|
return values[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return values.length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Entry<K, V>> entrySet() {
|
||||||
|
//noinspection unchecked
|
||||||
|
return new ImmutableArrayBackedSet<Entry<K ,V>>(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<T> implements SortedSet<T> {
|
||||||
|
private final T[] values;
|
||||||
|
|
||||||
|
public static <T extends Comparable<T>> SortedSet<T> of(Collection<T> 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 <T extends Comparable<T>> SortedSet<T> of(T... values) {
|
||||||
|
if (values.length == 0) {
|
||||||
|
return Collections.emptySortedSet();
|
||||||
|
}
|
||||||
|
Arrays.sort(values);
|
||||||
|
return new ImmutableArrayBackedSet<>(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Comparator<? super T> comparator() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull SortedSet<T> 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<T> 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<T> 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<T> iterator() {
|
||||||
|
return Arrays.stream(values).iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Object[] toArray() {
|
||||||
|
return Arrays.copyOf(values, values.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull <T1> 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<? extends T> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
|
||||||
|
Map.entry(2, 20),
|
||||||
|
Map.entry(1, 10)
|
||||||
|
));
|
||||||
|
assertThat(map.firstKey()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lastKey() {
|
||||||
|
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
|
||||||
|
Map.entry(2, 20),
|
||||||
|
Map.entry(1, 10)
|
||||||
|
));
|
||||||
|
assertThat(map.lastKey()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void size() {
|
||||||
|
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
|
||||||
|
Map.entry(2, 20),
|
||||||
|
Map.entry(1, 10)
|
||||||
|
));
|
||||||
|
assertThat(map).hasSize(2);
|
||||||
|
assertThat(map).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void containsKey() {
|
||||||
|
SortedMap<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
|
||||||
|
Map.entry(2, 20),
|
||||||
|
Map.entry(1, 10),
|
||||||
|
Map.entry(3, 30)
|
||||||
|
));
|
||||||
|
Set<Integer> 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<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
|
||||||
|
Map.entry(2, 40),
|
||||||
|
Map.entry(1, 90),
|
||||||
|
Map.entry(3, 10)
|
||||||
|
));
|
||||||
|
Collection<Integer> 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<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
|
||||||
|
Map.entry(2, 40),
|
||||||
|
Map.entry(1, 90),
|
||||||
|
Map.entry(3, 10)
|
||||||
|
));
|
||||||
|
Set<Map.Entry<Integer, Integer>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
tweed5-weaver-pojo-attributes-extension/build.gradle.kts
Normal file
11
tweed5-weaver-pojo-attributes-extension/build.gradle.kts
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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 <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> 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 <T> Map<String, List<String>> collectAttributesFromAnnotations(
|
||||||
|
T[] annotations,
|
||||||
|
Function<T, String> keyGetter,
|
||||||
|
Function<T, String[]> valueGetter
|
||||||
|
) {
|
||||||
|
if (annotations.length == 0) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@NullMarked
|
||||||
|
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
Reference in New Issue
Block a user