Initial commit

That's a lotta stuff for an initial commit, but well...
This commit is contained in:
2024-05-25 19:22:26 +02:00
commit b0f35b03b9
99 changed files with 6476 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
dependencies {
api(project(":tweed5-patchwork"))
}

View File

View File

@@ -0,0 +1,31 @@
package de.siphalor.tweed5.core.api.container;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import java.util.Collection;
import java.util.Map;
public interface ConfigContainer<T> {
ConfigContainerSetupPhase setupPhase();
default boolean isReady() {
return setupPhase() == ConfigContainerSetupPhase.READY;
}
void registerExtension(TweedExtension extension);
void finishExtensionSetup();
void attachAndSealTree(ConfigEntry<T> rootEntry);
EntryExtensionsData createExtensionsData();
void initialize();
ConfigEntry<T> rootEntry();
Collection<TweedExtension> extensions();
Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions();
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.core.api.container;
public enum ConfigContainerSetupPhase {
EXTENSIONS_SETUP,
TREE_SETUP,
SEALING_TREE,
TREE_SEALED,
READY,
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.Collection;
public interface CoherentCollectionConfigEntry<E, T extends Collection<E>> extends ConfigEntry<T> {
ConfigEntry<E> elementEntry();
T instantiateCollection(int size);
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.Map;
public interface CompoundConfigEntry<T> extends ConfigEntry<T> {
Map<String, ConfigEntry<?>> subEntries();
<V> void set(T compoundValue, String key, V value);
<V> V get(T compoundValue, String key);
T instantiateCompoundValue();
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
public interface ConfigEntry<T> {
Class<T> valueClass();
void validate(T value) throws ConfigEntryValueValidationException;
void seal(ConfigContainer<?> container);
boolean sealed();
EntryExtensionsData extensionsData();
void visitInOrder(ConfigEntryVisitor visitor);
}

View File

@@ -0,0 +1,28 @@
package de.siphalor.tweed5.core.api.entry;
public interface ConfigEntryVisitor {
void visitEntry(ConfigEntry<?> entry);
default boolean enterCollectionEntry(ConfigEntry<?> entry) {
visitEntry(entry);
return true;
}
default void leaveCollectionEntry(ConfigEntry<?> entry) {
}
default boolean enterCompoundEntry(ConfigEntry<?> entry) {
visitEntry(entry);
return true;
}
default boolean enterCompoundSubEntry(String key) {
return true;
}
default void leaveCompoundSubEntry(String key) {
}
default void leaveCompoundEntry(ConfigEntry<?> entry) {
}
}

View File

@@ -0,0 +1,4 @@
package de.siphalor.tweed5.core.api.entry;
public interface SimpleConfigEntry<T> extends ConfigEntry<T> {
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface EntryExtensionsData extends Patchwork<EntryExtensionsData> {
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface RegisteredExtensionData<U extends Patchwork<U>, E> {
void set(U patchwork, E extension);
}

View File

@@ -0,0 +1,13 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
public interface TweedExtension {
String getId();
default void setup(TweedExtensionSetupContext context) {
}
default void initEntry(ConfigEntry<?> configEntry) {
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
public interface TweedExtensionSetupContext {
ConfigContainer<?> configContainer();
<E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass);
}

View File

@@ -0,0 +1,96 @@
package de.siphalor.tweed5.core.api.middleware;
import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DefaultMiddlewareContainer<M> implements MiddlewareContainer<M> {
private static final String CONTAINER_ID = "";
private List<Middleware<M>> middlewares = new ArrayList<>();
private final Set<String> middlewareIds = new HashSet<>();
private boolean sealed = false;
@Override
public String id() {
return CONTAINER_ID;
}
@Override
public void register(Middleware<M> middleware) {
if (sealed) {
throw new IllegalStateException("Middleware container has already been sealed");
}
if (middleware.id().isEmpty()) {
throw new IllegalArgumentException("Middleware id cannot be empty");
}
if (middlewareIds.contains(middleware.id())) {
throw new IllegalArgumentException("Middleware id already registered: " + middleware.id());
}
middlewares.add(middleware);
middlewareIds.add(middleware.id());
}
@Override
public void seal() {
if (sealed) {
return;
}
sealed = true;
String[] allMentionedMiddlewareIds = middlewares.stream()
.flatMap(middleware -> Stream.concat(
Stream.of(middleware.id()),
Stream.concat(middleware.mustComeAfter().stream(), middleware.mustComeBefore().stream())
)).distinct().toArray(String[]::new);
Map<String, Integer> indecesByMiddlewareId = new HashMap<>();
for (int i = 0; i < allMentionedMiddlewareIds.length; i++) {
indecesByMiddlewareId.put(allMentionedMiddlewareIds[i], i);
}
AcyclicGraphSorter sorter = new AcyclicGraphSorter(allMentionedMiddlewareIds.length);
for (Middleware<M> middleware : middlewares) {
Integer currentIndex = indecesByMiddlewareId.get(middleware.id());
middleware.mustComeAfter().stream()
.map(indecesByMiddlewareId::get)
.forEach(beforeIndex -> sorter.addEdge(beforeIndex, currentIndex));
middleware.mustComeBefore().stream()
.map(indecesByMiddlewareId::get)
.forEach(afterIndex -> sorter.addEdge(currentIndex, afterIndex));
}
Map<String, Middleware<M>> middlewaresById = middlewares.stream().collect(Collectors.toMap(Middleware::id, Function.identity()));
try {
int[] sortedIndeces = sorter.sort();
middlewares = Arrays.stream(sortedIndeces)
.mapToObj(index -> allMentionedMiddlewareIds[index])
.map(middlewaresById::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (AcyclicGraphSorter.GraphCycleException e) {
throw new IllegalStateException(e);
}
}
@Override
public M process(M inner) {
if (!sealed) {
throw new IllegalStateException("Middleware container has not been sealed");
}
M combined = inner;
for (Middleware<M> middleware : middlewares) {
combined = middleware.process(combined);
}
return combined;
}
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.core.api.middleware;
import java.util.Collections;
import java.util.Set;
public interface Middleware<M> {
String id();
default Set<String> mustComeBefore() {
return Collections.emptySet();
}
default Set<String> mustComeAfter() {
return Collections.emptySet();
}
M process(M inner);
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.core.api.middleware;
public interface MiddlewareContainer<M> extends Middleware<M> {
void register(Middleware<M> middleware);
void seal();
}

View File

@@ -0,0 +1,287 @@
package de.siphalor.tweed5.core.api.sort;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.math.BigDecimal;
import java.util.*;
public class AcyclicGraphSorter {
private final int nodeCount;
private final int wordCount;
private final BitSet[] outgoingEdges;
private final BitSet[] incomingEdges;
public AcyclicGraphSorter(int nodeCount) {
this.nodeCount = nodeCount;
BigDecimal[] div = BigDecimal.valueOf(nodeCount)
.divideAndRemainder(BigDecimal.valueOf(BitSet.WORD_SIZE));
this.wordCount = div[0].intValue() + (BigDecimal.ZERO.equals(div[1]) ? 0 : 1);
outgoingEdges = new BitSet[nodeCount];
incomingEdges = new BitSet[nodeCount];
for (int i = 0; i < nodeCount; i++) {
outgoingEdges[i] = BitSet.empty(nodeCount, wordCount);
incomingEdges[i] = BitSet.empty(nodeCount, wordCount);
}
}
public void addEdge(int from, int to) {
checkBounds(from);
checkBounds(to);
if (from == to) {
throw new IllegalArgumentException("Edge from and to cannot be the same");
}
outgoingEdges[from].set(to);
incomingEdges[to].set(from);
}
private void checkBounds(int index) {
if (index < 0 || index >= nodeCount) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + nodeCount);
}
}
public int[] sort() throws GraphCycleException {
BitSet visited = BitSet.ready(nodeCount, wordCount);
BitSet.Iterator visitedIter = visited.iterator();
int lastVisited = -1;
int[] sortedIndeces = new int[nodeCount];
int nextSortedIndex = 0;
while (nextSortedIndex < sortedIndeces.length) {
if (!visitedIter.next()) {
BitSet incomingEdge = incomingEdges[visitedIter.index()];
if (incomingEdge.isEmptyAfterAndNot(visited)) {
sortedIndeces[nextSortedIndex] = visitedIter.index();
visitedIter.set();
lastVisited = visitedIter.index();
nextSortedIndex++;
}
} else if (visitedIter.index() == lastVisited) {
break;
}
if (!visitedIter.hasNext()) {
visitedIter.restart();
}
}
if (nextSortedIndex < sortedIndeces.length) {
findCycleAndThrow(visited);
}
return sortedIndeces;
}
private void findCycleAndThrow(BitSet visited) throws GraphCycleException {
Deque<Integer> stack = new LinkedList<>();
BitSet.Iterator visitedIter = visited.iterator();
while (visitedIter.next()) {
if (!visitedIter.hasNext()) {
throw new IllegalStateException("Unable to find unvisited node in cycle detection");
}
}
stack.push(visitedIter.index());
outer:
//noinspection InfiniteLoopStatement
while (true) {
BitSet leftoverOutgoing = outgoingEdges[stack.getFirst()].andNot(visited);
BitSet.Iterator outgoingIter = leftoverOutgoing.iterator();
while (outgoingIter.hasNext()) {
if (outgoingIter.next()) {
if (stack.contains(outgoingIter.index())) {
throw new GraphCycleException(stack.reversed());
}
stack.push(outgoingIter.index());
continue outer;
}
}
visited.set(stack.pop());
}
}
@Getter
@ToString
public static class GraphCycleException extends Exception {
private final Collection<Integer> cycleIndeces;
public GraphCycleException(Collection<Integer> cycleIndeces) {
super("Detected illegal cycle in directed graph");
this.cycleIndeces = cycleIndeces;
}
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
private static class BitSet {
private static final int WORD_SIZE = Long.SIZE;
private final int bitCount;
private final int wordCount;
private long[] words;
static BitSet ready(int bitCount, int wordCount) {
return new BitSet(bitCount, wordCount, new long[wordCount]);
}
static BitSet empty(int bitCount, int wordCount) {
return new BitSet(bitCount, wordCount, null);
}
private void set(int index) {
cloneOnWrite();
int wordIndex = index / WORD_SIZE;
int innerIndex = index % WORD_SIZE;
words[wordIndex] |= 1L << innerIndex;
}
private void cloneOnWrite() {
if (words == null) {
words = new long[wordCount];
}
}
public boolean isEmpty() {
if (words == null) {
return true;
}
for (long word : words) {
if (word != 0L) {
return false;
}
}
return true;
}
public BitSet andNot(BitSet mask) {
if (words == null) {
return BitSet.empty(bitCount, wordCount);
}
BitSet result = BitSet.ready(bitCount, wordCount);
for (int i = 0; i < words.length; i++) {
result.words[i] = words[i] & ~mask.words[i];
}
return result;
}
public boolean isEmptyAfterAndNot(BitSet mask) {
if (words == null) {
return true;
}
for (int i = 0; i < words.length; i++) {
long maskWord = mask.words[i];
long word = words[i];
if ((word & ~maskWord) != 0) {
return false;
}
}
return true;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BitSet)) return false;
BitSet bitSet = (BitSet) o;
if (this.words == null || bitSet.words == null) {
return this.isEmpty() && bitSet.isEmpty();
}
return Objects.deepEquals(words, bitSet.words);
}
@Override
public int hashCode() {
if (isEmpty()) {
return 0;
}
return Arrays.hashCode(words);
}
@Override
public String toString() {
if (wordCount == 0) {
return "";
}
StringBuilder sb = new StringBuilder(wordCount * 9);
int leftBitCount = bitCount;
if (words == null) {
for (int i = 0; i < wordCount; i++) {
sb.repeat("0", Math.min(WORD_SIZE, leftBitCount));
sb.append(" ");
leftBitCount -= WORD_SIZE;
}
} else {
for (long word : words) {
int wordEnd = Math.min(WORD_SIZE, leftBitCount);
for (int j = 0; j < wordEnd; j++) {
sb.append((word & 1) == 1 ? "1" : "0");
word >>>= 1;
}
sb.append(" ");
leftBitCount -= WORD_SIZE;
}
}
return sb.substring(0, sb.length() - 1);
}
public Iterator iterator() {
return new Iterator();
}
public class Iterator {
private int wordIndex = 0;
private int innerIndex = -1;
@Getter
private int index = -1;
public void restart() {
wordIndex = 0;
innerIndex = -1;
index = -1;
}
public boolean hasNext() {
return index < bitCount - 1;
}
public boolean next() {
innerIndex++;
if (innerIndex == WORD_SIZE) {
innerIndex = 0;
wordIndex++;
}
index++;
if (words == null) {
return false;
}
return (words[wordIndex] & (1L << innerIndex)) != 0L;
}
public void set() {
cloneOnWrite();
words[wordIndex] |= (1L << innerIndex);
}
}
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.validation;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface ConfigEntryValidationExtension {
Middleware<ConfigEntryValidationMiddleware> validationMiddleware(ConfigEntry<?> configEntry);
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.validation;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
@FunctionalInterface
public interface ConfigEntryValidationMiddleware {
<T> void validate(ConfigEntry<T> configEntry, T value) throws ConfigEntryValueValidationException;
}

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.core.api.validation;
public class ConfigEntryValueValidationException extends Exception {
public ConfigEntryValueValidationException() {
}
public ConfigEntryValueValidationException(String message) {
super(message);
}
public ConfigEntryValueValidationException(String message, Throwable cause) {
super(message, cause);
}
public ConfigEntryValueValidationException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1 @@
package de.siphalor.tweed5.core.generated;

View File

@@ -0,0 +1,152 @@
package de.siphalor.tweed5.core.impl;
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.extension.*;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import lombok.Getter;
import lombok.Setter;
import java.lang.invoke.MethodHandle;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final HashMap<Class<? extends TweedExtension>, TweedExtension> extensions = new HashMap<>();
private ConfigEntry<T> rootEntry;
private PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass;
private Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions;
@Override
public Collection<TweedExtension> extensions() {
return extensions.values();
}
@Override
public void registerExtension(TweedExtension extension) {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
extensions.put(extension.getClass(), extension);
}
@Override
public void finishExtensionSetup() {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
registeredEntryDataExtensions = new HashMap<>();
TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() {
@Override
public ConfigContainer<T> configContainer() {
return DefaultConfigContainer.this;
}
@Override
public <E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass) {
if (registeredEntryDataExtensions.containsKey(dataClass)) {
throw new IllegalArgumentException("Extension " + dataClass.getName() + " is already registered");
}
RegisteredExtensionDataImpl<EntryExtensionsData, E> registered = new RegisteredExtensionDataImpl<>();
registeredEntryDataExtensions.put(dataClass, registered);
return registered;
}
};
for (TweedExtension extension : extensions.values()) {
extension.setup(extensionSetupContext);
}
PatchworkClassCreator<EntryExtensionsData> entryExtensionsDataGenerator = PatchworkClassCreator.<EntryExtensionsData>builder()
.patchworkInterface(EntryExtensionsData.class)
.classPackage("de.siphalor.tweed5.core.generated.entryextensiondata")
.classPrefix("EntryExtensionsData$")
.build();
try {
entryExtensionsDataPatchworkClass = entryExtensionsDataGenerator.createClass(registeredEntryDataExtensions.keySet());
for (PatchworkClassPart part : entryExtensionsDataPatchworkClass.parts()) {
RegisteredExtensionDataImpl<EntryExtensionsData, ?> registeredExtension = registeredEntryDataExtensions.get(part.partInterface());
registeredExtension.setter(part.fieldSetter());
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new IllegalStateException("Failed to create patchwork class for entry extensions' data", e);
}
setupPhase = ConfigContainerSetupPhase.TREE_SETUP;
}
@Override
public void attachAndSealTree(ConfigEntry<T> rootEntry) {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
this.rootEntry = rootEntry;
finishEntrySetup();
}
private void finishEntrySetup() {
setupPhase = ConfigContainerSetupPhase.SEALING_TREE;
rootEntry.visitInOrder(entry -> entry.seal(DefaultConfigContainer.this));
setupPhase = ConfigContainerSetupPhase.TREE_SEALED;
}
@Override
public EntryExtensionsData createExtensionsData() {
requireSetupPhase(ConfigContainerSetupPhase.SEALING_TREE);
try {
return (EntryExtensionsData) entryExtensionsDataPatchworkClass.constructor().invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to construct patchwork class for entry extensions' data", e);
}
}
@Override
public Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED);
return registeredEntryDataExtensions;
}
@Override
public void initialize() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED);
rootEntry.visitInOrder(entry -> {
for (TweedExtension extension : extensions()) {
extension.initEntry(entry);
}
});
setupPhase = ConfigContainerSetupPhase.READY;
}
@Override
public ConfigEntry<T> rootEntry() {
return rootEntry;
}
private void requireSetupPhase(ConfigContainerSetupPhase required) {
if (setupPhase != required) {
throw new IllegalStateException("Config container is not in correct stage, expected " + required + ", but is in " + setupPhase);
}
}
@Setter
private static class RegisteredExtensionDataImpl<U extends Patchwork<U>, E> implements RegisteredExtensionData<U, E> {
private MethodHandle setter;
@Override
public void set(U patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
}

View File

@@ -0,0 +1,76 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
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.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.core.api.middleware.MiddlewareContainer;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValidationExtension;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValidationMiddleware;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
@RequiredArgsConstructor
@Getter
abstract class BaseConfigEntryImpl<T> implements ConfigEntry<T> {
private static final ConfigEntryValidationMiddleware ROOT_VALIDATION = new ConfigEntryValidationMiddleware() {
@Override
public <U> void validate(ConfigEntry<U> configEntry, U value) {}
};
@NotNull
private final Class<T> valueClass;
private ConfigContainer<?> container;
private EntryExtensionsData extensionsData;
private boolean sealed;
private ConfigEntryValidationMiddleware validationMiddleware;
@Override
public void seal(ConfigContainer<?> container) {
requireUnsealed();
this.container = container;
this.extensionsData = container.createExtensionsData();
MiddlewareContainer<ConfigEntryValidationMiddleware> validationMiddlewareContainer = new DefaultMiddlewareContainer<>();
for (TweedExtension extension : container().extensions()) {
if (extension instanceof ConfigEntryValidationExtension) {
validationMiddlewareContainer.register(((ConfigEntryValidationExtension) extension).validationMiddleware(this));
}
}
validationMiddlewareContainer.seal();
validationMiddleware = validationMiddlewareContainer.process(ROOT_VALIDATION);
sealed = true;
}
protected void requireUnsealed() {
if (sealed) {
throw new IllegalStateException("Config entry is already sealed!");
}
}
@Override
public void validate(T value) throws ConfigEntryValueValidationException {
if (value == null) {
if (valueClass.isPrimitive()) {
throw new ConfigEntryValueValidationException("Value must not be null");
}
} else if (!valueClass.isAssignableFrom(value.getClass())) {
throw new ConfigEntryValueValidationException("Value must be of type " + valueClass.getName());
}
validationMiddleware.validate(this, value);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
visitor.visitEntry(this);
}
}

View File

@@ -0,0 +1,42 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import java.util.Collection;
import java.util.function.IntFunction;
public class CoherentCollectionConfigEntryImpl<E, T extends Collection<E>> extends BaseConfigEntryImpl<T> implements CoherentCollectionConfigEntry<E, T> {
private final IntFunction<T> collectionConstructor;
private ConfigEntry<E> elementEntry;
public CoherentCollectionConfigEntryImpl(Class<T> valueClass, IntFunction<T> collectionConstructor) {
super(valueClass);
this.collectionConstructor = collectionConstructor;
}
public void elementEntry(ConfigEntry<E> elementEntry) {
requireUnsealed();
this.elementEntry = elementEntry;
}
@Override
public ConfigEntry<E> elementEntry() {
return elementEntry;
}
@Override
public T instantiateCollection(int size) {
return collectionConstructor.apply(size);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCollectionEntry(this)) {
elementEntry.visitInOrder(visitor);
visitor.leaveCollectionEntry(this);
}
}
}

View File

@@ -0,0 +1,108 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
import lombok.Getter;
import lombok.Value;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Getter
public class ReflectiveCompoundConfigEntryImpl<T> extends BaseConfigEntryImpl<T> implements CompoundConfigEntry<T> {
private final Constructor<T> noArgsConstructor;
private final Map<String, CompoundEntry> compoundEntries;
public ReflectiveCompoundConfigEntryImpl(Class<T> valueClass) {
super(valueClass);
try {
this.noArgsConstructor = valueClass.getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Value class must have a no-arg constructor", e);
}
this.compoundEntries = new LinkedHashMap<>();
}
public void addSubEntry(String name, Field field, ConfigEntry<?> configEntry) {
requireUnsealed();
if (field.getType() != valueClass()) {
throw new IllegalArgumentException("Field is not defined on the correct type");
}
//noinspection unchecked
compoundEntries.put(name, new CompoundEntry(name, field, (ConfigEntry<Object>) configEntry));
}
public Map<String, ConfigEntry<?>> subEntries() {
return compoundEntries.values().stream().collect(Collectors.toMap(CompoundEntry::name, CompoundEntry::configEntry));
}
@Override
public <V> void set(T compoundValue, String key, V value) {
CompoundEntry compoundEntry = compoundEntries.get(key);
if (compoundEntry == null) {
throw new IllegalArgumentException("Unknown config entry: " + key);
}
try {
compoundEntry.configEntry().validate(value);
compoundEntry.field().set(compoundValue, value);
} catch (ConfigEntryValueValidationException e) {
throw new IllegalArgumentException("Invalid value for config entry: " + key, e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@Override
public <V> V get(T compoundValue, String key) {
CompoundEntry compoundEntry = compoundEntries.get(key);
if (compoundEntry == null) {
throw new IllegalArgumentException("Unknown config entry: " + key);
}
try {
//noinspection unchecked
return (V) compoundEntry.field().get(compoundValue);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@Override
public T instantiateCompoundValue() {
try {
return noArgsConstructor.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Failed to instantiate compound value", e);
}
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCompoundEntry(this)) {
for (Map.Entry<String, CompoundEntry> entry : compoundEntries.entrySet()) {
if (visitor.enterCompoundSubEntry(entry.getKey())) {
entry.getValue().configEntry().visitInOrder(visitor);
visitor.leaveCompoundSubEntry(entry.getKey());
}
}
visitor.leaveCompoundEntry(this);
}
}
@Value
public static class CompoundEntry {
String name;
Field field;
ConfigEntry<Object> configEntry;
}
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
public class SimpleConfigEntryImpl<T> extends BaseConfigEntryImpl<T> implements SimpleConfigEntry<T> {
public SimpleConfigEntryImpl(Class<T> valueClass) {
super(valueClass);
}
}

View File

@@ -0,0 +1,67 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.IntFunction;
public class StaticMapCompoundConfigEntryImpl<T extends Map<String, Object>> extends BaseConfigEntryImpl<T> implements CompoundConfigEntry<T> {
private final IntFunction<T> mapConstructor;
private final Map<String, ConfigEntry<?>> compoundEntries = new LinkedHashMap<>();
public StaticMapCompoundConfigEntryImpl(@NotNull Class<T> valueClass, IntFunction<T> mapConstructor) {
super(valueClass);
this.mapConstructor = mapConstructor;
}
public void addSubEntry(String key, ConfigEntry<?> entry) {
requireUnsealed();
compoundEntries.put(key, entry);
}
@Override
public Map<String, ConfigEntry<?>> subEntries() {
return compoundEntries;
}
@Override
public <V> void set(T compoundValue, String key, V value) {
requireKey(key);
compoundValue.put(key, value);
}
@Override
public <V> V get(T compoundValue, String key) {
requireKey(key);
//noinspection unchecked
return (V) compoundValue.get(key);
}
private void requireKey(String key) {
if (!compoundEntries.containsKey(key)) {
throw new IllegalArgumentException("Key " + key + " does not exist on this compound entry!");
}
}
@Override
public T instantiateCompoundValue() {
return mapConstructor.apply(compoundEntries.size());
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCompoundEntry(this)) {
compoundEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) {
entry.visitInOrder(visitor);
visitor.leaveCompoundSubEntry(key);
}
});
visitor.leaveCompoundEntry(this);
}
}
}

View File

@@ -0,0 +1 @@
package de.siphalor.tweed5.core.impl;

View File

@@ -0,0 +1,66 @@
package de.siphalor.tweed5.core.impl.sort;
import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
class AcyclicGraphSorterTest {
@Test
void sort1() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(4);
sorter.addEdge(2, 1);
sorter.addEdge(0, 2);
assertArrayEquals(new int[]{ 0, 2, 3, 1 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sort2() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(7);
sorter.addEdge(3, 0);
sorter.addEdge(3, 1);
sorter.addEdge(3, 2);
sorter.addEdge(4, 3);
sorter.addEdge(5, 3);
sorter.addEdge(6, 3);
assertArrayEquals(new int[]{ 4, 5, 6, 3, 0, 1, 2 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sort3() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(8);
sorter.addEdge(0, 3);
sorter.addEdge(1, 0);
sorter.addEdge(1, 5);
sorter.addEdge(2, 0);
sorter.addEdge(4, 1);
sorter.addEdge(4, 5);
sorter.addEdge(5, 2);
sorter.addEdge(6, 7);
sorter.addEdge(7, 2);
assertArrayEquals(new int[] { 4, 6, 7, 1, 5, 2, 0, 3 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sortErrorCycle() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(8);
sorter.addEdge(0, 6);
sorter.addEdge(0, 1);
sorter.addEdge(6, 1);
sorter.addEdge(2, 3);
sorter.addEdge(2, 4);
sorter.addEdge(4, 5);
sorter.addEdge(5, 2);
AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort);
assertEquals(Arrays.asList(2, 4, 5), exception.cycleIndeces());
}
}