Initial commit
That's a lotta stuff for an initial commit, but well...
This commit is contained in:
3
tweed5-core/build.gradle.kts
Normal file
3
tweed5-core/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
api(project(":tweed5-patchwork"))
|
||||
}
|
||||
0
tweed5-core/gradle.properties
Normal file
0
tweed5-core/gradle.properties
Normal 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();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.siphalor.tweed5.core.api.container;
|
||||
|
||||
public enum ConfigContainerSetupPhase {
|
||||
EXTENSIONS_SETUP,
|
||||
TREE_SETUP,
|
||||
SEALING_TREE,
|
||||
TREE_SEALED,
|
||||
READY,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package de.siphalor.tweed5.core.api.entry;
|
||||
|
||||
public interface SimpleConfigEntry<T> extends ConfigEntry<T> {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.core.api.extension;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
|
||||
public interface EntryExtensionsData extends Patchwork<EntryExtensionsData> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package de.siphalor.tweed5.core.generated;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package de.siphalor.tweed5.core.impl;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user