Added code generation API for generating excel serializers

This commit is contained in:
Collin Smith 2020-12-11 17:54:46 -08:00
parent 32f9db81c2
commit 9b95eb1ac8
7 changed files with 505 additions and 0 deletions

View File

@ -35,6 +35,11 @@ dependencies {
api "com.jcraft:jzlib:1.1.3"
}
// Excel
dependencies {
implementation "com.squareup:javapoet:1.13.0"
}
// Networking
dependencies {
api "com.google.flatbuffers:flatbuffers-java:$flatbuffersVersion"

View File

@ -6,4 +6,6 @@ import com.riiablo.io.ByteOutput;
public interface Serializer<T extends Excel.Entry> {
void readBin(T entry, ByteInput in);
void writeBin(T entry, ByteOutput out);
boolean equals(T e1, T e2);
void logErrors(T e1, T e2);
}

View File

@ -0,0 +1,81 @@
package com.riiablo.excel2;
import com.squareup.javapoet.JavaFile;
import java.io.File;
import java.util.Arrays;
import com.badlogic.gdx.files.FileHandle;
import com.riiablo.excel2.Excel.Entry;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;
import com.riiablo.util.ClassUtils;
public class SerializerGenerator {
private static final Logger log = LogManager.getLogger(SerializerGenerator.class);
String sourcePackage = "com.riiablo.excel2.txt";
String serializerPackage = "com.riiablo.excel2.serializer";
FileHandle sourceDir;
FileHandle serializerDir;
SerializerSourceGenerator sourceGenerator;
void init() {
sourceGenerator = new SerializerSourceGenerator(sourcePackage, serializerPackage);
}
public void generateSerializers() {
log.info("Generating serializers for {}...", sourceDir);
FileHandle[] sourceFiles = sourceDir.list("java");
for (FileHandle sourceFile : sourceFiles) {
try {
log.info("Generating: '{}'", sourceFile);
configureSourceGenerator(sourceFile);
JavaFile serializerFile = sourceGenerator.generateFile(SerializerGenerator.class);
File file = serializerFile.writeToFile(serializerDir.file());
log.debug("Generated: '{}'", file);
} catch (Throwable t) {
log.error("Failed to generate serializer for {}", sourceFile, t);
}
}
}
public void configureSourceGenerator(FileHandle sourceFile) throws ClassNotFoundException {
String sourceName = sourceFile.nameWithoutExtension();
log.trace("sourceName: {}", sourceName);
Class sourceClass = Class.forName(sourcePackage + "." + sourceName);
// Prevent serializing literal Excel.class and non-subclasses of Excel.class
if (sourceClass == Excel.class || !Excel.class.isAssignableFrom(sourceClass)) {
return;
}
// Find impls of Entry.class within sourceClass
Class entryClass;
Class[] entryClasses = ClassUtils.findDeclaredClasses(sourceClass, Entry.class);
switch (entryClasses.length) {
case 0:
log.error("{} does not contain an implementation of {}", sourceClass, Entry.class);
return;
case 1:
entryClass = entryClasses[0];
log.trace("entryClass: {}", entryClass.getCanonicalName());
break;
default:
log.error("{} contains ambiguous implementations of {}: {}",
sourceClass, Entry.class, Arrays.toString(entryClasses));
return;
}
if (!entryClass.getSimpleName().equals(Entry.class.getSimpleName())) {
log.warn("entry class {} not named {}",
entryClass.getCanonicalName(),
sourceClass.getCanonicalName() + "$" + Entry.class.getSimpleName());
// return; // Allow it for now
}
sourceGenerator.configure(sourceClass, entryClass);
}
}

View File

@ -0,0 +1,358 @@
package com.riiablo.excel2;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import javax.annotation.Generated;
import javax.lang.model.element.Modifier;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateFormatUtils;
import com.badlogic.gdx.utils.Array;
import com.riiablo.excel2.Excel.Entry.Column;
import com.riiablo.io.ByteInput;
import com.riiablo.io.ByteOutput;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;
import static java.lang.reflect.Modifier.isTransient;
public class SerializerSourceGenerator {
private static final Class<Serializer> SERIALIZER = Serializer.class;
final Array<ColumnInfo> columns = new Array<>(true, 256, ColumnInfo.class);
String sourcePackage;
String serializerPackage;
ClassName serializerName;
ClassName excelName;
ClassName entryName;
public SerializerSourceGenerator() {
this("com.riiablo.excel2.txt", "com.riiablo.excel2.serializer");
}
static final class ColumnInfo {
final Column config;
final Class type;
final String name;
ColumnInfo(Column config, Class type, String name) {
this.config = config;
this.type = type;
this.name = name;
}
@Override
public String toString() {
return name;
}
}
SerializerSourceGenerator(String sourcePackage, String serializerPackage) {
this.sourcePackage = sourcePackage;
this.serializerPackage = serializerPackage;
serializerName = ClassName.get(SERIALIZER);
}
String serializerName() {
return excelName.simpleName() + serializerName.simpleName();
}
String serializerSource() {
return entryName.canonicalName();
}
SerializerSourceGenerator configure(Class excelClass, Class entryClass) {
excelName = ClassName.get(excelClass);
entryName = ClassName.get(entryClass);
columns.clear();
for (Field field : entryClass.getFields()) {
if (isTransient(field.getModifiers())) continue;
Column column = field.getAnnotation(Column.class);
if (column == null) continue;
columns.add(new ColumnInfo(
column,
field.getType(),
field.getName()
));
}
return this;
}
JavaFile generateFile() {
return generateFile(serializerSource());
}
JavaFile generateFile(String comment) {
return generateFile(SerializerSourceGenerator.class, comment);
}
JavaFile generateFile(Class codeGenerator) {
return generateFile(codeGenerator, serializerSource());
}
AnnotationSpec createGeneratedAnnotation(Class codeGenerator, String comment) {
AnnotationSpec.Builder generated = AnnotationSpec
.builder(Generated.class)
.addMember("value", "$S", codeGenerator.getCanonicalName())
.addMember("date", "$S", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(new Date()))
;
if (StringUtils.isNotBlank(comment)) generated.addMember("comments", "$S", comment);
return generated.build();
}
AnnotationSpec createSuppressWarningsAnnotation() {
return AnnotationSpec
.builder(SuppressWarnings.class)
.addMember("value", "$S", "unused")
.build();
}
JavaFile generateFile(Class codeGenerator, String comment) {
Validate.notNull(excelName, "excelName not configured");
Validate.notNull(entryName, "entryName not configured");
TypeSpec.Builder serializerType = generate_Serializer()
.addAnnotation(createGeneratedAnnotation(codeGenerator, comment))
.addAnnotation(createSuppressWarningsAnnotation())
;
return generateFile(serializerType.build());
}
JavaFile generateFile(TypeSpec serializerType) {
return JavaFile
.builder(serializerPackage, serializerType)
.skipJavaLangImports(true)
.addFileComment("automatically generated by $L, do not modify", SerializerSourceGenerator.class.getCanonicalName())
.build();
}
TypeSpec.Builder generate_Serializer() {
FieldSpec log = FieldSpec
.builder(Logger.class, "log", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.getLogger($N.class)", LogManager.class, serializerName())
.build();
return TypeSpec
.classBuilder(serializerName())
.addSuperinterface(ParameterizedTypeName.get(serializerName, entryName))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(log)
.addMethod(generate_readBin())
.addMethod(generate_writeBin())
.addMethod(generate_equals())
.addMethod(generate_logErrors(log))
;
}
static CodeBlock qualify(Object object, Object field) {
return CodeBlock.of("$N.$N", object, field);
}
static CodeBlock readX(Object in, Class type, Object var) {
return CodeBlock.of("$L = $N.$N$L()", var, in, "read", getIoMethod(type));
}
static CodeBlock writeX(Object out, Class type, Object var) {
return CodeBlock.of("$N.$N$L($L)", out, "write", getIoMethod(type), var);
}
static CodeBlock logX(Object log, Object message, Object args) {
return CodeBlock.of("$N.$N($S, $L)", log, "error", message, args);
}
static CodeBlock equalsX(Type type, Object obj1, Object obj2) {
return CodeBlock.of("$T.equals($L, $L)", type, obj1, obj2);
}
static CodeBlock defaultString(Object var) {
return CodeBlock.of("$T.$N($L)", StringUtils.class, "defaultString", var);
}
static String getIoMethod(Type type) {
if (type == String.class) {
return "String";
} else if (type == byte.class) {
return "8";
} else if (type == short.class) {
return "16";
} else if (type == int.class) {
return "32";
} else if (type == long.class) {
return "64";
} else if (type == boolean.class) {
return "Boolean";
} else {
throw new UnsupportedOperationException(type + " is not supported!");
}
}
MethodSpec generate_readBin() {
ParameterSpec entry = ParameterSpec.builder(entryName, "entry").build();
ParameterSpec in = ParameterSpec.builder(ByteInput.class, "in").build();
MethodSpec.Builder method = MethodSpec
.methodBuilder("readBin")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(entry)
.addParameter(in)
;
for (ColumnInfo column : columns) {
final Column config = column.config;
final Class type = column.type;
final CodeBlock field = qualify(entry, column.name);
if (type.isArray()) {
final Class componentType = type.getComponentType();
final String var = "x";
method.addCode(CodeBlock.builder()
.addStatement(
"$L = new $T[$L]", field, componentType, config.endIndex() - config.startIndex())
.beginControlFlow(
"for (int $1N = $2L; $1N < $3L; $1N++)", var, 0, config.endIndex() - config.startIndex())
.addStatement(
readX(in, componentType, CodeBlock.of("$L[$N]", field, var)))
.endControlFlow()
.build());
} else {
method.addCode(CodeBlock.builder()
.addStatement(readX(in, type, field))
.build());
}
}
return method.build();
}
MethodSpec generate_writeBin() {
ParameterSpec entry = ParameterSpec.builder(entryName, "entry").build();
ParameterSpec out = ParameterSpec.builder(ByteOutput.class, "out").build();
MethodSpec.Builder method = MethodSpec
.methodBuilder("writeBin")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(entry)
.addParameter(out)
;
for (ColumnInfo column : columns) {
final Class type = column.type;
final CodeBlock field = qualify(entry, column.name);
if (type.isArray()) {
final Class componentType = type.getComponentType();
final String var = "x";
method.addCode(CodeBlock.builder()
.beginControlFlow(
"for ($T $N : $L)", componentType, var, field)
.addStatement(componentType == String.class
? writeX(out, componentType, defaultString(var))
: writeX(out, componentType, var))
.endControlFlow()
.build());
} else {
method.addCode(CodeBlock.builder()
.addStatement(type == String.class
? writeX(out, type, defaultString(field))
: writeX(out, type, field))
.build());
}
}
return method.build();
}
MethodSpec generate_equals() {
ParameterSpec e1 = ParameterSpec.builder(entryName, "e1").build();
ParameterSpec e2 = ParameterSpec.builder(entryName, "e2").build();
MethodSpec.Builder method = MethodSpec
.methodBuilder("equals")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(boolean.class)
.addParameter(e1)
.addParameter(e2)
;
for (ColumnInfo column : columns) {
final Class type = column.type;
final String name = column.name;
final CodeBlock e1Field = qualify(e1, name);
final CodeBlock e2Field = qualify(e2, name);
final CodeBlock.Builder block = CodeBlock.builder();
if (type.isPrimitive()) {
block.beginControlFlow("if ($L != $L)",
e1Field,
e2Field);
} else {
block.beginControlFlow("if (!$L)",
equalsX(
type.isArray() ? Arrays.class : Objects.class,
e1Field,
e2Field));
}
method.addCode(block
.addStatement("return false")
.endControlFlow()
.build());
}
method.addStatement("return true");
return method.build();
}
MethodSpec generate_logErrors(FieldSpec log) {
ParameterSpec e1 = ParameterSpec.builder(entryName, "e1").build();
ParameterSpec e2 = ParameterSpec.builder(entryName, "e2").build();
MethodSpec.Builder method = MethodSpec
.methodBuilder("logErrors")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(e1)
.addParameter(e2)
;
for (ColumnInfo column : columns) {
final Class type = column.type;
final String name = column.name;
final CodeBlock e1Field = qualify(e1, name);
final CodeBlock e2Field = qualify(e2, name);
final CodeBlock.Builder block = CodeBlock.builder();
if (type.isPrimitive()) {
block.beginControlFlow("if ($L != $L)",
e1Field,
e2Field);
} else {
block.beginControlFlow("if (!$L)",
equalsX(
type.isArray() ? Arrays.class : Objects.class,
e1Field,
e2Field));
}
method.addCode(block
.addStatement(logX(log,
CodeBlock.of("$L does not match: $N={}, $N={}", name, e1, e2),
CodeBlock.of("$L, $L", e1Field, e2Field)))
.endControlFlow()
.build());
}
return method.build();
}
}

View File

@ -30,5 +30,7 @@ public class MonStats extends Excel<MonStats.Entry, MonStats.Serializer> {
public static class Serializer implements com.riiablo.excel2.Serializer<Entry> {
@Override public void readBin(Entry entry, ByteInput in) {}
@Override public void writeBin(Entry entry, ByteOutput out) {}
@Override public boolean equals(Entry e1, Entry e2) { throw new UnsupportedOperationException(); }
@Override public void logErrors(Entry e1, Entry e2) {}
}
}

View File

@ -0,0 +1,32 @@
package com.riiablo.excel2;
import org.junit.BeforeClass;
import org.junit.Test;
import com.badlogic.gdx.Gdx;
import com.riiablo.RiiabloTest;
import com.riiablo.logger.Level;
import com.riiablo.logger.LogManager;
public class SerializerGeneratorTest extends RiiabloTest {
@BeforeClass
public static void before() {
LogManager.setLevel("com.riiablo.excel2", Level.TRACE);
}
@Test
public void monstats() {
SerializerGenerator generator = new SerializerGenerator();
generator.init();
generator.sourceDir = Gdx.files.absolute(
"C:\\Users\\csmith\\projects\\libgdx\\riiablo"
+ "\\core\\src\\main\\java\\com\\riiablo\\excel2\\txt");
generator.serializerDir = Gdx.files.absolute(
"C:\\Users\\csmith\\projects\\libgdx\\riiablo"
+ "\\core\\src\\main\\java");
generator.sourcePackage = "com.riiablo.excel2.txt";
generator.serializerPackage = "com.riiablo.excel2.serializer";
generator.generateSerializers();
}
}

View File

@ -0,0 +1,25 @@
package com.riiablo.excel2;
import com.squareup.javapoet.JavaFile;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import com.riiablo.excel2.txt.MonStats;
public class SerializerSourceGeneratorTest {
@Rule
public TestName name = new TestName();
private static <E extends Excel.Entry, T extends Excel<E, ?>> JavaFile generateFile(SerializerSourceGenerator generator, Class<T> excelClass, Class<E> entryClass) {
return generator.configure(excelClass, entryClass).generateFile();
}
@Test
public void monstats() throws IOException {
SerializerSourceGenerator generator = new SerializerSourceGenerator();
JavaFile file = generateFile(generator, MonStats.class, MonStats.Entry.class);
file.writeTo(System.out);
}
}