diff --git a/core/build.gradle b/core/build.gradle index 1577da4c..f8e3818e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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" diff --git a/core/src/main/java/com/riiablo/excel2/Serializer.java b/core/src/main/java/com/riiablo/excel2/Serializer.java index cea27943..82e7f1ea 100644 --- a/core/src/main/java/com/riiablo/excel2/Serializer.java +++ b/core/src/main/java/com/riiablo/excel2/Serializer.java @@ -6,4 +6,6 @@ import com.riiablo.io.ByteOutput; public interface Serializer { void readBin(T entry, ByteInput in); void writeBin(T entry, ByteOutput out); + boolean equals(T e1, T e2); + void logErrors(T e1, T e2); } diff --git a/core/src/main/java/com/riiablo/excel2/SerializerGenerator.java b/core/src/main/java/com/riiablo/excel2/SerializerGenerator.java new file mode 100644 index 00000000..bc980bd3 --- /dev/null +++ b/core/src/main/java/com/riiablo/excel2/SerializerGenerator.java @@ -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); + } +} diff --git a/core/src/main/java/com/riiablo/excel2/SerializerSourceGenerator.java b/core/src/main/java/com/riiablo/excel2/SerializerSourceGenerator.java new file mode 100644 index 00000000..addc8388 --- /dev/null +++ b/core/src/main/java/com/riiablo/excel2/SerializerSourceGenerator.java @@ -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.class; + + final Array 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(); + } +} diff --git a/core/src/main/java/com/riiablo/excel2/txt/MonStats.java b/core/src/main/java/com/riiablo/excel2/txt/MonStats.java index 04f5c59f..3f784fb4 100644 --- a/core/src/main/java/com/riiablo/excel2/txt/MonStats.java +++ b/core/src/main/java/com/riiablo/excel2/txt/MonStats.java @@ -30,5 +30,7 @@ public class MonStats extends Excel { public static class Serializer implements com.riiablo.excel2.Serializer { @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) {} } } diff --git a/core/src/test/java/com/riiablo/excel2/SerializerGeneratorTest.java b/core/src/test/java/com/riiablo/excel2/SerializerGeneratorTest.java new file mode 100644 index 00000000..79fe2f49 --- /dev/null +++ b/core/src/test/java/com/riiablo/excel2/SerializerGeneratorTest.java @@ -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(); + } +} diff --git a/core/src/test/java/com/riiablo/excel2/SerializerSourceGeneratorTest.java b/core/src/test/java/com/riiablo/excel2/SerializerSourceGeneratorTest.java new file mode 100644 index 00000000..610f08a0 --- /dev/null +++ b/core/src/test/java/com/riiablo/excel2/SerializerSourceGeneratorTest.java @@ -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 > JavaFile generateFile(SerializerSourceGenerator generator, Class excelClass, Class 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); + } +}