mirror of
https://github.com/collinsmith/riiablo.git
synced 2025-03-13 03:21:24 +07:00
Added code generation API for generating excel serializers
This commit is contained in:
parent
32f9db81c2
commit
9b95eb1ac8
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user