[build] Restructure to composite build
This commit is contained in:
9
tweed5/annotation-inheritance/build.gradle.kts
Normal file
9
tweed5/annotation-inheritance/build.gradle.kts
Normal 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)
|
||||
}
|
||||
3
tweed5/annotation-inheritance/gradle.properties
Normal file
3
tweed5/annotation-inheritance/gradle.properties
Normal 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.
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.annotationinheritance.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Package for on-the-fly-generated classes.
|
||||
*/
|
||||
package de.siphalor.tweed5.annotationinheritance.impl.generated;
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.annotationinheritance.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
10
tweed5/attributes-extension/build.gradle.kts
Normal file
10
tweed5/attributes-extension/build.gradle.kts
Normal file
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id("de.siphalor.tweed5.base-module")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":tweed5-core"))
|
||||
compileOnly(project(":tweed5-serde-extension"))
|
||||
testImplementation(project(":tweed5-serde-extension"))
|
||||
testImplementation(project(":tweed5-serde-hjson"))
|
||||
}
|
||||
2
tweed5/attributes-extension/gradle.properties
Normal file
2
tweed5/attributes-extension/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
module.name = Tweed 5 Attributes Extension
|
||||
module.description = A Tweed extension that allows defining generic attributes on config entries.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.siphalor.tweed5.attributesextension.api;
|
||||
|
||||
public interface AttributesRelatedExtension {
|
||||
default void afterAttributesInitialized() {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.attributesextension.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.attributesextension.api.serde.filter;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.attributesextension.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.attributesextension.impl.serde.filter;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,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
24
tweed5/build.gradle.kts
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tweed5/comment-loader-extension/build.gradle.kts
Normal file
12
tweed5/comment-loader-extension/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
2
tweed5/comment-loader-extension/gradle.properties
Normal file
2
tweed5/comment-loader-extension/gradle.properties
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.commentloaderextension.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.commentloaderextension.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
3
tweed5/construct/build.gradle.kts
Normal file
3
tweed5/construct/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id("de.siphalor.tweed5.base-module")
|
||||
}
|
||||
3
tweed5/construct/gradle.properties
Normal file
3
tweed5/construct/gradle.properties
Normal 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.
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.construct.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.siphalor.tweed5.construct.impl;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
class Entry<K, V> {
|
||||
K key;
|
||||
V value;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.construct.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
9
tweed5/core/build.gradle.kts
Normal file
9
tweed5/core/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
2
tweed5/core/gradle.properties
Normal file
2
tweed5/core/gradle.properties
Normal 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.
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.siphalor.tweed5.core.api.container;
|
||||
|
||||
public enum ConfigContainerSetupPhase {
|
||||
EXTENSIONS_SETUP,
|
||||
TREE_SETUP,
|
||||
TREE_ATTACHED,
|
||||
INITIALIZED,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.core.api.container;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.core.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package de.siphalor.tweed5.core.generated;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.core.impl;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
10
tweed5/default-extensions/build.gradle.kts
Normal file
10
tweed5/default-extensions/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
2
tweed5/default-extensions/gradle.properties
Normal file
2
tweed5/default-extensions/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
module.name = Tweed 5 Default Extensions
|
||||
module.description = A collection of commonly used Tweed 5 extensions bundled for convenience.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.defaultextensions.comment.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.defaultextensions.comment.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.defaultextensions.patch.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.defaultextensions.patch.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.defaultextensions.pather.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.defaultextensions.pather.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user