[weaver-pojo] Introduce pojo weaver post processors

This commit is contained in:
2024-12-09 23:35:26 +01:00
parent aaf05d1a33
commit f10a23a0f5
27 changed files with 1078 additions and 97 deletions

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@code <spec> = <id> [ "(" <spec> ( "," <spec> )* ")" ] }
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EntryReadWriteConfig {
String value() default "";
String writer() default "";
String reader() default "";
}

View File

@@ -0,0 +1,182 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.impl.SerdePojoReaderWriterSpec;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
@Slf4j
public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor {
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>>> readerFactories = new HashMap<>();
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>>> writerFactories = new HashMap<>();
public ReadWritePojoPostProcessor() {
loadProviders();
}
private void loadProviders() {
ServiceLoader<TweedReaderWriterProvider> serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class);
for (TweedReaderWriterProvider readerWriterProvider : serviceLoader) {
TweedReaderWriterProvider.ProviderContext providerContext = new TweedReaderWriterProvider.ProviderContext() {
@Override
public void registerReaderFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>> readerFactory
) {
if (readerFactories.putIfAbsent(id, readerFactory) != null) {
log.warn(
"Found duplicate Tweed entry reader id \"{}\" in provider class {}",
id,
readerWriterProvider.getClass().getName()
);
}
}
@Override
public void registerWriterFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>> writerFactory
) {
if (writerFactories.putIfAbsent(id, writerFactory) != null) {
log.warn(
"Found duplicate Tweed entry writer id \"{}\" in provider class {}",
id,
readerWriterProvider.getClass().getName()
);
}
}
};
readerWriterProvider.provideReaderWriters(providerContext);
}
}
@Override
public void apply(ConfigEntry<?> configEntry, WeavingContext context) {
EntryReadWriteConfig entryConfig = context.annotations().getAnnotation(EntryReadWriteConfig.class);
if (entryConfig == null) {
return;
}
ReadWriteExtension readWriteExtension = context.configContainer().extension(ReadWriteExtension.class);
if (readWriteExtension == null) {
log.error("You must not use {} without the {}", this.getClass().getSimpleName(), ReadWriteExtension.class.getSimpleName());
return;
}
readWriteExtension.setEntryReaderWriterDefinition(configEntry, createDefinitionFromEntryConfig(entryConfig, context));
}
private EntryReaderWriterDefinition createDefinitionFromEntryConfig(EntryReadWriteConfig entryConfig, WeavingContext context) {
String readerSpecText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader();
String writerSpecText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer();
SerdePojoReaderWriterSpec readerSpec;
SerdePojoReaderWriterSpec writerSpec;
if (readerSpecText.equals(writerSpecText)) {
readerSpec = writerSpec = specFromText(readerSpecText, context);
} else {
readerSpec = specFromText(readerSpecText, context);
writerSpec = specFromText(writerSpecText, context);
}
//noinspection unchecked
TweedEntryReader<?, ?> reader = readerSpec == null
? TweedEntryReaderWriterImpls.NOOP_READER_WRITER
: resolveReaderWriterFromSpec((Class<TweedEntryReader<?, ?>>)(Object) TweedEntryReader.class, readerFactories, readerSpec, context);
//noinspection unchecked
TweedEntryWriter<?, ?> writer = writerSpec == null
? TweedEntryReaderWriterImpls.NOOP_READER_WRITER
: resolveReaderWriterFromSpec((Class<TweedEntryWriter<?, ?>>)(Object) TweedEntryWriter.class, writerFactories, writerSpec, context);
return new EntryReaderWriterDefinition() {
@Override
public TweedEntryReader<?, ?> reader() {
return reader;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return writer;
}
};
}
@Nullable
private SerdePojoReaderWriterSpec specFromText(String specText, WeavingContext context) {
if (specText.isEmpty()) {
return null;
}
try {
return SerdePojoReaderWriterSpec.parse(specText);
} catch (SerdePojoReaderWriterSpec.ParseException e) {
log.warn(
"Failed to parse definition for reader or writer on entry {}, entry will not be included in serde",
context.path(),
e
);
return null;
}
}
private <T> T resolveReaderWriterFromSpec(
Class<T> baseClass,
Map<String, TweedReaderWriterProvider.ReaderWriterFactory<T>> factories,
SerdePojoReaderWriterSpec spec,
WeavingContext context
) {
//noinspection unchecked
T[] arguments = spec.arguments()
.stream()
.map(argSpec -> resolveReaderWriterFromSpec(baseClass, factories, argSpec, context))
.toArray(length -> (T[]) Array.newInstance(baseClass, length));
TweedReaderWriterProvider.ReaderWriterFactory<T> factory = factories.get(spec.identifier());
T instance;
if (factory != null) {
instance = factory.create(arguments);
} else {
instance = loadClassIfExists(baseClass, spec.identifier(), arguments);
}
if (instance == null) {
log.warn(
"Failed to resolve reader or writer factory \"{}\" for entry {}, entry will not be included in serde",
spec.identifier(),
context.path()
);
return null;
}
return instance;
}
private <T> T loadClassIfExists(Class<T> baseClass, String className, T[] arguments) {
try {
Class<?> clazz = Class.forName(className);
Class<?>[] argClassses = new Class<?>[arguments.length];
Arrays.fill(argClassses, baseClass);
Constructor<?> constructor = clazz.getConstructor(argClassses);
//noinspection unchecked
return (T) constructor.newInstance((Object[]) arguments);
} catch (ClassNotFoundException e) {
return null;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
log.warn("Failed to instantiate class {}", className, e);
return null;
}
}
}

View File

@@ -0,0 +1,170 @@
package de.siphalor.tweed5.weaver.pojoext.serde.impl;
import lombok.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.PrimitiveIterator;
@Value
public class SerdePojoReaderWriterSpec {
String identifier;
List<SerdePojoReaderWriterSpec> arguments;
public static SerdePojoReaderWriterSpec parse(String input) throws ParseException {
Lexer lexer = new Lexer(input.codePoints().iterator());
SerdePojoReaderWriterSpec spec = parseSpec(lexer);
lexer.chompWhitespace();
int codePoint = lexer.nextCodePoint();
if (codePoint != -1) {
throw lexer.createException("Found trailing text after spec", codePoint);
}
return spec;
}
private static SerdePojoReaderWriterSpec parseSpec(Lexer lexer) throws ParseException {
lexer.chompWhitespace();
String identifier = lexer.nextIdentifier();
lexer.chompWhitespace();
int codePoint = lexer.peekCodePoint();
if (codePoint == '(') {
lexer.nextCodePoint();
lexer.chompWhitespace();
if (lexer.peekCodePoint() == ')') {
lexer.nextCodePoint();
return new SerdePojoReaderWriterSpec(identifier, Collections.emptyList());
}
SerdePojoReaderWriterSpec spec = new SerdePojoReaderWriterSpec(identifier, parseSpecList(lexer));
codePoint = lexer.nextCodePoint();
if (codePoint != ')') {
throw lexer.createException("Argument list must be ended with a closing parenthesis", codePoint);
}
return spec;
} else {
return new SerdePojoReaderWriterSpec(identifier, Collections.emptyList());
}
}
private static List<SerdePojoReaderWriterSpec> parseSpecList(Lexer lexer) throws ParseException {
List<SerdePojoReaderWriterSpec> specs = new ArrayList<>();
while (true) {
specs.add(parseSpec(lexer));
lexer.chompWhitespace();
int codePoint = lexer.peekCodePoint();
if (codePoint != ',') {
break;
}
lexer.nextCodePoint();
}
return Collections.unmodifiableList(specs);
}
@RequiredArgsConstructor
private static class Lexer {
private static final int EMPTY = -2;
private final PrimitiveIterator.OfInt codePointIterator;
private int peek = EMPTY;
private int index;
public String nextIdentifier() throws ParseException {
int codePoint = nextCodePoint();
if (codePoint == -1) {
throw createException("Expected identifier, got end of input", codePoint);
} else if (!isIdentifierChar(codePoint)) {
throw createException("Expected identifier (alphanumeric character)", codePoint);
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.appendCodePoint(codePoint);
boolean dot = false;
while ((codePoint = peekCodePoint()) >= 0) {
if (isIdentifierChar(codePoint)) {
stringBuilder.appendCodePoint(nextCodePoint());
dot = false;
} else if (codePoint == '.') {
if (dot) {
throw createException("Unexpected double dot in identifier", codePoint);
} else {
stringBuilder.appendCodePoint(nextCodePoint());
dot = true;
}
} else {
break;
}
}
if (dot) {
throw createException("Identifier must not end with dot", codePoint);
}
return stringBuilder.toString();
}
private boolean isIdentifierChar(int codePoint) {
return (codePoint >= '0' && codePoint <= '9')
|| (codePoint >= 'a' && codePoint <= 'z')
|| (codePoint >= 'A' && codePoint <= 'Z');
}
public void chompWhitespace() {
while (Character.isWhitespace(peekCodePoint())) {
nextCodePoint();
}
}
private int peekCodePoint() {
if (peek == EMPTY) {
peek = nextCodePoint();
}
return peek;
}
private int nextCodePoint() {
if (peek != EMPTY) {
int codePoint = peek;
peek = EMPTY;
return codePoint;
}
if (codePointIterator.hasNext()) {
index++;
return codePointIterator.nextInt();
} else {
return -1;
}
}
public ParseException createException(String message, int codePoint) {
return new ParseException(message, index, codePoint);
}
}
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public static class ParseException extends Exception {
private final int index;
private final int codePoint;
public ParseException(String message, int index, int codePoint) {
super(message);
this.index = index;
this.codePoint = codePoint;
}
@Override
public String getMessage() {
String message = super.getMessage();
StringBuilder stringBuilder = new StringBuilder(30 + message.length())
.append("Parse error at index ")
.append(index)
.append(" \"");
if (codePoint == -1) {
stringBuilder.append("EOF");
} else {
stringBuilder.appendCodePoint(codePoint);
}
return stringBuilder
.append("\": ")
.append(message)
.toString();
}
}
}

View File

@@ -0,0 +1,101 @@
package de.siphalor.tweed5.weaver.pojoext.serde;
import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.TweedPojoWeaverBootstrapper;
import de.siphalor.tweed5.weaver.pojoext.serde.api.EntryReadWriteConfig;
import de.siphalor.tweed5.weaver.pojoext.serde.api.ReadWritePojoPostProcessor;
import lombok.*;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.io.StringWriter;
import static org.assertj.core.api.Assertions.assertThat;
class WeaverPojoSerdeExtensionTest {
@Test
@SneakyThrows
void testAnnotated() {
TweedPojoWeaverBootstrapper<AnnotatedConfig> weaverBootstrapper = TweedPojoWeaverBootstrapper.create(AnnotatedConfig.class);
ConfigContainer<AnnotatedConfig> configContainer = weaverBootstrapper.weave();
configContainer.initialize();
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class);
assertThat(readWriteExtension).isNotNull();
AnnotatedConfig config = new AnnotatedConfig(123, "test", new TestClass(987));
StringWriter stringWriter = new StringWriter();
HjsonWriter hjsonWriter = new HjsonWriter(stringWriter, new HjsonWriter.Options());
readWriteExtension.write(hjsonWriter, config, configContainer.rootEntry(), readWriteExtension.createReadWriteContextExtensionsData());
assertThat(stringWriter).hasToString("{\n\tanInt: 123\n\ttext: test\n\ttest: my cool custom writer\n}\n");
HjsonReader reader = new HjsonReader(new HjsonLexer(new StringReader(
"{\n\tanInt: 987\n\ttext: abdef\n\ttest: { inner: 29 }\n}"
)));
assertThat(readWriteExtension.read(
reader,
configContainer.rootEntry(),
readWriteExtension.createReadWriteContextExtensionsData()
)).isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
}
@AutoService(TweedReaderWriterProvider.class)
public static class TestWriterProvider implements TweedReaderWriterProvider {
@Override
public void provideReaderWriters(ProviderContext context) {
context.registerWriterFactory("tweed5.test.dummy", delegates -> new TweedEntryWriter<Object, ConfigEntry<Object>>() {
@Override
public void write(
TweedDataVisitor writer,
Object value,
ConfigEntry<Object> entry,
TweedWriteContext context
) throws TweedDataWriteException {
writer.visitString("my cool custom writer");
}
});
}
}
@PojoWeaving(extensions = ReadWriteExtension.class, postProcessors = ReadWritePojoPostProcessor.class)
@CompoundWeaving
@EntryReadWriteConfig("tweed5.compound")
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public static class AnnotatedConfig {
@EntryReadWriteConfig("tweed5.integer")
public int anInt;
@EntryReadWriteConfig("tweed5.nullable(tweed5.string)")
public String text;
@EntryReadWriteConfig(writer = "tweed5.test.dummy", reader = "tweed5.compound")
@CompoundWeaving
public TestClass test;
}
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public static class TestClass {
@EntryReadWriteConfig("tweed5.integer")
public int inner;
}
}

View File

@@ -0,0 +1,65 @@
package de.siphalor.tweed5.weaver.pojoext.serde.impl;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.util.Arrays;
import java.util.Collections;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
class SerdePojoReaderWriterSpecTest {
@ParameterizedTest
@CsvSource(ignoreLeadingAndTrailingWhitespace = false, value = {
" abc ,abc",
" abc() ,abc",
" abc.123 ,abc.123",
"abc.123 ( ) ,abc.123",
"123.abc,123.abc",
})
@SneakyThrows
void parseSimpleIdentifier(String input, String identifier) {
SerdePojoReaderWriterSpec spec = SerdePojoReaderWriterSpec.parse(input);
assertThat(spec.identifier()).isEqualTo(identifier);
assertThat(spec.arguments()).isEmpty();
}
@Test
@SneakyThrows
void parseNested() {
SerdePojoReaderWriterSpec spec = SerdePojoReaderWriterSpec.parse("abc.def ( 12 ( def, ghi ( ) ), jkl ) ");
assertThat(spec).isEqualTo(new SerdePojoReaderWriterSpec("abc.def", Arrays.asList(
new SerdePojoReaderWriterSpec("12", Arrays.asList(
new SerdePojoReaderWriterSpec("def", Collections.emptyList()),
new SerdePojoReaderWriterSpec("ghi", Collections.emptyList())
)),
new SerdePojoReaderWriterSpec("jkl", Collections.emptyList())
)));
}
@ParameterizedTest
@CsvSource(ignoreLeadingAndTrailingWhitespace = false, nullValues = "EOF", delimiter = ';', value = {
" abc def ;6;d",
"abcäöüdef;4;ä",
"abc.def(;8;EOF",
"'';0;EOF",
",;1;,",
"abc(,);5;,",
"abc..def;5;.",
})
@SneakyThrows
void parseError(String input, int index, String codePoint) {
assertThatThrownBy(() -> SerdePojoReaderWriterSpec.parse(input))
.asInstanceOf(type(SerdePojoReaderWriterSpec.ParseException.class))
.isInstanceOf(SerdePojoReaderWriterSpec.ParseException.class)
.satisfies(
exception -> assertThat(exception.index()).as("index of: " + exception.getMessage()).isEqualTo(index),
exception -> assertThat(exception.codePoint()).as("code point of: " + exception.getMessage())
.isEqualTo(codePoint == null ? -1 : codePoint.codePointAt(0))
);
}
}