[build] Restructure to composite build

This commit is contained in:
2025-10-26 02:03:53 +02:00
parent 0e3990aed9
commit 1fbc97866c
348 changed files with 126 additions and 64 deletions

View File

@@ -0,0 +1,9 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
implementation(project(":tweed5-utils"))
implementation(project(":tweed5-type-utils"))
implementation(libs.asm.core)
}

View File

@@ -0,0 +1,3 @@
module.name = Tweed 5 Annotation Inheritance
module.description = Provides a mechanism to create meta-annotations. \
This allows bundling annotations that are commonly used together into a single convenience annotation.

View File

@@ -0,0 +1,10 @@
package de.siphalor.tweed5.annotationinheritance.api;
import java.lang.annotation.*;
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationInheritance {
Class<? extends Annotation>[] passOn() default {};
Class<? extends Annotation>[] override() default {};
}

View File

@@ -0,0 +1,91 @@
package de.siphalor.tweed5.annotationinheritance.api;
import de.siphalor.tweed5.annotationinheritance.impl.AnnotationInheritanceResolver;
import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType;
import de.siphalor.tweed5.utils.api.collection.ClassToInstanceMap;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
@RequiredArgsConstructor
public class AnnotationInheritanceAwareAnnotatedElement implements AnnotatedElement {
private final AnnotatedElement inner;
private @Nullable ClassToInstanceMap<Annotation> resolvedAnnotations;
@Override
public @Nullable <T extends Annotation> T getAnnotation(@NonNull Class<T> annotationClass) {
if (resolvedAnnotations != null) {
return resolvedAnnotations.get(annotationClass);
}
Annotation[] annotations = inner.getAnnotations();
boolean metaEncountered = false;
T foundAnnotation = null;
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(annotationClass)) {
//noinspection unchecked
foundAnnotation = (T) annotation;
} else if (!metaEncountered) {
metaEncountered = annotation.annotationType().isAnnotationPresent(AnnotationInheritance.class);
}
}
if (foundAnnotation != null) {
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(annotationClass);
if (repeatType instanceof AnnotationRepeatType.NonRepeatable) {
return foundAnnotation;
}
}
if (!metaEncountered) {
return foundAnnotation;
}
return getOrResolveAnnotations().get(annotationClass);
}
@Override
public @NonNull <T extends Annotation> @NotNull T[] getAnnotationsByType(@NonNull Class<T> annotationClass) {
T annotation = getOrResolveAnnotations().get(annotationClass);
if (annotation != null) {
//noinspection unchecked
T[] array = (T[]) Array.newInstance(annotationClass, 1);
array[0] = annotation;
return array;
}
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(annotationClass);
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
AnnotationRepeatType.RepeatableContainer containerRepeatType =
((AnnotationRepeatType.Repeatable) repeatType).containerRepeatType();
Annotation containerAnnotation = getOrResolveAnnotations().get(containerRepeatType.annotationClass());
if (containerAnnotation != null) {
return containerRepeatType.elements(containerAnnotation);
}
}
//noinspection unchecked
return (T[]) Array.newInstance(annotationClass, 0);
}
@Override
public @NotNull Annotation[] getAnnotations() {
return getOrResolveAnnotations().values().toArray(new Annotation[0]);
}
@Override
public @NotNull Annotation[] getDeclaredAnnotations() {
return inner.getDeclaredAnnotations();
}
private ClassToInstanceMap<Annotation> getOrResolveAnnotations() {
if (resolvedAnnotations == null) {
resolvedAnnotations = new AnnotationInheritanceResolver(inner).resolve();
}
return resolvedAnnotations;
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.annotationinheritance.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,152 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import de.siphalor.tweed5.annotationinheritance.api.AnnotationInheritance;
import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType;
import de.siphalor.tweed5.utils.api.collection.ClassToInstanceMap;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
@RequiredArgsConstructor
public class AnnotationInheritanceResolver {
private final AnnotatedElement main;
private final Map<Class<? extends Annotation>, Aggregator> aggregators = new LinkedHashMap<>();
private static final Set<Class<? extends Annotation>> IGNORED_META_ANNOTATIONS = new CopyOnWriteArraySet<>(Arrays.asList(
Target.class,
Retention.class,
AnnotationInheritance.class
));
public ClassToInstanceMap<Annotation> resolve() {
resolve(main, Collections.emptySet());
ClassToInstanceMap<Annotation> resolvedAnnotations = ClassToInstanceMap.backedBy(new LinkedHashMap<>());
List<Aggregator> aggregatorList = new ArrayList<>(aggregators.values());
for (int i = aggregatorList.size() - 1; i >= 0; i--) {
Aggregator aggregator = aggregatorList.get(i);
if (aggregator.annotations.size() == 1) {
//noinspection unchecked
resolvedAnnotations.put(
(Class<Annotation>) aggregator.repeatType.annotationClass(),
aggregator.annotations.iterator().next()
);
} else if (!aggregator.annotations.isEmpty()) {
Annotation[] annotations = (Annotation[]) Array.newInstance(
aggregator.repeatType.annotationClass(),
aggregator.annotations.size()
);
int j = aggregator.annotations.size() - 1;
for (Annotation annotation : aggregator.annotations) {
annotations[j--] = annotation;
}
Annotation containerAnnotation = RepeatableAnnotationContainerHelper.createContainer(annotations);
//noinspection unchecked
resolvedAnnotations.put((Class<Annotation>) containerAnnotation.annotationType(), containerAnnotation);
}
}
return resolvedAnnotations;
}
private void resolve(AnnotatedElement annotatedElement, Set<Class<? extends Annotation>> overriden) {
AnnotationInheritance inheritanceConfig = annotatedElement.getAnnotation(AnnotationInheritance.class);
Set<Class<? extends Annotation>> passOnAnnotations = null;
Set<Class<? extends Annotation>> overridenOnwards = new HashSet<>(overriden);
if (annotatedElement != main) {
if (inheritanceConfig == null || inheritanceConfig.passOn().length == 0) {
return;
}
passOnAnnotations = new HashSet<>(inheritanceConfig.passOn().length + 5);
for (Class<? extends Annotation> passOn : inheritanceConfig.passOn()) {
passOnAnnotations.add(passOn);
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(passOn);
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
passOnAnnotations.add(((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass());
}
}
}
if (inheritanceConfig != null) {
for (Class<? extends Annotation> override : inheritanceConfig.override()) {
overridenOnwards.add(override);
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(override);
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
overridenOnwards.add(((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass());
}
}
}
Annotation[] annotations = annotatedElement.getAnnotations();
for (int i = annotations.length - 1; i >= 0; i--) {
Annotation annotation = annotations[i];
if ((passOnAnnotations != null && !passOnAnnotations.contains(annotation.annotationType()))
|| IGNORED_META_ANNOTATIONS.contains(annotation.annotationType())
|| overriden.contains(annotation.annotationType())) {
continue;
}
Aggregator aggregator = aggregators.get(annotation.annotationType());
AnnotationRepeatType repeatType;
if (aggregator != null) {
repeatType = aggregator.repeatType;
if (repeatType instanceof AnnotationRepeatType.Repeatable) {
aggregator.annotations.add(annotation);
}
} else {
repeatType = AnnotationRepeatType.getType(annotation.annotationType());
if (repeatType instanceof AnnotationRepeatType.NonRepeatable) {
aggregator = new Aggregator(repeatType, Collections.singleton(annotation));
aggregators.put(annotation.annotationType(), aggregator);
overridenOnwards.add(annotation.annotationType());
} else if (repeatType instanceof AnnotationRepeatType.Repeatable) {
ArrayList<Annotation> repeatableAnnotations = new ArrayList<>();
repeatableAnnotations.add(annotation);
aggregator = new Aggregator(repeatType, repeatableAnnotations);
aggregators.put(annotation.annotationType(), aggregator);
} else if (repeatType instanceof AnnotationRepeatType.RepeatableContainer) {
AnnotationRepeatType.RepeatableContainer containerRepeatType
= (AnnotationRepeatType.RepeatableContainer) repeatType;
Class<? extends Annotation> elementAnnotationType = containerRepeatType.elementAnnotationClass();
Annotation[] elements = containerRepeatType.elements(annotation);
aggregator = aggregators.get(elementAnnotationType);
if (aggregator != null) {
for (int j = elements.length - 1; j >= 0; j--) {
aggregator.annotations.add(elements[j]);
}
} else {
List<Annotation> repeatedAnnotations = new ArrayList<>(elements.length);
for (int e = elements.length - 1; e >= 0; e--) {
repeatedAnnotations.add(elements[e]);
}
aggregators.put(
containerRepeatType.elementAnnotationClass(),
new Aggregator(containerRepeatType.elementRepeatType(), repeatedAnnotations)
);
}
}
}
if (repeatType instanceof AnnotationRepeatType.NonRepeatable && annotation.annotationType()
.isAnnotationPresent(AnnotationInheritance.class)) {
resolve(annotation.annotationType(), overridenOnwards);
}
}
}
@Value
private static class Aggregator {
AnnotationRepeatType repeatType;
Collection<Annotation> annotations;
}
}

View File

@@ -0,0 +1,23 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import org.jspecify.annotations.Nullable;
class ByteArrayClassLoader extends ClassLoader {
public static Class<?> loadClass(@Nullable String binaryClassName, byte[] byteCode) {
return new ByteArrayClassLoader(ByteArrayClassLoader.class.getClassLoader()).createClass(
binaryClassName,
byteCode
);
}
public ByteArrayClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> createClass(@Nullable String binaryClassName, byte[] byteCode) {
Class<?> clazz = defineClass(binaryClassName, byteCode, 0, byteCode.length);
resolveClass(clazz);
return clazz;
}
}

View File

@@ -0,0 +1,400 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import de.siphalor.tweed5.typeutils.api.annotations.AnnotationRepeatType;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.objectweb.asm.*;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class RepeatableAnnotationContainerHelper {
/**
* Class version to use for generation (Java 8)
*/
private static final int CLASS_VERSION = Opcodes.V1_8;
private static final String GENERATED_PACKAGE = RepeatableAnnotationContainerHelper.class.getPackage().getName()
+ ".generated";
private static String generateUniqueIdentifier() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replace("-", "");
}
private static final Map<Class<? extends Annotation>, Function<Annotation[], Annotation>> CACHE = new HashMap<>();
private static final ReadWriteLock CACHE_LOCK = new ReentrantReadWriteLock();
@SuppressWarnings("unchecked")
public static <R extends Annotation, C extends Annotation> C createContainer(R[] elements) {
if (elements.length == 0) {
throw new IllegalArgumentException("elements must not be empty");
}
Class<R> repeatableClass = (Class<R>) elements[0].annotationType();
AnnotationRepeatType repeatType = AnnotationRepeatType.getType(repeatableClass);
if (!(repeatType instanceof AnnotationRepeatType.Repeatable)) {
throw new IllegalArgumentException(repeatableClass.getName() + " is not a repeatable");
}
Class<? extends Annotation> containerClass
= ((AnnotationRepeatType.Repeatable) repeatType).containerAnnotationClass();
CACHE_LOCK.readLock().lock();
try {
Function<Annotation[], Annotation> constructor = CACHE.get(containerClass);
if (constructor != null) {
return (C) constructor.apply(elements);
}
} finally {
CACHE_LOCK.readLock().unlock();
}
Function<R[], ? extends Annotation> constructor = createContainerClassConstructor(
containerClass,
repeatableClass
);
CACHE_LOCK.writeLock().lock();
try {
//noinspection rawtypes
CACHE.put(containerClass, (Function) constructor);
} finally {
CACHE_LOCK.writeLock().unlock();
}
return (C) constructor.apply(elements);
}
@SuppressWarnings("unchecked")
private static <R extends Annotation, C extends Annotation> Function<R[], C> createContainerClassConstructor(
Class<C> containerClass,
Class<R> repeatableClass
) {
try {
if (!containerClass.isAnnotation()) {
throw new IllegalArgumentException(containerClass.getName() + " is not a container annotation");
}
String generatedClassName = GENERATED_PACKAGE + ".RepeatableContainer$" + generateUniqueIdentifier();
byte[] bytes = createContainerClassBytes(generatedClassName, containerClass, repeatableClass);
Class<?>
generatedClass
= new ByteArrayClassLoader(RepeatableAnnotationContainerHelper.class.getClassLoader())
.createClass(generatedClassName, bytes);
MethodHandle constructorHandle = MethodHandles.lookup().findConstructor(
generatedClass,
MethodType.methodType(void.class, arrayType(repeatableClass))
);
return (repeatedValues) -> {
try {
return (C) constructorHandle.invoke((Object) repeatedValues);
} catch (Throwable e) {
throw new RuntimeException("Failed to instantiate generated container annotation", e);
}
};
} catch (Exception e) {
throw new IllegalStateException("Class generation failed", e);
}
}
static <R extends Annotation, C extends Annotation> byte[] createContainerClassBytes(
String generatedClassName,
Class<C> containerClass,
Class<R> repeatableClass
) {
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
String generatedClassNameInternal = generatedClassName.replace('.', '/');
classWriter.visit(
CLASS_VERSION,
Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
generatedClassNameInternal,
null,
"java/lang/Object",
new String[]{containerClass.getName().replace('.', '/')}
);
Class<?> repeatableArrayClass = arrayType(repeatableClass);
classWriter.visitField(Opcodes.ACC_PRIVATE, "values", descriptor(repeatableArrayClass), null, null);
appendConstructor(classWriter, repeatableArrayClass, generatedClassNameInternal);
appendValueMethod(classWriter, repeatableArrayClass, generatedClassNameInternal);
appendEqualsMethod(classWriter, repeatableArrayClass, containerClass, generatedClassNameInternal);
appendHashCodeMethod(classWriter, repeatableArrayClass, generatedClassNameInternal);
appendToStringMethod(classWriter, repeatableArrayClass, containerClass, generatedClassNameInternal);
appendAnnotationTypeMethod(classWriter, containerClass);
classWriter.visitEnd();
return classWriter.toByteArray();
}
private static void appendConstructor(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"<init>",
"(" + descriptor(repeatableArrayClass) + ")V",
null,
null
);
methodWriter.visitParameter("values", 0);
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitVarInsn(Opcodes.ALOAD, 1);
methodWriter.visitFieldInsn(
Opcodes.PUTFIELD,
generatedClassNameInternal,
"values",
descriptor(repeatableArrayClass)
);
methodWriter.visitInsn(Opcodes.RETURN);
methodWriter.visitMaxs(2, 2);
methodWriter.visitEnd();
}
private static void appendValueMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"value",
"()" + descriptor(repeatableArrayClass),
null,
null
);
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
descriptor(repeatableArrayClass)
);
methodWriter.visitInsn(Opcodes.ARETURN);
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
private static void appendEqualsMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
Class<?> containerAnnotationClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"equals",
"(Ljava/lang/Object;)Z",
null,
null
);
methodWriter.visitParameter("other", 0);
Label falseLabel = new Label();
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 1);
String containerAnnotationClassBinaryName = Type.getInternalName(containerAnnotationClass);
methodWriter.visitTypeInsn(Opcodes.INSTANCEOF, containerAnnotationClassBinaryName);
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
descriptor(repeatableArrayClass)
);
methodWriter.visitVarInsn(Opcodes.ALOAD, 1);
methodWriter.visitMethodInsn(
Opcodes.INVOKEINTERFACE,
containerAnnotationClassBinaryName,
"value",
"()" + descriptor(repeatableArrayClass),
true
);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Arrays",
"equals",
"([Ljava/lang/Object;[Ljava/lang/Object;)Z",
false
);
methodWriter.visitInsn(Opcodes.IRETURN);
methodWriter.visitLabel(falseLabel);
methodWriter.visitLdcInsn(false);
methodWriter.visitInsn(Opcodes.IRETURN);
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private static void appendHashCodeMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"hashCode",
"()I",
null,
null
);
final int keyHashCode = "value".hashCode() * 127;
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
descriptor(repeatableArrayClass)
);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Arrays",
"hashCode",
"([Ljava/lang/Object;)I",
false
);
methodWriter.visitLdcInsn(keyHashCode);
methodWriter.visitInsn(Opcodes.IXOR);
methodWriter.visitInsn(Opcodes.IRETURN);
methodWriter.visitMaxs(2, 2);
methodWriter.visitEnd();
}
private static void appendToStringMethod(
ClassWriter classWriter,
Class<?> repeatableArrayClass,
Class<?> containerAnnotationClass,
String generatedClassNameInternal
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"toString",
"()Ljava/lang/String;",
null,
null
);
String prefix = "@" + containerAnnotationClass.getName() + "(value=";
String suffix = ")";
String stringBuilderBinaryName = StringBuilder.class.getName().replace('.', '/');
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
generatedClassNameInternal,
"values",
descriptor(repeatableArrayClass)
);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Arrays",
"toString",
"([Ljava/lang/Object;)Ljava/lang/String;",
false
);
methodWriter.visitInsn(Opcodes.DUP);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/String",
"length",
"()I",
false
);
methodWriter.visitLdcInsn(prefix.length() + suffix.length());
methodWriter.visitInsn(Opcodes.IADD);
methodWriter.visitTypeInsn(Opcodes.NEW, stringBuilderBinaryName);
methodWriter.visitInsn(Opcodes.DUP_X1); // S, I, SB -> S, SB, I, SB
methodWriter.visitInsn(Opcodes.SWAP); // S, SB, I, SB -> S, SB, SB, I
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
stringBuilderBinaryName,
"<init>",
"(I)V",
false
);
methodWriter.visitLdcInsn(prefix);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"append",
"(Ljava/lang/CharSequence;)L" + stringBuilderBinaryName + ";",
false
);
methodWriter.visitInsn(Opcodes.SWAP); // S, SB -> SB, S
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"append",
"(Ljava/lang/String;)L" + stringBuilderBinaryName + ";",
false
);
methodWriter.visitLdcInsn(suffix);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"append",
"(Ljava/lang/CharSequence;)L" + stringBuilderBinaryName + ";",
false
);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderBinaryName,
"toString",
"()Ljava/lang/String;",
false
);
methodWriter.visitInsn(Opcodes.ARETURN);
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private static void appendAnnotationTypeMethod(
ClassWriter classWriter,
Class<?> containerAnnotationClass
) {
MethodVisitor methodWriter = classWriter.visitMethod(
Opcodes.ACC_PUBLIC,
"annotationType",
"()Ljava/lang/Class;",
null,
null
);
methodWriter.visitCode();
methodWriter.visitLdcInsn(Type.getType(containerAnnotationClass));
methodWriter.visitInsn(Opcodes.ARETURN);
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
private static String descriptor(Class<?> clazz) {
return Type.getType(clazz).getDescriptor();
}
private static <T> Class<T[]> arrayType(Class<T> clazz) {
try {
//noinspection unchecked
return (Class<T[]>) Class.forName("[L" + clazz.getName() + ";");
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Failed to get array class for " + clazz.getName(), e);
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Package for on-the-fly-generated classes.
*/
package de.siphalor.tweed5.annotationinheritance.impl.generated;

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.annotationinheritance.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,199 @@
package de.siphalor.tweed5.annotationinheritance.api;
import org.junit.jupiter.api.Test;
import java.lang.annotation.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.array;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
class AnnotationInheritanceAwareAnnotatedElementTest {
@Test
void getAnnotationAForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(A.class))
.isNotNull()
.extracting(A::value)
.isEqualTo(1);
}
@Test
void getAnnotationsByTypeAForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotationsByType(A.class))
.singleElement()
.extracting(A::value)
.isEqualTo(1);
}
@Test
void getAnnotationBForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(B.class))
.isNotNull()
.extracting(B::value)
.isEqualTo(2);
}
@Test
void getAnnotationsByTypeBForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotationsByType(B.class))
.singleElement()
.extracting(B::value)
.isEqualTo(2);
}
@Test
void getAnnotationCForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(C.class))
.isNotNull()
.extracting(C::value)
.isEqualTo(10);
}
@Test
void getAnnotationsByTypeCForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotationsByType(C.class))
.singleElement()
.extracting(C::value)
.isEqualTo(10);
}
@Test
void getAnnotationRForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotation(R.class)).isNull();
}
@Test
void getAnnotationsByTypeRForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotationsByType(R.class))
.satisfiesExactly(
r -> assertThat(r.value()).isEqualTo(4),
r -> assertThat(r.value()).isEqualTo(2),
r -> assertThat(r.value()).isEqualTo(3),
r -> assertThat(r.value()).isEqualTo(10)
);
}
@Test
void getAnnotationRsForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
Rs rs = element.getAnnotation(Rs.class);
assertThat(rs)
.isNotNull()
.extracting(Rs::value)
.asInstanceOf(array(R[].class))
.satisfiesExactly(
r -> assertThat(r.value()).isEqualTo(4),
r -> assertThat(r.value()).isEqualTo(2),
r -> assertThat(r.value()).isEqualTo(3),
r -> assertThat(r.value()).isEqualTo(10)
);
}
@Test
void getAnnotationsByTypeRsForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotationsByType(Rs.class))
.singleElement()
.extracting(Rs::value)
.asInstanceOf(array(R[].class))
.satisfiesExactly(
r -> assertThat(r.value()).isEqualTo(4),
r -> assertThat(r.value()).isEqualTo(2),
r -> assertThat(r.value()).isEqualTo(3),
r -> assertThat(r.value()).isEqualTo(10)
);
}
@Test
void getAnnotationsForTarget1() {
var element = new AnnotationInheritanceAwareAnnotatedElement(Target1.class);
assertThat(element.getAnnotations())
.satisfiesExactlyInAnyOrder(
a -> assertThat(a).isInstanceOf(BequeatherThree.class),
a -> assertThat(a).isInstanceOf(BequeatherTwo.class),
a -> assertThat(a).isInstanceOf(BequeatherOne.class),
a -> assertThat(a).asInstanceOf(type(A.class)).extracting(A::value).isEqualTo(1),
a -> assertThat(a).asInstanceOf(type(B.class)).extracting(B::value).isEqualTo(2),
a -> assertThat(a).asInstanceOf(type(C.class)).extracting(C::value).isEqualTo(10),
a -> assertThat(a).asInstanceOf(type(Rs.class))
.extracting(Rs::value)
.asInstanceOf(array(R[].class))
.satisfiesExactly(
r -> assertThat(r).extracting(R::value).isEqualTo(4),
r -> assertThat(r).extracting(R::value).isEqualTo(2),
r -> assertThat(r).extracting(R::value).isEqualTo(3),
r -> assertThat(r).extracting(R::value).isEqualTo(10)
)
);
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface A {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface B {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface C {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Repeatable(Rs.class)
public @interface R {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface Rs {
R[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@A(1)
@B(1)
@C(1)
@R(1)
@R(2)
@AnnotationInheritance(passOn = {A.class, B.class, C.class, R.class})
public @interface BequeatherOne {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@BequeatherOne
@B(2)
@R(2)
@R(3)
@AnnotationInheritance(passOn = {BequeatherOne.class, B.class, R.class}, override = R.class)
public @interface BequeatherTwo {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@R(4)
@AnnotationInheritance(passOn = {R.class, A.class})
public @interface BequeatherThree {}
@BequeatherThree
@BequeatherTwo
@C(10)
@R(10)
public static class Target1 {}
}

View File

@@ -0,0 +1,95 @@
package de.siphalor.tweed5.annotationinheritance.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@RepeatableAnnotationContainerHelperTest.R(1)
@RepeatableAnnotationContainerHelperTest.R(2)
@RepeatableAnnotationContainerHelperTest.R(3)
class RepeatableAnnotationContainerHelperTest {
@Test
@Disabled("Dumping the class is only for debugging purposes")
@SneakyThrows
void dumpClass() {
byte[] bytes = RepeatableAnnotationContainerHelper.createContainerClassBytes(
"de.siphalor.tweed5.annotationinheritance.impl.generated.DumpIt",
Rs.class,
R.class
);
Path path = Path.of(getClass().getSimpleName() + ".class");
Files.write(path, bytes);
System.out.println("Dumped to " + path.toAbsolutePath());
}
@Test
void test() {
R[] elements = {new RImpl(1), new RImpl(2), new RImpl(3)};
Annotation result = RepeatableAnnotationContainerHelper.createContainer(elements);
assertThat(result).asInstanceOf(type(Rs.class))
.extracting(Rs::value)
.isEqualTo(elements);
assertThat(result.annotationType()).isEqualTo(Rs.class);
assertThat(result.toString()).containsSubsequence("Rs(value=", "1", "2", "3");
Rs ref = RepeatableAnnotationContainerHelperTest.class.getAnnotation(Rs.class);
assertThat(result.equals(ref)).isTrue();
assertThat(result.hashCode()).isEqualTo(ref.hashCode());
}
@RequiredArgsConstructor
@Getter
public static class RImpl implements R {
private final int value;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof R) {
return ((R) obj).value() == this.value();
}
return false;
}
@Override
public int hashCode() {
return (127 * "value".hashCode()) ^ Integer.valueOf(value).hashCode();
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public Class<? extends Annotation> annotationType() {
return R.class;
}
}
@Repeatable(Rs.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface R {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Rs {
R[] value();
}
}

View File

@@ -0,0 +1,10 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
implementation(project(":tweed5-core"))
compileOnly(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,2 @@
module.name = Tweed 5 Attributes Extension
module.description = A Tweed extension that allows defining generic attributes on config entries.

View File

@@ -0,0 +1,43 @@
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;
String EXTENSION_ID = "attributes";
default String getId() {
return EXTENSION_ID;
}
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);
}

View File

@@ -0,0 +1,5 @@
package de.siphalor.tweed5.attributesextension.api;
public interface AttributesRelatedExtension {
default void afterAttributesInitialized() {}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.attributesextension.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,18 @@
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;
String EXTENSION_ID = "attributes-read-write-filter";
default String getId() {
return EXTENSION_ID;
}
void markAttributeForFiltering(String key);
void addFilter(Patchwork contextExtensionsData, String key, String value);
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.attributesextension.api.serde.filter;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,206 @@
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.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 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 void setAttribute(ConfigEntry<?> entry, String key, List<String> values) {
requireEditable();
Map<String, List<String>> 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();
Map<String, List<String>> 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);
Map<String, List<String>> 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);
Map<String, List<String>> 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 enterStructuredEntry(ConfigEntry<?> entry) {
enterEntry(entry);
return true;
}
private void enterEntry(ConfigEntry<?> entry) {
CustomEntryData data = entry.extensionsData().get(dataAccess);
Map<String, List<String>> currentDefaults = defaults.getFirst();
if (data == null) {
defaults.push(currentDefaults);
return;
}
Map<String, List<String>> 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 leaveStructuredEntry(ConfigEntry<?> entry) {
defaults.pop();
}
@Override
public void visitEntry(ConfigEntry<?> entry) {
CustomEntryData data = getOrCreateCustomEntryData(entry);
Map<String, List<String>> 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;
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.attributesextension.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,384 @@
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.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.dataapi.api.*;
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.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.*;
public class AttributesReadWriteFilterExtensionImpl
implements AttributesReadWriteFilterExtension, AttributesRelatedExtension, ReadWriteRelatedExtension {
private static final Set<String> MIDDLEWARES_MUST_COME_BEFORE = Collections.emptySet();
private static final Set<String> MIDDLEWARES_MUST_COME_AFTER = new HashSet<>(Arrays.asList(
Middleware.DEFAULT_END,
"validation"
));
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 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 enterStructuredEntry(ConfigEntry<?> entry) {
attributesCollectors.push(new HashMap<>());
visitEntry(entry);
return true;
}
@Override
public void leaveStructuredEntry(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();
ReadWriteContextCustomData 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 EXTENSION_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 EXTENSION_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 DelegatingTweedDataWriter {
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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1,182 @@
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 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.generic.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)
);
}
}

24
tweed5/build.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
plugins {
`jacoco-report-aggregation`
`maven-publish`
id("de.siphalor.tweed5.root-properties")
}
group = "de.siphalor.tweed5"
version = project.property("tweed5.version").toString()
dependencies {
rootProject.subprojects.forEach { subproject ->
subproject.plugins.withId("jacoco") {
jacocoAggregation(project(subproject.path))
}
}
}
reporting {
reports {
val aggregatedCoverageReport by creating(JacocoCoverageReport::class) {
testSuiteName = "test"
}
}
}

View File

@@ -0,0 +1,12 @@
plugins {
id("de.siphalor.tweed5.base-module")
id("de.siphalor.tweed5.minecraft.mod.dummy")
id("de.siphalor.tweed5.shadow.explicit")
}
dependencies {
implementation(project(":tweed5-core"))
implementation(project(":tweed5-default-extensions"))
testImplementation(project(":tweed5-serde-gson"))
}

View File

@@ -0,0 +1,2 @@
module.name = Tweed 5 Comment Loader Extension
module.description = Tweed 5 module that allows dynamically loading comments from data files, e.g., for i18n

View File

@@ -0,0 +1,16 @@
package de.siphalor.tweed5.commentloaderextension.api;
import de.siphalor.tweed5.commentloaderextension.impl.CommentLoaderExtensionImpl;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
public interface CommentLoaderExtension extends TweedExtension {
Class<? extends CommentLoaderExtension> DEFAULT = CommentLoaderExtensionImpl.class;
String EXTENSION_ID = "comment-loader";
void loadComments(TweedDataReader reader, CommentPathProcessor pathProcessor);
default String getId() {
return EXTENSION_ID;
}
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.commentloaderextension.api;
public interface CommentPathProcessor {
MatchStatus matches(String path);
String process(String path);
enum MatchStatus {
YES,
NO,
MAYBE_DEEPER,
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.commentloaderextension.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,212 @@
package de.siphalor.tweed5.commentloaderextension.impl;
import de.siphalor.tweed5.commentloaderextension.api.CommentLoaderExtension;
import de.siphalor.tweed5.commentloaderextension.api.CommentPathProcessor;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.dataapi.api.IntuitiveVisitingTweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingConfigEntryVisitor;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Getter;
import lombok.Value;
import lombok.extern.apachecommons.CommonsLog;
import org.jspecify.annotations.Nullable;
import java.util.*;
@CommonsLog
public class CommentLoaderExtensionImpl implements CommentLoaderExtension, CommentModifyingExtension {
private final ConfigContainer<?> configContainer;
private final PatchworkPartAccess<String> loadedCommentAccess;
private @Nullable CommentExtension commentExtension;
public CommentLoaderExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext setupContext) {
this.configContainer = configContainer;
setupContext.registerExtension(CommentExtension.class);
loadedCommentAccess = setupContext.registerEntryExtensionData(String.class);
}
@Override
public void extensionsFinalized() {
commentExtension = configContainer.extension(CommentExtension.class)
.orElseThrow(() -> new IllegalStateException("CommentExtension not found"));
}
@Override
public Middleware<CommentProducer> commentMiddleware() {
return new Middleware<CommentProducer>() {
@Override
public String id() {
return EXTENSION_ID;
}
@Override
public Set<String> mustComeBefore() {
return Collections.singleton(Middleware.DEFAULT_START);
}
@Override
public Set<String> mustComeAfter() {
return Collections.emptySet();
}
@Override
public CommentProducer process(CommentProducer inner) {
return entry -> {
String loadedComment = entry.extensionsData().get(loadedCommentAccess);
String innerComment = inner.createComment(entry);
if (loadedComment != null) {
if (innerComment.isEmpty()) {
return loadedComment;
} else {
return innerComment + loadedComment;
}
}
return innerComment;
};
}
};
}
@Override
public void loadComments(TweedDataReader reader, CommentPathProcessor pathProcessor) {
if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.EXTENSIONS_SETUP) <= 0) {
throw new IllegalStateException("Comments cannot be loaded before the extensions are finalized");
}
CollectingCommentsVisitor collectingCommentsVisitor = new CollectingCommentsVisitor(pathProcessor);
try {
new IntuitiveVisitingTweedDataReader(collectingCommentsVisitor).readMap(reader);
} catch (TweedDataReadException e) {
log.error("Failed to load comments", e);
}
Map<String, String> commentsByKey = collectingCommentsVisitor.commentsByKey();
PathTracking pathTracking = PathTracking.create();
configContainer.rootEntry().visitInOrder(new PathTrackingConfigEntryVisitor(
entry -> {
String key = pathTracking.currentPath();
if (!key.isEmpty() && key.charAt(0) == '.') {
key = key.substring(1);
}
entry.extensionsData().set(loadedCommentAccess, commentsByKey.get(key));
},
pathTracking
));
if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) >= 0) {
assert commentExtension != null;
commentExtension.recomputeFullComments();
}
}
private static class CollectingCommentsVisitor implements TweedDataVisitor {
private final CommentPathProcessor pathProcessor;
@Getter
private final Map<String, String> commentsByKey = new HashMap<>();
private final Deque<State> stateStack = new ArrayDeque<>();
private State currentState = new State(CommentPathProcessor.MatchStatus.MAYBE_DEEPER, "");
public CollectingCommentsVisitor(CommentPathProcessor pathProcessor) {
this.pathProcessor = pathProcessor;
stateStack.push(currentState);
}
@Override
public void visitNull() {}
@Override
public void visitBoolean(boolean value) {}
@Override
public void visitByte(byte value) {}
@Override
public void visitShort(short value) {}
@Override
public void visitInt(int value) {}
@Override
public void visitLong(long value) {}
@Override
public void visitFloat(float value) {}
@Override
public void visitDouble(double value) {}
@Override
public void visitString(String value) {
if (currentState.matchStatus() == CommentPathProcessor.MatchStatus.YES) {
commentsByKey.put(pathProcessor.process(currentState.key()), value);
}
}
@Override
public void visitListStart() {
stateStack.push(State.IGNORED);
}
@Override
public void visitListEnd() {
stateStack.pop();
}
@Override
public void visitMapStart() {
stateStack.push(currentState);
currentState = State.IGNORED;
}
@Override
public void visitMapEntryKey(String key) {
State state = stateStack.peek();
assert state != null;
if (state.matchStatus() == CommentPathProcessor.MatchStatus.NO) {
return;
}
String fullPath;
if (state.key().isEmpty()) {
fullPath = key;
} else {
fullPath = state.key() + "." + key;
}
CommentPathProcessor.MatchStatus matchStatus = pathProcessor.matches(fullPath);
if (matchStatus == CommentPathProcessor.MatchStatus.NO) {
currentState = State.IGNORED;
} else {
currentState = new State(matchStatus, fullPath);
}
}
@Override
public void visitMapEnd() {
currentState = stateStack.pop();
}
@Override
public void visitDecoration(TweedDataDecoration decoration) {}
@Value
private static class State {
private static final State IGNORED = new State(CommentPathProcessor.MatchStatus.NO, "");
CommentPathProcessor.MatchStatus matchStatus;
String key;
}
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.commentloaderextension.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,112 @@
package de.siphalor.tweed5.commentloaderextension.impl;
import com.google.gson.GsonBuilder;
import de.siphalor.tweed5.commentloaderextension.api.CommentLoaderExtension;
import de.siphalor.tweed5.commentloaderextension.api.CommentPathProcessor;
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.defaultextensions.comment.api.CommentExtension;
import de.siphaolor.tweed5.data.gson.GsonReader;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
class CommentLoaderExtensionImplTest {
@SuppressWarnings({"unchecked", "rawtypes"})
@Test
@SneakyThrows
void test() {
var configContainer = new DefaultConfigContainer<Map<String, Object>>();
configContainer.registerExtension(CommentLoaderExtension.class);
configContainer.finishExtensionSetup();
var nestedIntEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class);
var nestedEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>) (Class) Map.class,
HashMap::newHashMap,
sequencedMap(List.of(
entry("int", nestedIntEntry)
))
);
var intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class);
var rootEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>)(Class) Map.class,
HashMap::newHashMap,
sequencedMap(List.of(
entry("nested", nestedEntry),
entry("int", intEntry)
))
);
configContainer.attachTree(rootEntry);
configContainer.initialize();
CommentLoaderExtension extension = configContainer.extension(CommentLoaderExtension.class).orElseThrow();
// language=json
var text = """
{
"test": {
"description": "Root comment"
},
"test.int.description": "What an int!",
"test.nested": {
"description": "Comment for nested entry",
"int.description": "A cool nested entry"
}
}
""";
try (var reader = new GsonReader(new GsonBuilder().create().newJsonReader(new StringReader(text)))) {
extension.loadComments(
reader,
new CommentPathProcessor() {
private static final String PREFIX_RAW = "test";
private static final String PREFIX = PREFIX_RAW + ".";
private static final String SUFFIX = ".description";
@Override
public MatchStatus matches(String path) {
if (path.equals(PREFIX_RAW)) {
return MatchStatus.MAYBE_DEEPER;
} else if (path.startsWith(PREFIX)) {
if (path.endsWith(SUFFIX)) {
return MatchStatus.YES;
} else {
return MatchStatus.MAYBE_DEEPER;
}
} else {
return MatchStatus.NO;
}
}
@Override
public String process(String path) {
return path.substring(
PREFIX.length(),
Math.max(PREFIX.length(), path.length() - SUFFIX.length())
);
}
}
);
}
CommentExtension commentExtension = configContainer.extension(CommentExtension.class).orElseThrow();
assertThat(commentExtension.getFullComment(rootEntry)).isEqualTo("Root comment");
assertThat(commentExtension.getFullComment(nestedEntry)).isEqualTo("Comment for nested entry");
assertThat(commentExtension.getFullComment(nestedIntEntry)).isEqualTo("A cool nested entry");
assertThat(commentExtension.getFullComment(intEntry)).isEqualTo("What an int!");
}
}

View File

@@ -0,0 +1,3 @@
plugins {
id("de.siphalor.tweed5.base-module")
}

View File

@@ -0,0 +1,3 @@
module.name = Tweed 5 Construct
module.description = Provides a generic factory system for creating instances of subclasses with predefined constructor arguments. \
This solves Java's lack of constructor inheritance by enabling flexible object instantiation patterns.

View File

@@ -0,0 +1,19 @@
package de.siphalor.tweed5.construct.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Provides more information about a parameter of a {@link TweedConstruct}.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConstructParameter {
/**
* Allows defining a named parameter with the given name.
* Parameters with this set are not considered as typed parameters.
*/
String name();
}

View File

@@ -0,0 +1,21 @@
package de.siphalor.tweed5.construct.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates a method or constructor that should be used for construction using {@link TweedConstructFactory}.
* <p>
* There must only be a single annotation for a certain target class on any constructor or static method of a class.
*/
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TweedConstruct {
/**
* Defines the target base class that this constructor may be used to create.
* This is the base class defined in {@link TweedConstructFactory#builder(Class)}.
*/
Class<?> value();
}

View File

@@ -0,0 +1,98 @@
package de.siphalor.tweed5.construct.api;
import de.siphalor.tweed5.construct.impl.TweedConstructFactoryImpl;
import org.jetbrains.annotations.CheckReturnValue;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
/**
* A factory that allows to construct instances of subclasses of a specific type.
* <p>
* This factory basically extends the interfaces' contract to include
* a public constructor or public static method with any of the defined arguments.
* <p>
* The factory should usually be defined as a public static final member of the base class.
*
* @param <T> the base class/interface
*/
public interface TweedConstructFactory<T> {
/**
* Starts building a new factory for the given base class.
*/
static <T> TweedConstructFactory.FactoryBuilder<T> builder(Class<T> baseClass) {
return TweedConstructFactoryImpl.builder(baseClass);
}
/**
* Starts the instantiation process for a subclass.
* All defined arguments must be bound to values.
*/
@CheckReturnValue
@Contract(pure = true)
<C extends T> Construct<C> construct(Class<C> subClass);
/**
* Builder for the factory.
*/
interface FactoryBuilder<T> {
/**
* Defines a new typed argument of the given type.
*/
@Contract(mutates = "this", value = "_ -> this")
<A> FactoryBuilder<T> typedArg(Class<A> argType);
/**
* Defines a new named argument with the given name and value type.
*/
@Contract(mutates = "this", value = "_, _ -> this")
<A> FactoryBuilder<T> namedArg(String name, Class<A> argType);
/**
* Builds the factory.
*/
@Contract(pure = true)
TweedConstructFactory<T> build();
}
/**
* Builder-style helper for the instantiation process.
* <p>
* Allows to successively bind all previously defined arguments to actual values.
* <p>
* Any method call in this class may perform checks against the defined arguments and throw according exceptions.
*/
interface Construct<C> {
/**
* Binds a value to a typed argument of the exact same class.
* <p>
* This will not work if the given value merely inherits from the defined class.
* Use {@link #typedArg(Class, Object)} for these cases instead.
* @see #namedArg(String, Object)
*/
@Contract(mutates = "this", value = "_ -> this")
<A> Construct<C> typedArg(A value);
/**
* Binds a value to a typed argument of the given type.
* <p>
* This allows binding the value to super classes of the value.
* @see #typedArg(Object)
* @see #namedArg(String, Object)
*/
@Contract(mutates = "this", value = "_, _ -> this")
<A> Construct<C> typedArg(Class<? super A> argType, @Nullable A value);
/**
* Binds a value to a named argument.
* @see #typedArg(Object)
*/
@Contract(mutates = "this", value = "_, _ -> this")
<A> Construct<C> namedArg(String name, @Nullable A value);
/**
* Finishes the binding and actually constructs the class.
*/
@Contract(pure = true)
C finish();
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.construct.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.construct.impl;
import lombok.Value;
@Value
class Entry<K, V> {
K key;
V value;
}

View File

@@ -0,0 +1,460 @@
package de.siphalor.tweed5.construct.impl;
import de.siphalor.tweed5.construct.api.ConstructParameter;
import de.siphalor.tweed5.construct.api.TweedConstruct;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import lombok.*;
import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
@Getter(AccessLevel.PACKAGE)
public class TweedConstructFactoryImpl<T> implements TweedConstructFactory<T> {
private static final int CONSTRUCTOR_MODIFIERS = Modifier.PUBLIC;
private static final int STATIC_METHOD_MODIFIERS = Modifier.PUBLIC | Modifier.STATIC;
private final Class<T> constructBaseClass;
private final Set<Class<?>> typedArgs;
private final Map<String, Class<?>> namedArgs;
private final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
private final Map<Class<?>, Optional<ConstructTarget<?>>> cachedConstructTargets = new HashMap<>();
@SuppressWarnings("unused")
private final ReadWriteLock cachedConstructTargetsLock = new ReentrantReadWriteLock();
public static <T> TweedConstructFactoryImpl.FactoryBuilder<T> builder(Class<T> baseClass) {
return new FactoryBuilder<>(baseClass);
}
@Override
public <C extends T> TweedConstructFactory.Construct<C> construct(Class<C> subClass) {
return new Construct<>(getConstructTarget(subClass));
}
private <C extends T> ConstructTarget<C> getConstructTarget(Class<C> type) {
ConstructTarget<C> cachedConstructTarget = readConstructTargetFromCache(type);
if (cachedConstructTarget != null) {
return cachedConstructTarget;
}
ConstructTarget<C> constructTarget = locateConstructTarget(type);
cacheConstructTarget(type, constructTarget);
return constructTarget;
}
@Locked.Read("cachedConstructTargetsLock")
private <C extends T> @Nullable ConstructTarget<C> readConstructTargetFromCache(Class<C> type) {
Optional<ConstructTarget<?>> cachedConstructTarget = cachedConstructTargets.get(type);
if (cachedConstructTarget != null) {
if (!cachedConstructTarget.isPresent()) {
throw new IllegalStateException("Could not locate construct for " + type.getName());
} else {
//noinspection unchecked
return (ConstructTarget<C>) cachedConstructTarget.get();
}
}
return null;
}
@Locked.Write("cachedConstructTargetsLock")
private <C extends T> void cacheConstructTarget(Class<C> type, ConstructTarget<C> constructTarget) {
cachedConstructTargets.put(type, Optional.of(constructTarget));
}
private <C extends T> ConstructTarget<C> locateConstructTarget(Class<C> type) {
if (!constructBaseClass.isAssignableFrom(type)) {
throw new IllegalArgumentException(
"Type " + type.getName() + " is not a subclass of " + constructBaseClass.getName()
);
}
Collection<Constructor<?>> constructorCandidates = findConstructorCandidates(type);
Collection<Method> staticConstructorCandidates = findStaticConstructorCandidates(type);
List<Executable> annotated = Stream.concat(constructorCandidates.stream(), staticConstructorCandidates.stream())
.filter(candidate -> {
TweedConstruct annotation = candidate.getAnnotation(TweedConstruct.class);
return annotation != null && annotation.value().equals(constructBaseClass);
})
.collect(Collectors.toList());
if (annotated.size() > 1) {
throw new IllegalStateException(
"Found multiple matching constructors for " + type.getName()
+ " annotated with a matching TweedConstruct for " + constructBaseClass.getName() + ": "
+ annotated
);
} else if (annotated.size() == 1) {
return resolveConstructTarget(type, annotated.get(0));
} else if (constructorCandidates.size() == 1) {
return resolveConstructTarget(type, constructorCandidates.iterator().next());
} else {
throw new IllegalStateException(
"Failed to determine actual constructor on " + type.getName()
+ " for " + constructBaseClass.getName() + ". "
+ "Constructor candidates: " + constructorCandidates + "; "
+ "Static method candidates: " + staticConstructorCandidates + ". "
+ "The desired constructor should be marked with @" + TweedConstruct.class.getName()
);
}
}
private Collection<Constructor<?>> findConstructorCandidates(Class<?> type) {
return Arrays.stream(type.getConstructors())
.filter(constructor -> (constructor.getModifiers() & CONSTRUCTOR_MODIFIERS) == CONSTRUCTOR_MODIFIERS)
.collect(Collectors.toList());
}
private Collection<Method> findStaticConstructorCandidates(Class<?> type) {
return Arrays.stream(type.getDeclaredMethods())
.filter(method -> (method.getModifiers() & STATIC_METHOD_MODIFIERS) == STATIC_METHOD_MODIFIERS)
.filter(method -> type.isAssignableFrom(method.getReturnType()))
.collect(Collectors.toList());
}
private <C extends T> ConstructTarget<C> resolveConstructTarget(Class<C> type, Executable executable) {
Object[] argOrder = new Object[executable.getParameterCount()];
Map<Class<?>, List<Parameter>> typedParameters = new HashMap<>();
Map<String, List<Parameter>> namedParameters = new HashMap<>();
Parameter[] parameters = executable.getParameters();
boolean issue = false;
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
ConstructParameter annotation = parameter.getAnnotation(ConstructParameter.class);
if (annotation != null) {
String name = annotation.name();
List<Parameter> named = namedParameters.computeIfAbsent(name, n -> new ArrayList<>());
named.add(parameter);
Class<?> argType = namedArgs.get(name);
argOrder[i] = name;
if (!issue && (
named.size() > 1
|| argType == null
|| !boxClass(parameter.getType()).isAssignableFrom(argType)
)) {
issue = true;
}
} else {
Class<?> paramType = boxClass(parameter.getType());
List<Parameter> typed = typedParameters.computeIfAbsent(paramType, n -> new ArrayList<>());
typed.add(parameter);
argOrder[i] = paramType;
if (!issue && (typed.size() > 1 || !typedArgs.contains(paramType))) {
issue = true;
}
}
}
if (issue) {
throw new IllegalStateException(
createConstructorTargetArgCheckFailMessage(executable, typedParameters, namedParameters)
);
}
return new ConstructTarget<>(type, argOrder, createInvokerFromCandidate(type, executable));
}
private <C> Function<@Nullable Object[], C> createInvokerFromCandidate(Class<C> type, Executable executable) {
MethodHandle handle;
try {
if (executable instanceof Method) {
handle = lookup.unreflect((Method) executable);
} else if (executable instanceof Constructor) {
handle = lookup.unreflectConstructor((Constructor<?>) executable);
} else {
throw new IllegalStateException("Unsupported executable type: " + executable);
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Constructor for type " + type.getName() + " is not accessible", e);
}
return args -> {
try {
//noinspection unchecked
return (C) handle.invokeWithArguments(args);
} catch (ClassCastException | WrongMethodTypeException e) {
throw new IllegalStateException(
"Failed to construct type " + type.getName() + " as " + constructBaseClass.getName(), e
);
} catch (Throwable e) {
throw new RuntimeException(
"Uncaught exception during construct of type " + type.getName()
+ " as " + constructBaseClass.getName(),
e
);
}
};
}
private String createConstructorTargetArgCheckFailMessage(
Executable executable,
Map<Class<?>, List<Parameter>> typedParameters,
Map<String, List<Parameter>> namedParameters
) {
StringBuilder sb = new StringBuilder();
sb.append("Failed to resolve parameters for ");
sb.append(executable);
sb.append(" (for ");
sb.append(constructBaseClass.getName());
sb.append("). The following issues have been detected:");
Set<Class<?>> unexpectedTypes = new HashSet<>(typedParameters.keySet());
unexpectedTypes.removeAll(typedArgs);
if (!unexpectedTypes.isEmpty()) {
for (Class<?> unexpectedType : unexpectedTypes) {
sb.append("\n - Typed parameter of type ");
sb.append(unexpectedType.getName());
sb.append(" is not known: ");
sb.append(typedParameters.get(unexpectedType));
}
}
Set<String> unexpectedNames = new HashSet<>(namedParameters.keySet());
unexpectedNames.removeAll(namedArgs.keySet());
if (!unexpectedNames.isEmpty()) {
for (String unexpectedName : unexpectedNames) {
sb.append("\n - Named parameter ");
sb.append(unexpectedName);
sb.append(" is not known: ");
sb.append(namedParameters.get(unexpectedName));
}
}
typedParameters.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> sb.append("\n - Duplicate typed parameter ")
.append(entry.getKey())
.append(": ")
.append(entry.getValue()));
namedParameters.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> sb.append("\n - Duplicate named parameter ")
.append(entry.getKey())
.append(": ")
.append(entry.getValue()));
namedParameters.entrySet().stream()
.filter(entry -> !unexpectedNames.contains(entry.getKey()))
.flatMap(entry -> entry.getValue().stream().map(parameter -> new Entry<>(entry.getKey(), parameter)))
.forEach(entry -> {
Class<?> argType = namedArgs.get(entry.key());
if (!boxClass(entry.value().getType()).isAssignableFrom(argType)) {
sb.append("\n - Named parameter ").append(entry.key());
sb.append(" expects values of type ").append(argType.getName());
sb.append(": ").append(entry.value());
}
});
return sb.toString();
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class FactoryBuilder<T> implements TweedConstructFactory.FactoryBuilder<T> {
private final Class<T> constructBaseClass;
private final Set<Class<?>> typedArgs = new HashSet<>();
private final Map<String, Class<?>> namedArgs = new HashMap<>();
@Override
public <A> TweedConstructFactory.FactoryBuilder<T> typedArg(Class<A> argType) {
argType = boxClass(argType);
if (typedArgs.contains(argType)) {
throw new IllegalArgumentException("Argument for type " + argType + " has already been registered");
}
typedArgs.add(argType);
return this;
}
@Override
public <A> TweedConstructFactory.FactoryBuilder<T> namedArg(String name, Class<A> argType) {
Class<?> existingArgType = namedArgs.get(name);
if (existingArgType != null) {
throw new IllegalArgumentException(
"Argument for name " + name + " has already been registered; "
+ "existing type " + existingArgType.getName() + "; "
+ "new type " + argType.getName()
);
}
namedArgs.put(name, boxClass(argType));
return this;
}
@Override
public TweedConstructFactory<T> build() {
return new TweedConstructFactoryImpl<>(
constructBaseClass,
typedArgs,
namedArgs
);
}
}
@RequiredArgsConstructor
private class Construct<C> implements TweedConstructFactory.Construct<C> {
private final ConstructTarget<C> target;
private final Map<Class<?>, @Nullable Object> typedArgValues = new HashMap<>();
private final Map<String, @Nullable Object> namedArgValues = new HashMap<>();
@Override
public <A> TweedConstructFactory.Construct<C> typedArg(A value) {
requireTypedArgExists(value.getClass(), value);
typedArgValues.put(value.getClass(), value);
return this;
}
@Override
public <A> TweedConstructFactory.Construct<C> typedArg(Class<? super A> argType, @Nullable A value) {
argType = boxClass(argType);
if (value != null && !argType.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException(
"Typed argument for type " + argType.getName()
+ " is of incorrect type " + value.getClass().getName()
+ ", value: " + value
);
}
requireTypedArgExists(argType, value);
typedArgValues.put(argType, value);
return this;
}
private <A> void requireTypedArgExists(Class<?> type, @Nullable A value) {
if (!typedArgs.contains(type)) {
throw new IllegalArgumentException(
"Typed argument for type " + type.getName() + " does not exist, value: " + value
);
}
}
@Override
public <A> TweedConstructFactory.Construct<C> namedArg(String name, @Nullable A value) {
Class<?> argType = namedArgs.get(name);
if (argType == null) {
throw new IllegalArgumentException(
"Named argument for name " + name + " does not exist, value: " + value
);
} else if (value != null && !argType.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException(
"Named argument for name " + name + " is defined with type " + argType.getName() +
" but got type " + value.getClass().getName() + " with value " + value
);
}
namedArgValues.put(name, value);
return this;
}
@Override
public C finish() {
checkAllArgsFilled();
@Nullable Object[] argValues = new Object[target.argOrder.length];
for (int i = 0; i < target.argOrder.length; i++) {
Object arg = target.argOrder[i];
if (arg instanceof Class<?>) {
argValues[i] = typedArgValues.get((Class<?>) arg);
} else if (arg instanceof String) {
argValues[i] = namedArgValues.get((String) arg);
} else {
throw new IllegalStateException("Encountered illegal argument indicator " + arg + " at " + i);
}
}
return target.invoker.apply(argValues);
}
private void checkAllArgsFilled() {
Set<Class<?>> missingTypedArgs = Collections.emptySet();
if (typedArgValues.size() != typedArgs.size()) {
missingTypedArgs = new HashSet<>(typedArgs);
missingTypedArgs.removeAll(typedArgValues.keySet());
}
Set<String> missingNamedArgs = Collections.emptySet();
if (namedArgValues.size() != namedArgs.size()) {
missingNamedArgs = new HashSet<>(namedArgs.keySet());
missingNamedArgs.removeAll(namedArgValues.keySet());
}
if (!missingTypedArgs.isEmpty() || !missingNamedArgs.isEmpty()) {
throw new IllegalArgumentException(createMissingArgsMessage(missingTypedArgs, missingNamedArgs));
}
}
private String createMissingArgsMessage(Set<Class<?>> missingTypedArgs, Set<String> missingNamedArgs) {
StringBuilder message = new StringBuilder()
.append("Missing arguments for construction of ")
.append(target.type().getName())
.append(" as ")
.append(constructBaseClass.getName())
.append(", missing: ");
if (!missingTypedArgs.isEmpty()) {
message.append("typed args (");
boolean requiresDelimiter = false;
for (Class<?> missingTypedArg : missingTypedArgs) {
if (requiresDelimiter) {
message.append(", ");
}
message.append(missingTypedArg.getName());
requiresDelimiter = true;
}
message.append(") ");
}
if (!missingNamedArgs.isEmpty()) {
message.append("named args (");
boolean requiresDelimiter = false;
for (String missingNamedArg : missingNamedArgs) {
if (requiresDelimiter) {
message.append(", ");
}
message.append(missingNamedArg);
requiresDelimiter = true;
}
message.append(") ");
}
return message.toString();
}
}
/**
* Boxes primitive classes into their reference variants.
* Allows for easier class comparison down the line.
*/
@SuppressWarnings("unchecked")
static <V> Class<V> boxClass(Class<V> type) {
if (!type.isPrimitive()) {
return type;
}
if (type == boolean.class) {
return (Class<V>) Boolean.class;
} else if (type == byte.class) {
return (Class<V>) Byte.class;
} else if (type == char.class) {
return (Class<V>) Character.class;
} else if (type == short.class) {
return (Class<V>) Short.class;
} else if (type == int.class) {
return (Class<V>) Integer.class;
} else if (type == long.class) {
return (Class<V>) Long.class;
} else if (type == float.class) {
return (Class<V>) Float.class;
} else if (type == double.class) {
return (Class<V>) Double.class;
} else if (type == void.class) {
return (Class<V>) Void.class;
} else {
throw new IllegalArgumentException("Unsupported primitive type " + type);
}
}
@Value
private static class ConstructTarget<C> {
Class<?> type;
Object[] argOrder;
Function<@Nullable Object[], C> invoker;
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.construct.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,483 @@
package de.siphalor.tweed5.construct.impl;
import de.siphalor.tweed5.construct.api.ConstructParameter;
import de.siphalor.tweed5.construct.api.TweedConstruct;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import lombok.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@SuppressWarnings("unused")
class TweedConstructFactoryImplTest {
@SuppressWarnings("unchecked")
@Test
void factoryBuilder() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
builder.typedArg(Integer.class).typedArg(String.class);
builder.namedArg("hey", String.class).namedArg("ho", String.class);
TweedConstructFactory<DummyBase> factory = builder.build();
assertThat(factory)
.asInstanceOf(type(TweedConstructFactoryImpl.class))
.satisfies(
f -> assertThat(f.constructBaseClass()).isEqualTo(DummyBase.class),
f -> assertThat(f.typedArgs())
.containsExactlyInAnyOrder(Integer.class, String.class),
f -> assertThat(f.namedArgs())
.containsEntry("hey", String.class)
.containsEntry("ho", String.class)
.hasSize(2)
);
}
@SuppressWarnings("unchecked")
@Test
void factoryBuilderPrimitives() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
builder.typedArg(int.class).typedArg(long.class);
builder.namedArg("bool", boolean.class).namedArg("byte", byte.class);
TweedConstructFactory<DummyBase> factory = builder.build();
assertThat(factory)
.asInstanceOf(type(TweedConstructFactoryImpl.class))
.satisfies(
f -> assertThat(f.typedArgs())
.containsExactlyInAnyOrder(Integer.class, Long.class),
f -> assertThat(f.namedArgs())
.containsEntry("bool", Boolean.class)
.containsEntry("byte", Byte.class)
.hasSize(2)
);
}
@Test
void factoryBuilderDuplicateTypedArgs() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
assertThatThrownBy(() -> {
builder.typedArg(Integer.class).typedArg(String.class).typedArg(Integer.class);
builder.build();
}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Integer");
}
@Test
void factoryBuilderDuplicateNamedArgs() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
assertThatThrownBy(() -> {
builder.namedArg("hey", String.class).namedArg("ho", String.class).namedArg("hey", Integer.class);
builder.build();
}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("hey");
}
@Test
void constructMissingInheritance() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
//noinspection unchecked,RedundantCast
assertThatThrownBy(() ->
factory.construct((Class<? extends DummyBase>) (Class<?>) MissingInheritance.class)
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(DummyBase.class.getName());
}
@Test
void constructPrivateConstructor() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(PrivateConstructor.class)).isInstanceOf(IllegalStateException.class);
}
@Test
void constructConflictingConstructors() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(ConstructorConflict.class))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructConflictingStatics() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(StaticConflict.class))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructConflictingMixed() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(MixedConflict.class))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructMissingTypedValue() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(SingleConstructor.class)
.namedArg("user", "Siphalor")
.namedArg("context", "world")
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Integer");
}
@Test
void constructMissingNamedValue() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(SingleConstructor.class)
.typedArg(123)
.namedArg("context", "world")
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("user");
}
@Test
void constructForSingleConstructor() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.namedArg("other", String.class)
.build();
val result = factory.construct(SingleConstructor.class)
.typedArg(123)
.namedArg("user", "Siphalor")
.namedArg("context", "world")
.namedArg("other", "something")
.finish();
assertThat(result).isEqualTo(new SingleConstructor(123, "Siphalor", "world"));
}
@Test
void constructForStatic() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.namedArg("other", String.class)
.build();
val result = factory.construct(Static.class)
.typedArg(123)
.namedArg("user", "Siphalor")
.namedArg("context", "world")
.namedArg("other", "something")
.finish();
assertThat(result).isEqualTo(new Static(1230, "Siphalor", "static"));
}
@ParameterizedTest
@CsvSource(
{
"de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyBase, base, 4560",
"de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyOtherBase, other, -456",
"de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyAltBase, alt, -4560",
}
)
void constructFindBase(Class<? super FindBase> base, String origin, int value) {
val factory = TweedConstructFactoryImpl.builder(base).typedArg(int.class).build();
val result = factory.construct(FindBase.class).typedArg(456).finish();
assertThat(result.origin()).isEqualTo(origin);
assertThat(result.value()).isEqualTo(value);
}
@Test
void constructPrimitives() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.typedArg(long.class)
.build();
val result = factory.construct(Primitives.class)
.typedArg(1)
.typedArg(2L)
.finish();
assertThat(result).isEqualTo(new Primitives(1, 2L));
}
@Test
void constructNamedCasting() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).namedArg("test", Integer.class).build();
val result = factory.construct(NamedCasting.class).namedArg("test", 1234).finish();
assertThat(result.value()).isEqualTo(1234);
}
@Test
void constructDuplicateParams() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(String.class)
.typedArg(Long.class)
.namedArg("number", int.class)
.build();
assertThatThrownBy(() -> factory.construct(DuplicateParams.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("java.lang.String", "number")
.containsIgnoringCase("typed")
.containsIgnoringCase("named")
.hasLineCount(3);
}
@Test
void constructUnexpectedTypedParameter() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Long.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() -> factory.construct(Static.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("java.lang.Integer")
.containsIgnoringCase("typed")
.hasLineCount(2);
}
@Test
void constructUnexpectedNamedParameter() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("other", String.class)
.build();
assertThatThrownBy(() -> factory.construct(Static.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("user", "java.lang.String")
.containsIgnoringCase("named")
.hasLineCount(2);
}
@Test
void constructIllegalNamedType() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", Long.class)
.build();
assertThatThrownBy(() -> factory.construct(Static.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("user", "java.lang.String", "java.lang.Long")
.containsIgnoringCase("named")
.hasLineCount(2);
}
@Test
void constructFinishUnknownTypedArgument() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg(12)
.namedArg("user", "Someone")
.typedArg(567L)
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Long");
}
@Test
void constructFinishUnknownNamedArgument() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg(12)
.namedArg("user", "Someone")
.namedArg("other", "test")
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("other");
}
@Test
void constructFinishNamedArgumentWrongType() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg(12)
.namedArg("user", 456L)
.finish())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("user", "java.lang.String", "java.lang.Long");
}
@Test
void constructFinishInconsistentNamedArgument() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
//noinspection unchecked
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg((Class<Object>)(Class<?>) String.class, 123)
.finish()
).isInstanceOf(IllegalArgumentException.class);
}
@ParameterizedTest
@CsvSource(
{
"boolean, java.lang.Boolean",
"java.lang.Boolean, java.lang.Boolean",
"byte, java.lang.Byte",
"java.lang.Byte, java.lang.Byte",
"char, java.lang.Character",
"java.lang.Character, java.lang.Character",
"short, java.lang.Short",
"java.lang.Short, java.lang.Short",
"int, java.lang.Integer",
"java.lang.Integer, java.lang.Integer",
"long, java.lang.Long",
"java.lang.Long, java.lang.Long",
"float, java.lang.Float",
"java.lang.Float, java.lang.Float",
"double, java.lang.Double",
"java.lang.Double, java.lang.Double",
"void, java.lang.Void",
"java.lang.Void, java.lang.Void",
"java.lang.String, java.lang.String",
}
)
void boxClass(Class<?> type, Class<?> expected) {
assertThat(TweedConstructFactoryImpl.boxClass(type)).isEqualTo(expected);
}
interface DummyBase {
}
interface DummyOtherBase {
}
interface DummyAltBase {
}
public static class MissingInheritance {
@TweedConstruct(DummyBase.class)
public MissingInheritance() {
}
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class PrivateConstructor implements DummyBase {
}
public static class ConstructorConflict implements DummyBase {
@TweedConstruct(DummyBase.class)
public ConstructorConflict() {
}
@TweedConstruct(DummyBase.class)
public ConstructorConflict(String context) {
}
}
public static class StaticConflict implements DummyBase {
@TweedConstruct(DummyBase.class)
public static StaticConflict ofA() {
return null;
}
@TweedConstruct(DummyBase.class)
public static StaticConflict ofB() {
return null;
}
}
public static class MixedConflict implements DummyBase {
@TweedConstruct(DummyBase.class)
public MixedConflict() {
}
@TweedConstruct(DummyBase.class)
public static MixedConflict of() {
return null;
}
}
@Getter
@EqualsAndHashCode
public static class SingleConstructor implements DummyBase {
private final Integer times;
private final String user;
private final String context;
public SingleConstructor(
Integer times,
@ConstructParameter(name = "user") String user,
@ConstructParameter(name = "context") String context
) {
this.times = times;
this.user = user;
this.context = context;
}
}
@Value
public static class Static implements DummyBase {
Integer times;
String user;
String context;
@TweedConstruct(DummyBase.class)
public static Static of(Integer times, @ConstructParameter(name = "user") String user) {
return new Static(times * 10, user, "static");
}
}
@Value
@AllArgsConstructor
public static class FindBase implements DummyBase, DummyOtherBase, DummyAltBase {
String origin;
int value;
@TweedConstruct(DummyBase.class)
public FindBase(int value) {
this("base", value * 10);
}
@TweedConstruct(DummyOtherBase.class)
public static FindBase ofOther(int value) {
return new FindBase("other", value * -1);
}
@TweedConstruct(DummyAltBase.class)
public static FindBase ofAlt(int value) {
return new FindBase("alt", value * -10);
}
}
@Value
public static class Primitives implements DummyBase {
int a;
Long b;
}
@Value
public static class NamedCasting implements DummyBase {
Number value;
public NamedCasting(@ConstructParameter(name = "test") Number value) {
this.value = value;
}
}
@Value
public static class DuplicateParams implements DummyBase {
public DuplicateParams(
String one,
String two,
Long test,
@ConstructParameter(name = "number") int three,
@ConstructParameter(name = "number") int four
) {
}
}
}

View File

@@ -0,0 +1,9 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
implementation(project(":tweed5-construct"))
api(project(":tweed5-patchwork"))
api(project(":tweed5-utils"))
}

View File

@@ -0,0 +1,2 @@
module.name = Tweed 5 Core
module.description = Provides core APIs and functionality for Tweed 5, like entries, containers and extensions.

View File

@@ -0,0 +1,43 @@
package de.siphalor.tweed5.core.api.container;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.Optional;
/**
* The main wrapper for a config tree.<br />
* Holds certain global metadata like registered extensions and manages the initialization phases.
* @param <T> The class that the config tree represents
* @see ConfigContainerSetupPhase
*/
public interface ConfigContainer<T extends @Nullable Object> {
@SuppressWarnings("rawtypes")
TweedConstructFactory<ConfigContainer> FACTORY = TweedConstructFactory.builder(ConfigContainer.class).build();
ConfigContainerSetupPhase setupPhase();
default void registerExtensions(Class<? extends TweedExtension>... extensionClasses) {
for (Class<? extends TweedExtension> extensionClass : extensionClasses) {
registerExtension(extensionClass);
}
}
void registerExtension(Class<? extends TweedExtension> extensionClass);
<E extends TweedExtension> Optional<E> extension(Class<E> extensionClass);
Collection<TweedExtension> extensions();
void finishExtensionSetup();
void attachTree(ConfigEntry<T> rootEntry);
Patchwork createExtensionsData();
void initialize();
ConfigEntry<T> rootEntry();
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.container;
public enum ConfigContainerSetupPhase {
EXTENSIONS_SETUP,
TREE_SETUP,
TREE_ATTACHED,
INITIALIZED,
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.core.api.container;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,18 @@
package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.Getter;
@Getter
public abstract class BaseConfigEntry<T> implements ConfigEntry<T> {
private final ConfigContainer<?> container;
private final Class<T> valueClass;
private final Patchwork extensionsData;
public BaseConfigEntry(ConfigContainer<?> container, Class<T> valueClass) {
this.container = container;
this.valueClass = valueClass;
this.extensionsData = container.createExtensionsData();
}
}

View File

@@ -0,0 +1,45 @@
package de.siphalor.tweed5.core.api.entry;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
public interface CollectionConfigEntry<E, T extends Collection<E>> extends StructuredConfigEntry<T> {
@Override
default CollectionConfigEntry<E, T> apply(Consumer<ConfigEntry<T>> function) {
StructuredConfigEntry.super.apply(function);
return this;
}
@Override
default void visitInOrder(ConfigEntryValueVisitor visitor, @Nullable T value) {
if (value == null) {
return;
}
if (visitor.enterStructuredEntry(this, value)) {
int index = 0;
for (E item : value) {
String indexString = Integer.toString(index);
if (visitor.enterStructuredSubEntry("element", indexString)) {
elementEntry().visitInOrder(visitor, item);
visitor.leaveStructuredSubEntry("element", indexString);
}
index++;
}
visitor.leaveStructuredEntry(this, value);
}
}
@Override
default Map<String, ConfigEntry<?>> subEntries() {
return Collections.singletonMap("element", elementEntry());
}
ConfigEntry<E> elementEntry();
T instantiateCollection(int size);
}

View File

@@ -0,0 +1,16 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.function.Consumer;
public interface CompoundConfigEntry<T> extends StructuredConfigEntry<T> {
@Override
default CompoundConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
StructuredConfigEntry.super.apply(function);
return this;
}
<V> void set(T compoundValue, String key, V value);
<V> V get(T compoundValue, String key);
T instantiateCompoundValue();
}

View File

@@ -0,0 +1,32 @@
package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
import java.util.function.Function;
public interface ConfigEntry<T extends @Nullable Object> {
ConfigContainer<?> container();
Class<T> valueClass();
Patchwork extensionsData();
default ConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
function.accept(this);
return this;
}
default <R> R call(Function<ConfigEntry<T>, R> function) {
return function.apply(this);
}
void visitInOrder(ConfigEntryVisitor visitor);
void visitInOrder(ConfigEntryValueVisitor visitor, @Nullable T value);
T deepCopy(@NonNull T value);
}

View File

@@ -0,0 +1,20 @@
package de.siphalor.tweed5.core.api.entry;
public interface ConfigEntryValueVisitor {
<T> void visitEntry(ConfigEntry<T> entry, T value);
default <T> boolean enterStructuredEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
return true;
}
default boolean enterStructuredSubEntry(String entryKey, String valueKey) {
return true;
}
default void leaveStructuredSubEntry(String entryKey, String valueKey) {
}
default <T> void leaveStructuredEntry(ConfigEntry<T> entry, T value) {
}
}

View File

@@ -0,0 +1,20 @@
package de.siphalor.tweed5.core.api.entry;
public interface ConfigEntryVisitor {
void visitEntry(ConfigEntry<?> entry);
default boolean enterStructuredEntry(ConfigEntry<?> entry) {
visitEntry(entry);
return true;
}
default boolean enterStructuredSubEntry(String key) {
return true;
}
default void leaveStructuredSubEntry(String key) {
}
default void leaveStructuredEntry(ConfigEntry<?> entry) {
}
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.function.Consumer;
public interface SimpleConfigEntry<T> extends ConfigEntry<T> {
@Override
default SimpleConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
ConfigEntry.super.apply(function);
return this;
}
}

View File

@@ -0,0 +1,27 @@
package de.siphalor.tweed5.core.api.entry;
import java.util.Map;
import java.util.function.Consumer;
public interface StructuredConfigEntry<T> extends ConfigEntry<T> {
@Override
default StructuredConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
ConfigEntry.super.apply(function);
return this;
}
@Override
default void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterStructuredEntry(this)) {
subEntries().forEach((key, entry) -> {
if (visitor.enterStructuredSubEntry(key)) {
entry.visitInOrder(visitor);
visitor.leaveStructuredSubEntry(key);
}
});
visitor.leaveStructuredEntry(this);
}
}
Map<String, ConfigEntry<?>> subEntries();
}

View File

@@ -0,0 +1,23 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
public interface TweedExtension {
TweedConstructFactory<TweedExtension> FACTORY = TweedConstructFactory.builder(TweedExtension.class)
.typedArg(ConfigContainer.class)
.typedArg(TweedExtensionSetupContext.class)
.build();
String getId();
default void extensionsFinalized() {
}
default void initialize() {
}
default void initEntry(ConfigEntry<?> configEntry) {
}
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
public interface TweedExtensionSetupContext {
<E> PatchworkPartAccess<E> registerEntryExtensionData(Class<E> dataClass);
void registerExtension(Class<? extends TweedExtension> extensionClass);
}

View File

@@ -0,0 +1,120 @@
package de.siphalor.tweed5.core.api.middleware;
import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter;
import lombok.Getter;
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 = "";
@Getter
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 registerAll(Collection<Middleware<M>> middlewares) {
requireUnsealed();
for (Middleware<M> middleware : middlewares) {
if (middleware.id().isEmpty()) {
throw new IllegalArgumentException("Middleware id cannot be empty");
}
if (!this.middlewareIds.add(middleware.id())) {
throw new IllegalArgumentException("Middleware id already registered: " + middleware.id());
}
}
this.middlewares.addAll(middlewares);
}
@Override
public void register(Middleware<M> middleware) {
requireUnsealed();
if (middleware.id().isEmpty()) {
throw new IllegalArgumentException("Middleware id cannot be empty");
}
if (!middlewareIds.add(middleware.id())) {
throw new IllegalArgumentException("Middleware id already registered: " + middleware.id());
}
middlewares.add(middleware);
}
private void requireUnsealed() {
if (sealed) {
throw new IllegalStateException("Middleware container has already been sealed");
}
}
@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) {
StringBuilder messageBuilder = new StringBuilder("Found cycle in middleware dependencies: ");
e.cycleIndeces().forEach(index -> messageBuilder.append(allMentionedMiddlewareIds[index]).append(" -> "));
messageBuilder.append(allMentionedMiddlewareIds[e.cycleIndeces().iterator().next()]);
throw new IllegalStateException(messageBuilder.toString(), 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,20 @@
package de.siphalor.tweed5.core.api.middleware;
import java.util.Collections;
import java.util.Set;
public interface Middleware<M> {
String DEFAULT_START = "$default.start";
String DEFAULT_END = "$default.end";
String id();
default Set<String> mustComeBefore() {
return Collections.singleton(DEFAULT_END);
}
default Set<String> mustComeAfter() {
return Collections.singleton(DEFAULT_START);
}
M process(M inner);
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.core.api.middleware;
import java.util.Collection;
public interface MiddlewareContainer<M> extends Middleware<M> {
default void registerAll(Collection<Middleware<M>> middlewares) {
middlewares.forEach(this::register);
}
void register(Middleware<M> middleware);
void seal();
Collection<Middleware<M>> middlewares();
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.core.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,294 @@
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()) {
if (lastVisited == -1) {
break;
}
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())) {
Collection<Integer> reversed = new ArrayList<>(stack.size());
stack.descendingIterator().forEachRemaining(reversed::add);
throw new GraphCycleException(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++) {
for (int j = 0; j < Math.min(WORD_SIZE, leftBitCount); j++) {
sb.append("0");
}
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 @@
package de.siphalor.tweed5.core.generated;

View File

@@ -0,0 +1,264 @@
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.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.utils.api.collection.InheritanceMap;
import lombok.Getter;
import org.jspecify.annotations.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final Set<Class<? extends TweedExtension>> requestedExtensions = new HashSet<>();
private final InheritanceMap<TweedExtension> extensions = new InheritanceMap<>(TweedExtension.class);
private @Nullable ConfigEntry<T> rootEntry;
private @Nullable PatchworkFactory entryExtensionsDataPatchworkFactory;
@Override
public void registerExtension(Class<? extends TweedExtension> extensionClass) {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
requestedExtensions.add(extensionClass);
}
@Override
public void finishExtensionSetup() {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
PatchworkFactory.Builder entryExtensionDataPatchworkFactoryBuilder = PatchworkFactory.builder();
TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() {
@Override
public <E> PatchworkPartAccess<E> registerEntryExtensionData(Class<E> dataClass) {
return entryExtensionDataPatchworkFactoryBuilder.registerPart(dataClass);
}
@Override
public void registerExtension(Class<? extends TweedExtension> extensionClass) {
if (!extensions.containsAnyInstanceForClass(extensionClass)) {
requestedExtensions.add(extensionClass);
}
}
};
Set<Class<? extends TweedExtension>> abstractExtensionClasses = new HashSet<>();
while (true) {
if (!requestedExtensions.isEmpty()) {
Class<? extends TweedExtension> extensionClass = popFromIterable(requestedExtensions);
if (isAbstractClass(extensionClass)) {
abstractExtensionClasses.add(extensionClass);
} else {
extensions.put(instantiateExtension(extensionClass, extensionSetupContext));
}
} else if (!abstractExtensionClasses.isEmpty()) {
Class<? extends TweedExtension> extensionClass = popFromIterable(abstractExtensionClasses);
if (!extensions.containsAnyInstanceForClass(extensionClass)) {
extensions.put(instantiateAbstractExtension(extensionClass, extensionSetupContext));
}
} else {
break;
}
}
entryExtensionsDataPatchworkFactory = entryExtensionDataPatchworkFactoryBuilder.build();
setupPhase = ConfigContainerSetupPhase.TREE_SETUP;
extensions.values().forEach(TweedExtension::extensionsFinalized);
}
private static <T> T popFromIterable(Iterable<T> iterable) {
Iterator<T> iterator = iterable.iterator();
T value = iterator.next();
iterator.remove();
return value;
}
protected <E extends TweedExtension> E instantiateAbstractExtension(
Class<E> extensionClass,
TweedExtensionSetupContext setupContext
) {
try {
Field defaultField = extensionClass.getDeclaredField("DEFAULT");
if (defaultField.getType() != Class.class) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field has incorrect class " + defaultField.getType().getName() + "."
));
}
if ((defaultField.getModifiers() & Modifier.STATIC) == 0) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field is not static."
));
}
if ((defaultField.getModifiers() & Modifier.PUBLIC) == 0) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field is not public."
));
}
Class<?> defaultClass = (Class<?>) defaultField.get(null);
if (!extensionClass.isAssignableFrom(defaultClass)) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field contains class " + defaultClass.getName() + ", but that class "
+ "does not inherit from " + extensionClass.getName()
));
}
//noinspection unchecked
return instantiateExtension((Class<? extends E>) defaultClass, setupContext);
} catch (NoSuchFieldException ignored) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(extensionClass, null));
} catch (IllegalAccessException e) {
throw new IllegalStateException(
createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"Couldn't access DEFAULT field."
),
e
);
}
}
private String createAbstractExtensionInstantiationExceptionMessage(
Class<?> extensionClass,
@Nullable String detail
) {
StringBuilder sb = new StringBuilder();
sb.append("Requested extension class ").append(extensionClass.getName()).append(" is ");
if (extensionClass.isInterface()) {
sb.append("an interface ");
} else {
sb.append("an abstract class ");
}
sb.append("and cannot be instantiated directly.\n");
sb.append("As the extension developer you can declare a public static DEFAULT field containing the class of ");
sb.append("a default implementation.\n");
sb.append("As a user you can try registering an implementation of the extension class directly.");
if (detail != null) {
sb.append("\n");
sb.append(detail);
}
return sb.toString();
}
protected <E extends TweedExtension> E instantiateExtension(
Class<E> extensionClass,
TweedExtensionSetupContext setupContext
) {
if (isAbstractClass(extensionClass)) {
throw new IllegalStateException(
"Cannot instantiate extension class " + extensionClass.getName() + " as it is abstract"
);
}
return TweedExtension.FACTORY.construct(extensionClass)
.typedArg(TweedExtensionSetupContext.class, setupContext)
.typedArg(ConfigContainer.class, this)
.finish();
}
private boolean isAbstractClass(Class<?> clazz) {
return clazz.isInterface() || (clazz.getModifiers() & Modifier.ABSTRACT) != 0;
}
@Override
public <E extends TweedExtension> Optional<E> extension(Class<E> extensionClass) {
requireSetupPhase(
ConfigContainerSetupPhase.TREE_SETUP,
ConfigContainerSetupPhase.TREE_ATTACHED,
ConfigContainerSetupPhase.INITIALIZED
);
try {
return Optional.ofNullable(extensions.getSingleInstance(extensionClass));
} catch (InheritanceMap.NonUniqueResultException e) {
throw new IllegalStateException("Multiple extensions registered for class " + extensionClass.getName(), e);
}
}
@Override
public Collection<TweedExtension> extensions() {
requireSetupPhase(
ConfigContainerSetupPhase.TREE_SETUP,
ConfigContainerSetupPhase.TREE_ATTACHED,
ConfigContainerSetupPhase.INITIALIZED
);
return Collections.unmodifiableCollection(extensions.values());
}
@Override
public void attachTree(ConfigEntry<T> rootEntry) {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
this.rootEntry = rootEntry;
setupPhase = ConfigContainerSetupPhase.TREE_ATTACHED;
}
@Override
public Patchwork createExtensionsData() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
assert entryExtensionsDataPatchworkFactory != null;
return entryExtensionsDataPatchworkFactory.create();
}
@Override
public void initialize() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_ATTACHED);
for (TweedExtension extension : extensions()) {
extension.initialize();
}
assert rootEntry != null;
rootEntry.visitInOrder(entry -> {
for (TweedExtension extension : extensions()) {
extension.initEntry(entry);
}
});
setupPhase = ConfigContainerSetupPhase.INITIALIZED;
}
@Override
public ConfigEntry<T> rootEntry() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_ATTACHED, ConfigContainerSetupPhase.INITIALIZED);
assert rootEntry != null;
return rootEntry;
}
private void requireSetupPhase(ConfigContainerSetupPhase... allowedPhases) {
for (ConfigContainerSetupPhase allowedPhase : allowedPhases) {
if (allowedPhase == setupPhase) {
return;
}
}
if (allowedPhases.length == 1) {
throw new IllegalStateException(
"Config container is not in correct phase, expected "
+ allowedPhases[0]
+ ", but is in "
+ setupPhase
);
} else {
throw new IllegalStateException(
"Config container is not in correct phase, expected any of "
+ Arrays.toString(allowedPhases)
+ ", but is in "
+ setupPhase
);
}
}
}

View File

@@ -0,0 +1,42 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.*;
import java.util.Collection;
import java.util.function.IntFunction;
public class CollectionConfigEntryImpl<E, T extends Collection<E>> extends BaseConfigEntry<T> implements CollectionConfigEntry<E, T> {
private final IntFunction<T> collectionConstructor;
private final ConfigEntry<E> elementEntry;
public CollectionConfigEntryImpl(
ConfigContainer<?> container,
Class<T> valueClass,
IntFunction<T> collectionConstructor,
ConfigEntry<E> elementEntry
) {
super(container, valueClass);
this.collectionConstructor = collectionConstructor;
this.elementEntry = elementEntry;
}
@Override
public ConfigEntry<E> elementEntry() {
return elementEntry;
}
@Override
public T instantiateCollection(int size) {
return collectionConstructor.apply(size);
}
@Override
public T deepCopy(T value) {
T copy = collectionConstructor.apply(value.size());
for (E element : value) {
copy.add(elementEntry().deepCopy(element));
}
return copy;
}
}

View File

@@ -0,0 +1,29 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.BaseConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import org.jspecify.annotations.NonNull;
public class SimpleConfigEntryImpl<T> extends BaseConfigEntry<T> implements SimpleConfigEntry<T> {
public SimpleConfigEntryImpl(ConfigContainer<?> container, Class<T> valueClass) {
super(container, valueClass);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
visitor.visitEntry(this);
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
visitor.visitEntry(this, value);
}
@Override
public T deepCopy(@NonNull T value) {
return value;
}
}

View File

@@ -0,0 +1,82 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.IntFunction;
public class StaticMapCompoundConfigEntryImpl<T extends Map<String, Object>> extends BaseConfigEntry<T> implements CompoundConfigEntry<T> {
private final IntFunction<T> mapConstructor;
private final Map<String, ConfigEntry<?>> compoundEntries;
public StaticMapCompoundConfigEntryImpl(
ConfigContainer<?> container,
Class<T> valueClass,
IntFunction<T> mapConstructor,
Map<String, ConfigEntry<?>> compoundEntries
) {
super(container, valueClass);
this.mapConstructor = mapConstructor;
this.compoundEntries = new LinkedHashMap<>(compoundEntries);
}
@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(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterStructuredEntry(this, value)) {
if (value != null) {
compoundEntries.forEach((key, entry) -> {
if (visitor.enterStructuredSubEntry(key, key)) {
//noinspection unchecked
((ConfigEntry<Object>) entry).visitInOrder(visitor, value.get(key));
visitor.leaveStructuredSubEntry(key, key);
}
});
}
visitor.leaveStructuredEntry(this, value);
}
}
@Override
public T deepCopy(T value) {
T copy = instantiateCompoundValue();
value.forEach((String key, Object element) -> {
//noinspection unchecked
ConfigEntry<Object> elementEntry = (ConfigEntry<Object>) compoundEntries.get(key);
if (elementEntry != null) {
copy.put(key, elementEntry.deepCopy(element));
}
});
return copy;
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.core.impl;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,226 @@
package de.siphalor.tweed5.core.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import lombok.Getter;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class DefaultConfigContainerTest {
@Test
void extensionSetup() {
var configContainer = new DefaultConfigContainer<>();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
configContainer.registerExtension(ExtensionA.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SETUP);
assertThat(configContainer.extensions())
.satisfiesExactlyInAnyOrder(
extension -> assertThat(extension).isInstanceOf(ExtensionA.class),
extension -> assertThat(extension).isInstanceOf(ExtensionBImpl.class)
);
}
@Test
void extensionSetupDefaultOverride() {
var configContainer = new DefaultConfigContainer<>();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
configContainer.registerExtensions(ExtensionA.class, ExtensionBNonDefaultImpl.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SETUP);
assertThat(configContainer.extensions())
.satisfiesExactlyInAnyOrder(
extension -> assertThat(extension).isInstanceOf(ExtensionA.class),
extension -> assertThat(extension).isInstanceOf(ExtensionBNonDefaultImpl.class)
);
}
@Test
void extensionSetupMissingDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionMissingDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup).hasMessageContaining("ExtensionMissingDefault");
}
@Test
void extensionSetupAbstractDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionSelfReferencingDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup).hasMessageContaining("ExtensionSelfReferencingDefault");
}
@Test
void extensionSetupWrongDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionWrongDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup)
.hasMessageContaining("ExtensionWrongDefault", "ExtensionBImpl");
}
@Test
void extensionSetupNonStaticDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionNonStaticDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup)
.hasMessageContaining("ExtensionNonStaticDefault", "static");
}
@Test
void extensionSetupNonPublicDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionNonPublicDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup)
.hasMessageContaining("ExtensionNonPublicDefault", "public");
}
@Test
void extension() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtensions(ExtensionA.class, ExtensionB.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.extension(ExtensionA.class)).containsInstanceOf(ExtensionA.class);
assertThat(configContainer.extension(ExtensionB.class)).containsInstanceOf(ExtensionBImpl.class);
assertThat(configContainer.extension(ExtensionBImpl.class)).containsInstanceOf(ExtensionBImpl.class);
assertThat(configContainer.extension(ExtensionBNonDefaultImpl.class)).isEmpty();
}
@SuppressWarnings("unchecked")
@Test
void attachTree() {
var configContainer = new DefaultConfigContainer<Map<String, Object>>();
configContainer.registerExtension(ExtensionInitTracker.class);
configContainer.finishExtensionSetup();
var subEntry = new SimpleConfigEntryImpl<>(configContainer, String.class);
var compoundEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>)(Class<?>) Map.class,
(capacity) -> new HashMap<>(capacity * 2, 0.5F),
Map.of("test", subEntry)
);
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SETUP);
configContainer.attachTree(compoundEntry);
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_ATTACHED);
assertThat(configContainer.rootEntry()).isSameAs(compoundEntry);
}
@Test
void createExtensionsData() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionB.class);
configContainer.finishExtensionSetup();
var extensionData = configContainer.createExtensionsData();
assertThat(extensionData).isNotNull();
}
@SuppressWarnings("unchecked")
@Test
void initialize() {
var configContainer = new DefaultConfigContainer<Map<String, Object>>();
configContainer.registerExtension(ExtensionInitTracker.class);
configContainer.finishExtensionSetup();
var subEntry = new SimpleConfigEntryImpl<>(configContainer, String.class);
var compoundEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>)(Class<?>) Map.class,
(capacity) -> new HashMap<>(capacity * 2, 0.5F),
Map.of("test", subEntry)
);
configContainer.attachTree(compoundEntry);
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_ATTACHED);
configContainer.initialize();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.INITIALIZED);
var initTracker = configContainer.extension(ExtensionInitTracker.class).orElseThrow();
assertThat(initTracker.initializedEntries()).containsExactlyInAnyOrder(compoundEntry, subEntry);
}
public static class ExtensionA implements TweedExtension {
public ExtensionA(TweedExtensionSetupContext context) {
context.registerExtension(ExtensionB.class);
}
@Override
public String getId() {
return "a";
}
}
public interface ExtensionB extends TweedExtension {
Class<? extends ExtensionB> DEFAULT = ExtensionBImpl.class;
}
public static class ExtensionBImpl implements ExtensionB {
public ExtensionBImpl(TweedExtensionSetupContext context) {
context.registerEntryExtensionData(ExtensionBData.class);
}
@Override
public String getId() {
return "b";
}
}
public static class ExtensionBNonDefaultImpl implements ExtensionB {
@Override
public String getId() {
return "b-non-default";
}
}
public interface ExtensionBData {
String test();
}
record ExtensionBDataImpl(String test) implements ExtensionBData { }
@Getter
public static class ExtensionInitTracker implements TweedExtension {
private final Collection<ConfigEntry<?>> initializedEntries = new ArrayList<>();
@Override
public String getId() {
return "init-tracker";
}
@Override
public void initEntry(ConfigEntry<?> configEntry) {
initializedEntries.add(configEntry);
}
}
public interface ExtensionMissingDefault extends TweedExtension {}
public interface ExtensionSelfReferencingDefault extends TweedExtension {
Class<ExtensionSelfReferencingDefault> DEFAULT = ExtensionSelfReferencingDefault.class;
}
public interface ExtensionWrongDefault extends TweedExtension {
Class<?> DEFAULT = ExtensionBImpl.class;
}
public static abstract class ExtensionNonStaticDefault implements TweedExtension {
public final Class<?> DEFAULT = ExtensionNonStaticDefault.class;
}
public static abstract class ExtensionNonPublicDefault implements TweedExtension {
protected static final Class<?> DEFAULT = ExtensionNonPublicDefault.class;
}
}

View File

@@ -0,0 +1,83 @@
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 trivialSort() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(2);
sorter.addEdge(0, 1);
assertArrayEquals(new int[]{ 0, 1 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sort1() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(4);
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());
}
@Test
void minimalCycle() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(2);
sorter.addEdge(0, 1);
sorter.addEdge(1, 0);
AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort);
assertEquals(Arrays.asList(0, 1), exception.cycleIndeces());
}
}

View File

@@ -0,0 +1,10 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
api(project(":tweed5-core"))
api(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,2 @@
module.name = Tweed 5 Default Extensions
module.description = A collection of commonly used Tweed 5 extensions bundled for convenience.

View File

@@ -0,0 +1,27 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.defaultextensions.comment.impl.CommentExtensionImpl;
import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
public interface CommentExtension extends TweedExtension {
Class<? extends CommentExtension> DEFAULT = CommentExtensionImpl.class;
String EXTENSION_ID = "comment";
static <C extends ConfigEntry<?>> Consumer<C> baseComment(String baseComment) {
return entry -> {
CommentExtension extension = entry.container().extension(CommentExtension.class)
.orElseThrow(() -> new IllegalStateException("No comment extension registered"));
extension.setBaseComment(entry, baseComment);
};
}
void setBaseComment(ConfigEntry<?> configEntry, String baseComment);
void recomputeFullComments();
@Nullable String getFullComment(ConfigEntry<?> configEntry);
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface CommentModifyingExtension {
Middleware<CommentProducer> commentMiddleware();
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
@FunctionalInterface
public interface CommentProducer {
String createComment(ConfigEntry<?> entry);
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.defaultextensions.comment.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,105 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import com.google.auto.service.AutoService;
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.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Data;
import lombok.Getter;
import org.jspecify.annotations.Nullable;
@AutoService(CommentExtension.class)
public class CommentExtensionImpl implements ReadWriteRelatedExtension, CommentExtension {
private final ConfigContainer<?> configContainer;
@Getter
private final PatchworkPartAccess<CustomEntryData> customEntryDataAccess;
private final DefaultMiddlewareContainer<CommentProducer> middlewareContainer;
@Getter
private @Nullable PatchworkPartAccess<Boolean> writerInstalledReadWriteContextAccess;
public CommentExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) {
this.configContainer = configContainer;
this.customEntryDataAccess = context.registerEntryExtensionData(CustomEntryData.class);
this.middlewareContainer = new DefaultMiddlewareContainer<>();
}
@Override
public String getId() {
return EXTENSION_ID;
}
@Override
public void extensionsFinalized() {
for (TweedExtension extension : configContainer.extensions()) {
if (extension instanceof CommentModifyingExtension) {
middlewareContainer.register(((CommentModifyingExtension) extension).commentMiddleware());
}
}
middlewareContainer.seal();
}
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
writerInstalledReadWriteContextAccess = context.registerReadWriteContextExtensionData(Boolean.class);
context.registerWriterMiddleware(new TweedEntryWriterCommentMiddleware(this));
}
@Override
public void setBaseComment(ConfigEntry<?> configEntry, String baseComment) {
if (configEntry.container() != configContainer) {
throw new IllegalArgumentException("config entry doesn't belong to config container of this extension");
} else if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) >= 0) {
throw new IllegalStateException("config container must not be initialized");
}
getOrCreateCustomEntryData(configEntry).baseComment(baseComment);
}
@Override
public void initialize() {
recomputeFullComments();
}
@Override
public void recomputeFullComments() {
configContainer.rootEntry().visitInOrder(entry -> {
CustomEntryData entryData = getOrCreateCustomEntryData(entry);
entryData.commentProducer(middlewareContainer.process(_entry -> entryData.baseComment()));
});
}
private CustomEntryData getOrCreateCustomEntryData(ConfigEntry<?> entry) {
CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
customEntryData = new CustomEntryData();
entry.extensionsData().set(customEntryDataAccess, customEntryData);
}
return customEntryData;
}
@Override
public @Nullable String getFullComment(ConfigEntry<?> configEntry) {
CustomEntryData customEntryData = configEntry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
return null;
}
String comment = customEntryData.commentProducer().createComment(configEntry);
return comment.isEmpty() ? null : comment;
}
@Data
private static class CustomEntryData {
private String baseComment = "";
private CommentProducer commentProducer;
}
}

View File

@@ -0,0 +1,100 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.dataapi.api.DelegatingTweedDataWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.Deque;
@RequiredArgsConstructor
class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?, ?>> {
private final CommentExtensionImpl commentExtension;
@Override
public String id() {
return "comment-writer";
}
@Override
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
PatchworkPartAccess<Boolean> writerInstalledAccess = commentExtension.writerInstalledReadWriteContextAccess();
assert writerInstalledAccess != null;
//noinspection unchecked
TweedEntryWriter<Object, ConfigEntry<Object>> innerCasted = (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) (writer, value, entry, context) -> {
if (!Boolean.TRUE.equals(context.extensionsData().get(writerInstalledAccess))) {
context.extensionsData().set(writerInstalledAccess, Boolean.TRUE);
writer = new MapEntryKeyDeferringWriter(writer);
}
String comment = commentExtension.getFullComment(entry);
if (comment != null) {
writer.visitDecoration(new PiercingCommentDecoration(() -> comment));
}
innerCasted.write(writer, value, entry, context);
};
}
private static class MapEntryKeyDeferringWriter extends DelegatingTweedDataWriter {
private final Deque<TweedDataDecoration> decorationQueue = new ArrayDeque<>();
private @Nullable String mapEntryKey;
protected MapEntryKeyDeferringWriter(TweedDataVisitor delegate) {
super(delegate);
}
@Override
public void visitMapEntryKey(String key) {
if (mapEntryKey != null) {
throw new IllegalStateException("Map entry key already visited");
} else {
mapEntryKey = key;
}
}
@Override
public void visitDecoration(TweedDataDecoration decoration) {
if (decoration instanceof PiercingCommentDecoration) {
super.visitDecoration(((PiercingCommentDecoration) decoration).commentDecoration());
return;
}
if (mapEntryKey != null) {
decorationQueue.addLast(decoration);
} else {
super.visitDecoration(decoration);
}
}
@Override
protected void beforeValueWrite() {
if (mapEntryKey != null) {
super.visitMapEntryKey(mapEntryKey);
mapEntryKey = null;
TweedDataDecoration decoration;
while ((decoration = decorationQueue.pollFirst()) != null) {
super.visitDecoration(decoration);
}
}
super.beforeValueWrite();
}
}
@Value
private static class PiercingCommentDecoration implements TweedDataDecoration {
TweedDataCommentDecoration commentDecoration;
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.defaultextensions.comment.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,23 @@
package de.siphalor.tweed5.defaultextensions.patch.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.defaultextensions.patch.impl.PatchExtensionImpl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import org.jspecify.annotations.Nullable;
import java.util.function.Function;
public interface PatchExtension extends TweedExtension {
Class<? extends PatchExtension> DEFAULT = PatchExtensionImpl.class;
String EXTENSION_ID = "patch";
@Override
default String getId() {
return EXTENSION_ID;
}
PatchInfo collectPatchInfo(Patchwork readWriteContextExtensionsData);
<T extends @Nullable Object> T patch(ConfigEntry<T> entry, T targetValue, T patchValue, PatchInfo patchInfo);
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.patch.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
public interface PatchInfo {
boolean containsEntry(ConfigEntry<?> entry);
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.defaultextensions.patch.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,121 @@
package de.siphalor.tweed5.defaultextensions.patch.impl;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
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.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchInfo;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Data;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
public class PatchExtensionImpl implements PatchExtension, ReadWriteRelatedExtension {
private @Nullable PatchworkPartAccess<ReadWriteContextCustomData> readWriteContextDataAccess;
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
readWriteContextDataAccess = context.registerReadWriteContextExtensionData(ReadWriteContextCustomData.class);
context.registerReaderMiddleware(new ReaderMiddleware());
}
@Override
public PatchInfo collectPatchInfo(Patchwork readWriteContextExtensionsData) {
ReadWriteContextCustomData customData = getOrCreateCustomData(readWriteContextExtensionsData);
PatchInfoImpl patchInfo = customData.patchInfo();
if (patchInfo == null) {
patchInfo = new PatchInfoImpl();
customData.patchInfo(patchInfo);
}
return patchInfo;
}
private ReadWriteContextCustomData getOrCreateCustomData(Patchwork readWriteContextExtensionsData) {
assert readWriteContextDataAccess != null;
ReadWriteContextCustomData customData = readWriteContextExtensionsData.get(readWriteContextDataAccess);
if (customData == null) {
customData = new ReadWriteContextCustomData();
readWriteContextExtensionsData.set(readWriteContextDataAccess, customData);
}
return customData;
}
@Override
public <T extends @Nullable Object> T patch(ConfigEntry<T> entry, T targetValue, T patchValue, PatchInfo patchInfo) {
if (!patchInfo.containsEntry(entry)) {
return targetValue;
} else if (patchValue == null) {
return null;
}
if (entry instanceof CompoundConfigEntry) {
CompoundConfigEntry<T> compoundEntry = (CompoundConfigEntry<T>) entry;
T targetCompoundValue;
if (targetValue != null) {
targetCompoundValue = targetValue;
} else {
targetCompoundValue = compoundEntry.instantiateCompoundValue();
}
compoundEntry.subEntries().forEach((key, subEntry) -> {
if (!patchInfo.containsEntry(subEntry)) {
return;
}
compoundEntry.set(
targetCompoundValue, key, patch(
subEntry,
compoundEntry.get(targetCompoundValue, key),
compoundEntry.get(patchValue, key),
patchInfo
)
);
});
return targetCompoundValue;
} else {
return patchValue;
}
}
private class ReaderMiddleware implements Middleware<TweedEntryReader<?, ?>> {
@Override
public String id() {
return "patch-info-collector";
}
@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 {
Object readValue = innerCasted.read(reader, entry, context);
ReadWriteContextCustomData customData = context.extensionsData().get(readWriteContextDataAccess);
if (customData != null && customData.patchInfo() != null) {
customData.patchInfo().addEntry(entry);
}
return readValue;
}
};
}
}
@Data
private static class ReadWriteContextCustomData {
private @Nullable PatchInfoImpl patchInfo;
}
}

View File

@@ -0,0 +1,21 @@
package de.siphalor.tweed5.defaultextensions.patch.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchInfo;
import org.jspecify.annotations.Nullable;
import java.util.IdentityHashMap;
import java.util.Map;
public class PatchInfoImpl implements PatchInfo {
private final Map<ConfigEntry<?>, @Nullable Void> subPatchInfos = new IdentityHashMap<>();
@Override
public boolean containsEntry(ConfigEntry<?> entry) {
return subPatchInfos.containsKey(entry);
}
void addEntry(ConfigEntry<?> entry) {
subPatchInfos.put(entry, null);
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.defaultextensions.patch.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,15 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.defaultextensions.pather.impl.PathTrackingImpl;
public interface PathTracking {
static PathTracking create() {
return new PathTrackingImpl();
}
void pushPathPart(String pathPart);
void popPathPart();
String currentPath();
}

View File

@@ -0,0 +1,42 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
@RequiredArgsConstructor
public class PathTrackingConfigEntryValueVisitor implements ConfigEntryValueVisitor {
private final ConfigEntryValueVisitor delegate;
private final ValuePathTracking pathTracking;
@Override
public <T extends @Nullable Object> void visitEntry(ConfigEntry<T> entry, T value) {
delegate.visitEntry(entry, value);
}
@Override
public <T> boolean enterStructuredEntry(ConfigEntry<T> entry, T value) {
return delegate.enterStructuredEntry(entry, value);
}
@Override
public boolean enterStructuredSubEntry(String entryKey, String valueKey) {
boolean enter = delegate.enterStructuredSubEntry(entryKey, valueKey);
if (enter) {
pathTracking.pushPathPart(entryKey, valueKey);
}
return enter;
}
@Override
public void leaveStructuredSubEntry(String entryKey, String valueKey) {
delegate.leaveStructuredSubEntry(entryKey, valueKey);
pathTracking.popPathPart();
}
@Override
public <T> void leaveStructuredEntry(ConfigEntry<T> entry, T value) {
delegate.leaveStructuredEntry(entry, value);
}
}

View File

@@ -0,0 +1,41 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class PathTrackingConfigEntryVisitor implements ConfigEntryVisitor {
private final ConfigEntryVisitor delegate;
private final PathTracking pathTracking;
@Override
public void visitEntry(ConfigEntry<?> entry) {
delegate.visitEntry(entry);
}
@Override
public boolean enterStructuredEntry(ConfigEntry<?> entry) {
return delegate.enterStructuredEntry(entry);
}
@Override
public boolean enterStructuredSubEntry(String key) {
boolean enter = delegate.enterStructuredSubEntry(key);
if (enter) {
pathTracking.pushPathPart(key);
}
return enter;
}
@Override
public void leaveStructuredSubEntry(String key) {
delegate.leaveStructuredSubEntry(key);
pathTracking.popPathPart();
}
@Override
public void leaveStructuredEntry(ConfigEntry<?> entry) {
delegate.leaveStructuredEntry(entry);
}
}

View File

@@ -0,0 +1,66 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataToken;
import lombok.RequiredArgsConstructor;
import java.util.ArrayDeque;
@RequiredArgsConstructor
public class PathTrackingDataReader implements TweedDataReader {
private final TweedDataReader delegate;
private final PathTracking pathTracking;
private final ArrayDeque<Context> contextStack = new ArrayDeque<>(50);
private final ArrayDeque<Integer> listIndexStack = new ArrayDeque<>(50);
@Override
public TweedDataToken peekToken() throws TweedDataReadException {
return delegate.peekToken();
}
@Override
public TweedDataToken readToken() throws TweedDataReadException {
TweedDataToken token = delegate.readToken();
if (token.isListValue()) {
if (contextStack.peek() == Context.LIST) {
int index = listIndexStack.pop() + 1;
if (index != 0) {
pathTracking.popPathPart();
}
pathTracking.pushPathPart(Integer.toString(index));
listIndexStack.push(index);
}
}
if (token.isListStart()) {
contextStack.push(Context.LIST);
listIndexStack.push(-1);
} else if (token.isListEnd()) {
contextStack.pop();
int lastIndex = listIndexStack.pop();
if (lastIndex >= 0) {
pathTracking.popPathPart();
}
} else if (token.isMapStart()) {
contextStack.push(Context.MAP);
pathTracking.pushPathPart("$");
} else if (token.isMapEntryKey()) {
pathTracking.popPathPart();
pathTracking.pushPathPart(token.readAsString());
} else if (token.isMapEnd()) {
pathTracking.popPathPart();
contextStack.pop();
}
return token;
}
@Override
public void close() throws Exception {
delegate.close();
}
private enum Context {
LIST, MAP,
}
}

View File

@@ -0,0 +1,134 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.dataapi.api.TweedDataUnsupportedValueException;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque;
@RequiredArgsConstructor
public class PathTrackingDataVisitor implements TweedDataVisitor {
private final TweedDataVisitor delegate;
private final PathTracking pathTracking;
private final ArrayDeque<Context> contextStack = new ArrayDeque<>(50);
private final ArrayDeque<Integer> listIndexStack = new ArrayDeque<>(50);
@Override
public void visitNull() {
delegate.visitNull();
valueVisited();
}
@Override
public void visitBoolean(boolean value) {
delegate.visitBoolean(value);
valueVisited();
}
@Override
public void visitByte(byte value) {
delegate.visitByte(value);
valueVisited();
}
@Override
public void visitShort(short value) {
delegate.visitShort(value);
valueVisited();
}
@Override
public void visitInt(int value) {
delegate.visitInt(value);
valueVisited();
}
@Override
public void visitLong(long value) {
delegate.visitLong(value);
valueVisited();
}
@Override
public void visitFloat(float value) {
delegate.visitFloat(value);
valueVisited();
}
@Override
public void visitDouble(double value) {
delegate.visitDouble(value);
valueVisited();
}
@Override
public void visitString(String value) {
delegate.visitString(value);
valueVisited();
}
@Override
public void visitValue(@Nullable Object value) throws TweedDataUnsupportedValueException {
TweedDataVisitor.super.visitValue(value);
valueVisited();
}
private void valueVisited() {
Context context = contextStack.peek();
if (context == Context.MAP_ENTRY) {
contextStack.pop();
pathTracking.popPathPart();
} else if (context == Context.LIST) {
pathTracking.popPathPart();
int index = listIndexStack.pop();
listIndexStack.push(index + 1);
pathTracking.pushPathPart(Integer.toString(index));
}
}
@Override
public void visitListStart() {
delegate.visitListStart();
contextStack.push(Context.LIST);
listIndexStack.push(0);
pathTracking.pushPathPart("0");
}
@Override
public void visitListEnd() {
delegate.visitListEnd();
contextStack.pop();
listIndexStack.pop();
pathTracking.popPathPart();
valueVisited();
}
@Override
public void visitMapStart() {
delegate.visitMapStart();
}
@Override
public void visitMapEntryKey(String key) {
delegate.visitMapEntryKey(key);
pathTracking.pushPathPart(key);
contextStack.push(Context.MAP_ENTRY);
}
@Override
public void visitMapEnd() {
delegate.visitMapEnd();
valueVisited();
}
@Override
public void visitDecoration(TweedDataDecoration decoration) {
delegate.visitDecoration(decoration);
}
private enum Context {
LIST, MAP_ENTRY,
}
}

View File

@@ -0,0 +1,19 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.TweedWriteContext;
import de.siphalor.tweed5.defaultextensions.pather.impl.PatherExtensionImpl;
public interface PatherExtension extends TweedExtension {
Class<? extends PatherExtension> DEFAULT = PatherExtensionImpl.class;
String EXTENSION_ID = "pather";
@Override
default String getId() {
return EXTENSION_ID;
}
String getPath(TweedReadContext context);
String getPath(TweedWriteContext context);
}

View File

@@ -0,0 +1,34 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import lombok.NoArgsConstructor;
@NoArgsConstructor(staticName = "create")
public final class ValuePathTracking implements PathTracking {
private final PathTracking entryPathTracking = PathTracking.create();
private final PathTracking valuePathTracking = PathTracking.create();
@Override
public void pushPathPart(String pathPart) {
this.pushPathPart(pathPart, pathPart);
}
public void pushPathPart(String entryPathPart, String valuePathPart) {
entryPathTracking.pushPathPart(entryPathPart);
valuePathTracking.pushPathPart(valuePathPart);
}
@Override
public void popPathPart() {
valuePathTracking.popPathPart();
entryPathTracking.popPathPart();
}
@Override
public String currentPath() {
return valuePathTracking.currentPath();
}
public String currentEntryPath() {
return entryPathTracking.currentPath();
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.defaultextensions.pather.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,30 @@
package de.siphalor.tweed5.defaultextensions.pather.impl;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import java.util.ArrayDeque;
import java.util.Deque;
public class PathTrackingImpl implements PathTracking {
private final StringBuilder pathBuilder = new StringBuilder(256);
private final Deque<String> pathParts = new ArrayDeque<>(50);
@Override
public void pushPathPart(String entryPathPart) {
pathParts.push(entryPathPart);
pathBuilder.append(".").append(entryPathPart);
}
@Override
public void popPathPart() {
if (!pathParts.isEmpty()) {
String poppedPart = pathParts.pop();
pathBuilder.setLength(pathBuilder.length() - poppedPart.length() - 1);
}
}
@Override
public String currentPath() {
return pathBuilder.toString();
}
}

View File

@@ -0,0 +1,137 @@
package de.siphalor.tweed5.defaultextensions.pather.impl;
import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingDataReader;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingDataVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.val;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
@AutoService(PatherExtension.class)
public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExtension {
private @Nullable PatchworkPartAccess<PathTracking> rwContextPathTrackingAccess;
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
rwContextPathTrackingAccess = context.registerReadWriteContextExtensionData(PathTracking.class);
context.registerReaderMiddleware(createEntryReaderMiddleware());
context.registerWriterMiddleware(createEntryWriterMiddleware());
}
@Override
public String getPath(TweedReadContext context) {
assert rwContextPathTrackingAccess != null;
PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking == null) {
throw new IllegalStateException("Path tracking is not active!");
}
return pathTracking.currentPath();
}
@Override
public String getPath(TweedWriteContext context) {
assert rwContextPathTrackingAccess != null;
PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking == null) {
throw new IllegalStateException("Path tracking is not active!");
}
return pathTracking.currentPath();
}
private Middleware<TweedEntryReader<?, ?>> createEntryReaderMiddleware() {
return new Middleware<TweedEntryReader<?, ?>>() {
@Override
public String id() {
return EXTENSION_ID;
}
@Override
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
assert rwContextPathTrackingAccess != null;
//noinspection unchecked
val castedInner = (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> {
PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking != null) {
return castedInner.read(reader, entry, context);
}
pathTracking = PathTracking.create();
context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
try {
return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context);
} catch (TweedEntryReadException e) {
val exceptionPathTracking = e.context().extensionsData().get(rwContextPathTrackingAccess);
if (exceptionPathTracking != null) {
throw new TweedEntryReadException(
"Exception while reading entry at "
+ exceptionPathTracking.currentPath()
+ ": " + e.getMessage(),
e
);
} else {
throw e;
}
}
};
}
};
}
private Middleware<TweedEntryWriter<?, ?>> createEntryWriterMiddleware() {
return new Middleware<TweedEntryWriter<?, ?>>() {
@Override
public String id() {
return EXTENSION_ID;
}
@Override
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
assert rwContextPathTrackingAccess != null;
//noinspection unchecked
val castedInner = (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedDataVisitor writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) -> {
PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking != null) {
castedInner.write(writer, value, entry, context);
return;
}
pathTracking = PathTracking.create();
context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
try {
castedInner.write(new PathTrackingDataVisitor(writer, pathTracking), value, entry, context);
} catch (TweedEntryWriteException e) {
val exceptionPathTracking = e.context().extensionsData().get(rwContextPathTrackingAccess);
if (exceptionPathTracking != null) {
throw new TweedEntryWriteException(
"Exception while writing entry at "
+ exceptionPathTracking.currentPath()
+ ": " + e.getMessage(),
e
);
} else {
throw e;
}
}
};
}
};
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.defaultextensions.pather.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import org.jspecify.annotations.Nullable;
public interface ConfigEntryValidator {
<T extends @Nullable Object> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value);
<T> String description(ConfigEntry<T> configEntry);
}

View File

@@ -0,0 +1,62 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.SimpleValidatorMiddleware;
import de.siphalor.tweed5.defaultextensions.validation.impl.ValidationExtensionImpl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
public interface ValidationExtension extends TweedExtension {
Class<? extends ValidationExtension> DEFAULT = ValidationExtensionImpl.class;
String EXTENSION_ID = "validation";
@Override
default String getId() {
return EXTENSION_ID;
}
static <C extends ConfigEntry<T>, T> Consumer<C> validators(ConfigEntryValidator... validators) {
return entry -> {
ValidationExtension extension = entry.container().extension(ValidationExtension.class)
.orElseThrow(() -> new IllegalStateException("No validation extension registered"));
extension.addValidators(entry, validators);
};
}
static <C extends ConfigEntry<T>, T> Function<C, ValidationIssues> validate(T value) {
return entry -> {
ValidationExtension extension = entry.container().extension(ValidationExtension.class)
.orElseThrow(() -> new IllegalStateException("No validation extension registered"));
return extension.validate(entry, value);
};
}
default <T> void addValidators(ConfigEntry<T> entry, ConfigEntryValidator... validators) {
String lastId = null;
for (ConfigEntryValidator validator : validators) {
String id = UUID.randomUUID().toString();
Set<String> mustComeAfter = lastId == null ? Collections.emptySet() : Collections.singleton(lastId);
addValidatorMiddleware(entry, new SimpleValidatorMiddleware(id, validator) {
@Override
public Set<String> mustComeAfter() {
return mustComeAfter;
}
});
lastId = id;
}
}
<T> void addValidatorMiddleware(ConfigEntry<T> entry, Middleware<ConfigEntryValidator> validator);
ValidationIssues captureValidationIssues(Patchwork readContextExtensionsData);
<T extends @Nullable Object> ValidationIssues validate(ConfigEntry<T> entry, T value);
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface ValidationProvidingExtension {
Middleware<ConfigEntryValidator> validationMiddleware();
}

Some files were not shown because too many files have changed in this diff Show More