[attributes] Introduce attributes extensions
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user