From e0b4904efe81bf7bbd926bf31455556df390c731 Mon Sep 17 00:00:00 2001 From: Collin Smith Date: Sat, 19 Dec 2020 14:20:24 -0800 Subject: [PATCH] Added support for foreign keys within schemas --- .../table/annotation/AnnotationElement.java | 14 ++ .../riiablo/table/annotation/Constants.java | 26 ++- .../table/annotation/FieldElement.java | 24 +- .../table/annotation/ForeignKeyElement.java | 23 ++ .../annotation/InjectorCodeGenerator.java | 74 +++++++ .../table/annotation/InjectorElement.java | 72 ++++++ .../table/annotation/ParserCodeGenerator.java | 6 +- .../table/annotation/SchemaElement.java | 32 ++- .../table/annotation/SchemaProcessor.java | 192 ++++++++++++---- .../annotation/SerializerCodeGenerator.java | 5 +- .../table/annotation/TableCodeGenerator.java | 6 +- .../riiablo/table/annotation/ForeignKey.java | 19 ++ .../riiablo/table/annotation/Injector.java | 23 ++ .../main/java/com/riiablo/table/Injector.java | 12 + .../main/java/com/riiablo/table/Table.java | 15 +- .../com/riiablo/table/schema/MonStats.java | 4 + .../com/riiablo/table/schema/MonStats2.java | 206 ++++++++++-------- .../table/schema/MonStatsInjectorImpl.java | 10 + 18 files changed, 620 insertions(+), 143 deletions(-) create mode 100644 table/annotation-processor/src/main/java/com/riiablo/table/annotation/ForeignKeyElement.java create mode 100644 table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorCodeGenerator.java create mode 100644 table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorElement.java create mode 100644 table/annotations/src/main/java/com/riiablo/table/annotation/ForeignKey.java create mode 100644 table/annotations/src/main/java/com/riiablo/table/annotation/Injector.java create mode 100644 table/core/src/main/java/com/riiablo/table/Injector.java create mode 100644 table/integration/src/main/java/com/riiablo/table/schema/MonStatsInjectorImpl.java diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/AnnotationElement.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/AnnotationElement.java index 9281b0e9..aa21251f 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/AnnotationElement.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/AnnotationElement.java @@ -23,6 +23,20 @@ abstract class AnnotationElement { return context.elementUtils.getElementValuesWithDefaults(mirror); } + AnnotationValue value(String key) { + for ( + Map.Entry< + ? extends ExecutableElement, + ? extends AnnotationValue + > entry : defaults().entrySet()) { + if (entry.getKey().getSimpleName().contentEquals(key)) { + return entry.getValue(); + } + } + + return null; + } + @Override public String toString() { return new ToStringBuilder(this) diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/Constants.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/Constants.java index 43ab1182..6713a009 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/Constants.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/Constants.java @@ -4,8 +4,17 @@ import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import javax.lang.model.element.Element; import javax.lang.model.element.Name; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.apache.commons.lang3.ArrayUtils; +import static com.squareup.javapoet.TypeName.BOOLEAN; +import static com.squareup.javapoet.TypeName.BYTE; +import static com.squareup.javapoet.TypeName.INT; +import static com.squareup.javapoet.TypeName.LONG; +import static com.squareup.javapoet.TypeName.SHORT; + final class Constants { private Constants() {} @@ -17,11 +26,24 @@ final class Constants { static final ClassName STRING = ClassName.get(String.class); static final ClassName PRIMARY_KEY = ClassName.get(PrimaryKey.class); + static final ClassName FOREIGN_KEY = ClassName.get(ForeignKey.class); static final ClassName FORMAT = ClassName.get(Format.class); - static final TypeName[] PRIMARY_KEY_TYPES = { TypeName.INT, STRING }; + static final TypeName[] PRIMARY_KEY_TYPES = { INT, STRING }; - static boolean isPrimaryKey(Element element) { + static boolean isPrimaryKeyType(Element element) { return ArrayUtils.contains(PRIMARY_KEY_TYPES, TypeName.get(element.asType())); } + + static final TypeName[] RECORD_FIELD_TYPES = { + BYTE, SHORT, INT, LONG, BOOLEAN, STRING + }; + + static boolean isRecordFieldType(Element element) { + TypeMirror mirror = element.asType(); + return ArrayUtils.contains(RECORD_FIELD_TYPES, + TypeName.get(mirror.getKind() == TypeKind.ARRAY + ? ((ArrayType) mirror).getComponentType() + : mirror)); + } } diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/FieldElement.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/FieldElement.java index f0dc90b3..65dfe4e6 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/FieldElement.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/FieldElement.java @@ -14,6 +14,7 @@ final class FieldElement { static FieldElement get(Context context, VariableElement element) { FormatElement formatElement = FormatElement.get(context, element); PrimaryKeyElement primaryKeyElement = PrimaryKeyElement.get(context, element); + ForeignKeyElement foreignKeyElement = ForeignKeyElement.get(context, element); Set modifiers = element.getModifiers(); if (!modifiers.contains(Modifier.PUBLIC)) { context.warn(element, "record fields should be declared {}", Modifier.PUBLIC); @@ -27,12 +28,15 @@ final class FieldElement { context.error(element, "'{}' is an illegal record field name", Constants.RESERVED_NAME); return null; } - return new FieldElement(element, formatElement, primaryKeyElement); + if (foreignKeyElement == null && !Constants.isRecordFieldType(element)) { + context.error(element, "{element} is not a supported record field type"); + } + return new FieldElement(element, formatElement, primaryKeyElement, foreignKeyElement); } static FieldElement firstPrimaryKey(Collection fields) { for (FieldElement field : fields) { - if (field.primaryKeyElement != null || Constants.isPrimaryKey(field.element)) { + if (field.primaryKeyElement != null || Constants.isPrimaryKeyType(field.element)) { return field; } } @@ -44,13 +48,19 @@ final class FieldElement { final TypeMirror mirror; final FormatElement formatElement; final PrimaryKeyElement primaryKeyElement; + final ForeignKeyElement foreignKeyElement; final String[] fieldNames; - FieldElement(VariableElement element, FormatElement formatElement, PrimaryKeyElement primaryKeyElement) { + FieldElement( + VariableElement element, + FormatElement formatElement, + PrimaryKeyElement primaryKeyElement, + ForeignKeyElement foreignKeyElement) { this.element = element; this.mirror = element.asType(); this.formatElement = formatElement; this.primaryKeyElement = primaryKeyElement; + this.foreignKeyElement = foreignKeyElement; fieldNames = formatElement != null ? formatElement.fieldNames : ArrayUtils.toArray(element.getSimpleName().toString()); @@ -60,6 +70,14 @@ final class FieldElement { return element.getSimpleName(); } + boolean isPrimaryKey() { + return primaryKeyElement != null; + } + + boolean isForeignKey() { + return foreignKeyElement != null; + } + boolean isTransient() { return element.getModifiers().contains(Modifier.TRANSIENT); } diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ForeignKeyElement.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ForeignKeyElement.java new file mode 100644 index 00000000..176add0d --- /dev/null +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ForeignKeyElement.java @@ -0,0 +1,23 @@ +package com.riiablo.table.annotation; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; + +final class ForeignKeyElement extends AnnotationElement { + static ForeignKeyElement get(Context context, VariableElement element) { + ForeignKey annotation = element.getAnnotation(ForeignKey.class); + if (annotation == null) return null; + if (!element.getModifiers().contains(Modifier.PUBLIC)) { + context.warn(element, "{} fields must be declared {}", ForeignKey.class, Modifier.PUBLIC); + return null; + } + + AnnotationMirror mirror = context.getAnnotationMirror(element, Constants.FOREIGN_KEY); + return new ForeignKeyElement(context, annotation, mirror); + } + + ForeignKeyElement(Context context, ForeignKey annotation, AnnotationMirror mirror) { + super(context, annotation, mirror); + } +} diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorCodeGenerator.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorCodeGenerator.java new file mode 100644 index 00000000..e8424af6 --- /dev/null +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorCodeGenerator.java @@ -0,0 +1,74 @@ +package com.riiablo.table.annotation; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Map; +import javax.lang.model.element.Modifier; + +final class InjectorCodeGenerator extends CodeGenerator { + final ClassName tableManifest; + final Map tables; + + InjectorCodeGenerator( + Context context, + String injectorPackage, + ClassName tableManifest, + Map tables) { + super(context, injectorPackage); + this.tableManifest = tableManifest; + this.tables = tables; + } + + @Override + ClassName formatName(String packageName, SchemaElement schemaElement) { + return schemaElement.parserClassName = ClassName.get( + packageName, + schemaElement.element.getSimpleName() + Injector.class.getSimpleName()); + } + + @Override + TypeSpec.Builder newTypeSpec(SchemaElement schemaElement) { + return super.newTypeSpec(schemaElement) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(ParameterizedTypeName.get( + ClassName.get(com.riiablo.table.Injector.class), + ClassName.get(schemaElement.element), + tableManifest)) + .addMethod(inject(schemaElement)) + ; + } + + // R inject(Object manifest, R record); + MethodSpec inject(SchemaElement schemaElement) { + ClassName schemaName = ClassName.get(schemaElement.element); + final ParameterSpec manifest = ParameterSpec.builder(tableManifest, "arg0").build(); + final ParameterSpec record = ParameterSpec.builder(schemaName, "arg1").build(); + MethodSpec.Builder method = MethodSpec + .methodBuilder("inject") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(schemaName) + .addParameter(manifest) + .addParameter(record) + ; + + for (FieldElement field : schemaElement.foreignKeys) { + FieldSpec fieldSpec = tables.get(ClassName.get(field.element())); + if (fieldSpec == null) continue; + method.addStatement("$N.$N = $N.$N.get($N.$N)", + record, + field.name(), + manifest, + fieldSpec, + record, + field.foreignKeyElement.annotation.value()); + } + + method.addStatement("return $N", record); + return method.build(); + } +} diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorElement.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorElement.java new file mode 100644 index 00000000..f33d8691 --- /dev/null +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/InjectorElement.java @@ -0,0 +1,72 @@ +package com.riiablo.table.annotation; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.MirroredTypeException; +import org.apache.commons.lang3.builder.ToStringBuilder; + +final class InjectorElement { + static InjectorElement get(Context context, Element element) { + Injector annotation = element.getAnnotation(Injector.class); + final TypeElement injectorElement, injectorImplElement; + if (annotation == null) { + // Only need injectorElement if generating Injector impl + injectorElement = context.elementUtils.getTypeElement(com.riiablo.table.Injector.class.getCanonicalName()); + injectorImplElement = null; + } else { + // Only need injectorImplElement if @Injector present + injectorImplElement = getInjectorImpl(context, annotation); + injectorElement = null; + } + return new InjectorElement(annotation, injectorElement, injectorImplElement); + } + + static TypeElement getInjectorImpl(Context context, Injector annotation) { + if (annotation == null) return null; + try { + Class injectorImpl = annotation.value(); + return context.elementUtils.getTypeElement(injectorImpl.getCanonicalName()); + } catch (MirroredTypeException t) { + DeclaredType injectorImplMirror = (DeclaredType) t.getTypeMirror(); + return (TypeElement) injectorImplMirror.asElement(); + } + } + + final Injector annotation; + final TypeElement injectorElement; + final TypeElement injectorImplElement; + + InjectorElement( + Injector annotation, + TypeElement injectorElement, + TypeElement injectorImplElement) { + this.annotation = annotation; + this.injectorElement = injectorElement; + this.injectorImplElement = injectorImplElement; + } + + ExecutableElement getMethod(CharSequence methodName) { + for (Element e : injectorElement.getEnclosedElements()) { + if (e.getKind() == ElementKind.METHOD) { + ExecutableElement methodElement = (ExecutableElement) e; + if (methodElement.getSimpleName().contentEquals(methodName)) { + return methodElement; + } + } + } + + throw new AssertionError(injectorElement + " does not contain " + methodName); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("annotation", annotation) + .append("injectorElement", injectorElement) + .append("injectorImplElement", injectorImplElement) + .toString(); + } +} diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ParserCodeGenerator.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ParserCodeGenerator.java index 209617a4..fd475730 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ParserCodeGenerator.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/ParserCodeGenerator.java @@ -15,15 +15,14 @@ import com.riiablo.table.ParserInput; import static com.riiablo.table.annotation.Constants.STRING; -class ParserCodeGenerator extends CodeGenerator { +final class ParserCodeGenerator extends CodeGenerator { ParserCodeGenerator(Context context, String parserPackage) { super(context, parserPackage); } @Override ClassName formatName(String packageName, SchemaElement schemaElement) { - return schemaElement.parserClassName - = ClassName.get( + return schemaElement.parserClassName = ClassName.get( packageName, schemaElement.element.getSimpleName() + Parser.class.getSimpleName()); } @@ -90,6 +89,7 @@ class ParserCodeGenerator extends CodeGenerator { final ParameterSpec recordId = method.parameters.get(1); final ParameterSpec record = method.parameters.get(2); for (FieldElement field : schemaElement.fields) { + if (field.isForeignKey()) continue; final TypeName fieldTypeName = TypeName.get(field.element()); final CodeBlock fqFieldName = qualify(record, field.name()); if (field.isArray()) { diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaElement.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaElement.java index 31c3169d..a027d75c 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaElement.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaElement.java @@ -29,7 +29,7 @@ final class SchemaElement { return null; } - ExecutableElement defaultConstructor = defaultConstructor(context, typeElement); + ExecutableElement defaultConstructor = defaultConstructor(context, typeElement); if (defaultConstructor == null) { context.error(typeElement, "{element} must contain a default constructor"); return null; @@ -68,6 +68,7 @@ final class SchemaElement { } TableElement tableElement = TableElement.get(context, typeElement); + InjectorElement injectorElement = InjectorElement.get(context, typeElement); SerializerElement serializerElement = SerializerElement.get(context, typeElement); ParserElement parserElement = ParserElement.get(context, typeElement); @@ -75,6 +76,7 @@ final class SchemaElement { annotation, typeElement, tableElement, + injectorElement, serializerElement, parserElement, primaryKeyFieldElement, @@ -122,15 +124,28 @@ final class SchemaElement { return fields; } + static Collection collectForeignKeys(Collection fields) { + return CollectionUtils.select(fields, new Predicate() { + @Override + public boolean evaluate(FieldElement field) { + return field.isForeignKey(); + } + }); + } + final Schema annotation; final TypeElement element; final TableElement tableElement; + final InjectorElement injectorElement; final SerializerElement serializerElement; final ParserElement parserElement; final FieldElement primaryKeyFieldElement; final Collection fields; final int numFields; + final Collection foreignKeys; + ClassName tableClassName; + ClassName injectorClassName; ClassName serializerClassName; ClassName parserClassName; @@ -138,6 +153,7 @@ final class SchemaElement { Schema annotation, TypeElement element, TableElement tableElement, + InjectorElement injectorElement, SerializerElement serializerElement, ParserElement parserElement, FieldElement primaryKeyFieldElement, @@ -145,11 +161,19 @@ final class SchemaElement { this.annotation = annotation; this.element = element; this.tableElement = tableElement; + this.injectorElement = injectorElement; this.serializerElement = serializerElement; this.parserElement = parserElement; this.primaryKeyFieldElement = primaryKeyFieldElement; this.fields = fields; this.numFields = countNumFields(fields); + this.foreignKeys = collectForeignKeys(fields); + if (tableElement.tableImplElement != null) { + tableClassName = ClassName.get(tableElement.tableImplElement); + } + if (injectorElement.injectorImplElement != null) { + injectorClassName = ClassName.get(injectorElement.injectorImplElement); + } if (serializerElement.serializerImplElement != null) { serializerClassName = ClassName.get(serializerElement.serializerImplElement); } @@ -169,10 +193,12 @@ final class SchemaElement { return new ToStringBuilder(this) .append("element", element) .append("tableElement", tableElement) + .append("injectorElement", injectorElement) + .append("injectorClassName", injectorClassName) .append("serializerElement", serializerElement) .append("serializerClassName", serializerClassName) - .append("ParserElement", parserElement) - .append("serializerClassName", serializerClassName) + .append("parserElement", parserElement) + .append("parserClassName", parserClassName) .append("primaryKeyFieldElement", primaryKeyFieldElement) .toString(); } diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaProcessor.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaProcessor.java index 68d2a932..279f8605 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaProcessor.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SchemaProcessor.java @@ -1,67 +1,142 @@ package com.riiablo.table.annotation; import com.google.auto.service.AutoService; -import com.squareup.javapoet.ArrayTypeName; -import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeSpec; +import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import org.apache.commons.collections4.SetUtils; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.exception.ExceptionUtils; +import static com.riiablo.table.annotation.Constants.FOREIGN_KEY; +import static com.riiablo.table.annotation.Constants.PRIMARY_KEY; +import static com.riiablo.table.annotation.Constants.PRIMARY_KEY_TYPES; + @AutoService(Processor.class) public class SchemaProcessor extends AbstractProcessor { - private final Set schemas = new HashSet<>(); + private final List schemas = new ArrayList<>(); + private final Map tables = new HashMap<>(); + private final Set tableTypes = new HashSet<>(); private Context context; @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - super.init(processingEnv); - context = new Context(processingEnv); + public synchronized void init(ProcessingEnvironment p) { + super.init(p); + Validate.validState(context == null, "context already configured"); + context = new Context(p); } @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - if (roundEnv.processingOver()) { - generateManifest(); + public boolean process(Set annotations, RoundEnvironment r) { + if (r.processingOver()) { + ClassName tableManifest = generateManifest(); + if (tableManifest != null) { + generateInjectors(tableManifest); + } } else { - processAnnotations(roundEnv); + processPrimaryKeyAnnotations(r); + processSchemaAnnotations(r); + processForeignKeyAnnotations(r); } return true; } - private void processAnnotations(RoundEnvironment roundEnv) { + private void processPrimaryKeyAnnotations(RoundEnvironment r) { + for (Element element : r.getElementsAnnotatedWith(PrimaryKey.class)) { + AnnotationMirror annotationMirror = context.getAnnotationMirror(element, PRIMARY_KEY); + if (element.getKind() != ElementKind.FIELD) { + context.error(element, annotationMirror, + "{} can only be applied to fields", + PrimaryKey.class); + } - for (Element element : roundEnv.getElementsAnnotatedWith(PrimaryKey.class)) { - VariableElement variableElement = (VariableElement) element; - if (!Constants.isPrimaryKey(variableElement)) { - context.error(variableElement, "{} must be one of {}", - PrimaryKey.class, Constants.PRIMARY_KEY_TYPES); + if (!Constants.isPrimaryKeyType(element)) { + context.error(element, annotationMirror, + "{} must be one of {}", + PrimaryKey.class, PRIMARY_KEY_TYPES); } } + } + private void processForeignKeyAnnotations(RoundEnvironment r) { + for (Element element : r.getElementsAnnotatedWith(ForeignKey.class)) { + AnnotationMirror annotationMirror = context.getAnnotationMirror(element, FOREIGN_KEY); + if (element.getKind() != ElementKind.FIELD) { + context.error(element, annotationMirror, + "{} can only be applied to fields", + ForeignKey.class); + } + + // validates that foreign key field type matches an existing table type + TypeMirror mirror = element.asType(); + if (!tableTypes.contains(mirror)) { + context.error(element, annotationMirror, + "cannot locate table of type {} for {element}", + mirror); + } + + // finds schema element of this foreign key element + SchemaElement schemaElement = null; + ForeignKeyElement foreignKey = null; +finder: + for (SchemaElement e : schemas) { + for (FieldElement f : e.fields) { + if (f.element == element) { + schemaElement = e; + foreignKey = f.foreignKeyElement; + break finder; + } + } + } + + // validates that foreign key column name matches an existing column name + if (schemaElement != null) { + boolean found = false; + for (FieldElement f : schemaElement.fields) { + if (f.name().contentEquals(foreignKey.annotation.value())) { + found = true; + break; + } + } + + if (!found) { + context.error(element, foreignKey.mirror, foreignKey.value("value"), + "{} does not contain any field named '{}'", + schemaElement.element.getQualifiedName(), foreignKey.annotation.value()); + } + } + } + } + + private void processSchemaAnnotations(RoundEnvironment r) { TableCodeGenerator tableCodeGenerator = new TableCodeGenerator( context, "com.riiablo.table.table"); SerializerCodeGenerator serializerCodeGenerator = new SerializerCodeGenerator( context, "com.riiablo.table.serializer"); ParserCodeGenerator parserCodeGenerator = new ParserCodeGenerator( context, "com.riiablo.table.parser"); - for (Element element : roundEnv.getElementsAnnotatedWith(Schema.class)) { + for (Element element : r.getElementsAnnotatedWith(Schema.class)) { if (element.getKind() != ElementKind.CLASS) { context.error(element, "{} can only be applied to classes", Schema.class); continue; @@ -71,7 +146,8 @@ public class SchemaProcessor extends AbstractProcessor { if (schemaElement == null) continue; if (schemaElement.serializerElement.declaredType != null) { try { - serializerCodeGenerator.generate(schemaElement) + serializerCodeGenerator + .generate(schemaElement) .writeTo(processingEnv.getFiler()); } catch (Throwable t) { context.error(ExceptionUtils.getRootCauseMessage(t)); @@ -81,7 +157,8 @@ public class SchemaProcessor extends AbstractProcessor { if (schemaElement.parserElement.declaredType != null) { try { - parserCodeGenerator.generate(schemaElement) + parserCodeGenerator + .generate(schemaElement) .writeTo(processingEnv.getFiler()); } catch (Throwable t) { context.error(ExceptionUtils.getRootCauseMessage(t)); @@ -93,7 +170,8 @@ public class SchemaProcessor extends AbstractProcessor { // Depends on parserElement to generate Parser impl if (schemaElement.tableElement.declaredType != null) { try { - tableCodeGenerator.generate(schemaElement) + tableCodeGenerator + .generate(schemaElement) .writeTo(processingEnv.getFiler()); } catch (Throwable t) { context.error(ExceptionUtils.getRootCauseMessage(t)); @@ -101,33 +179,62 @@ public class SchemaProcessor extends AbstractProcessor { } } - schemas.add(CodeBlock.of("$S", schemaElement.element).toString()); + schemas.add(schemaElement); + tableTypes.add(schemaElement.element.asType()); } } - private void generateManifest() { + private ClassName generateManifest() { try { - JavaFile.builder("com.riiablo.table", - TypeSpec - .classBuilder("TableManifest") - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addMethod(MethodSpec - .constructorBuilder() - .addModifiers(Modifier.PRIVATE) - .build()) - .addMethod(MethodSpec - .methodBuilder("names") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ArrayTypeName.of(String.class)) - .addStatement("return new String[] {\n$L\n}", StringUtils - .join(schemas, ",\n")) - .build()) + ClassName manifestName = ClassName.get("com.riiablo.table", "TableManifest"); + TypeSpec.Builder tableManifest = TypeSpec + .classBuilder(manifestName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethod(MethodSpec + .constructorBuilder() + .addModifiers(Modifier.PRIVATE) .build()) - .build() + ; + + for (SchemaElement schema : schemas) { + ClassName schemaName = ClassName.get(schema.element); + FieldSpec tableFieldSpec = FieldSpec + .builder( + schema.tableClassName, + schemaName.simpleName().toLowerCase(), + Modifier.PUBLIC, Modifier.FINAL) + .initializer("new $T()", schema.tableClassName) + .build(); + tableManifest.addField(tableFieldSpec); + tables.put(schemaName, tableFieldSpec); + } + + JavaFile + .builder(manifestName.packageName(), tableManifest.build()).build() .writeTo(processingEnv.getFiler()); + return manifestName; } catch (Throwable t) { - context.error(ExceptionUtils.getRootCauseMessage(t)); - t.printStackTrace(System.err); + context.error(ExceptionUtils.getRootCauseMessage(t)); + t.printStackTrace(System.err); + return null; + } + } + + private void generateInjectors(ClassName tableManifest) { + InjectorCodeGenerator injectorCodeGenerator = new InjectorCodeGenerator( + context, "com.riiablo.table.injector", tableManifest, tables); + for (SchemaElement schemaElement : schemas) { + if (schemaElement.foreignKeys.isEmpty()) continue; + if (schemaElement.parserElement.declaredType != null) { + try { + injectorCodeGenerator + .generate(schemaElement) + .writeTo(processingEnv.getFiler()); + } catch (Throwable t) { + context.error(ExceptionUtils.getRootCauseMessage(t)); + t.printStackTrace(System.err); + } + } } } @@ -136,6 +243,7 @@ public class SchemaProcessor extends AbstractProcessor { Set set = new LinkedHashSet<>(); set.add(Schema.class.getCanonicalName()); set.add(PrimaryKey.class.getCanonicalName()); + set.add(ForeignKey.class.getCanonicalName()); return SetUtils.unmodifiableSet(set); } diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SerializerCodeGenerator.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SerializerCodeGenerator.java index e99c7f0f..d32861db 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SerializerCodeGenerator.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/SerializerCodeGenerator.java @@ -58,6 +58,7 @@ class SerializerCodeGenerator extends CodeGenerator { final ParameterSpec in = method.parameters.get(1); for (FieldElement field : schemaElement.fields) { if (field.isTransient()) continue; + if (field.isForeignKey()) continue; final TypeName fieldTypeName = TypeName.get(field.element()); final CodeBlock fqFieldName = qualify(record, field.name()); if (field.isArray()) { @@ -97,6 +98,7 @@ class SerializerCodeGenerator extends CodeGenerator { final ParameterSpec out = method.parameters.get(1); for (FieldElement field : schemaElement.fields) { if (field.isTransient()) continue; + if (field.isForeignKey()) continue; final TypeName fieldTypeName = TypeName.get(field.element()); final CodeBlock fqFieldName = qualify(record, field.name()); if (field.isArray()) { @@ -135,6 +137,7 @@ class SerializerCodeGenerator extends CodeGenerator { final ParameterSpec e2 = method.parameters.get(1); for (FieldElement field : schemaElement.fields) { if (field.isTransient()) continue; + if (field.isForeignKey()) continue; final Name fieldName = field.name(); final CodeBlock e1FqFieldName = qualify(e1, fieldName); final CodeBlock e2FqFieldName = qualify(e2, fieldName); @@ -180,6 +183,7 @@ class SerializerCodeGenerator extends CodeGenerator { for (FieldElement field : schemaElement.fields) { if (field.isTransient()) continue; + if (field.isForeignKey()) continue; final Name fieldName = field.name(); final CodeBlock e1FqFieldName = qualify(e1, fieldName); final CodeBlock e2FqFieldName = qualify(e2, fieldName); @@ -231,7 +235,6 @@ class SerializerCodeGenerator extends CodeGenerator { } static CodeBlock defaultString(Object var) { - // return CodeBlock.of("$T.$N($L)", StringUtils.class, "defaultString", var); return CodeBlock.of("$1L == null ? $2S : $1L", var, ""); } diff --git a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/TableCodeGenerator.java b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/TableCodeGenerator.java index 84bab8e4..fb0d2908 100644 --- a/table/annotation-processor/src/main/java/com/riiablo/table/annotation/TableCodeGenerator.java +++ b/table/annotation-processor/src/main/java/com/riiablo/table/annotation/TableCodeGenerator.java @@ -14,9 +14,9 @@ class TableCodeGenerator extends CodeGenerator { @Override ClassName formatName(String packageName, SchemaElement schemaElement) { - return ClassName.get( - packageName, - schemaElement.element.getSimpleName() + Table.class.getSimpleName()); + return schemaElement.tableClassName = ClassName.get( + packageName, + schemaElement.element.getSimpleName() + Table.class.getSimpleName()); } @Override diff --git a/table/annotations/src/main/java/com/riiablo/table/annotation/ForeignKey.java b/table/annotations/src/main/java/com/riiablo/table/annotation/ForeignKey.java new file mode 100644 index 00000000..8a443cb3 --- /dev/null +++ b/table/annotations/src/main/java/com/riiablo/table/annotation/ForeignKey.java @@ -0,0 +1,19 @@ +package com.riiablo.table.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the field is a reference to a record in another + * {@link Schema schema} of field's type using the field in this schema + * specified by {@link #value() value} as the foreign key. + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface ForeignKey { + String value(); +} diff --git a/table/annotations/src/main/java/com/riiablo/table/annotation/Injector.java b/table/annotations/src/main/java/com/riiablo/table/annotation/Injector.java new file mode 100644 index 00000000..7e8472c5 --- /dev/null +++ b/table/annotations/src/main/java/com/riiablo/table/annotation/Injector.java @@ -0,0 +1,23 @@ +package com.riiablo.table.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the specified {@link Schema schema} should use the given + * {@link #value() injector} in lieu of generating one. The injector + * implementation should have the schema set as its generic parameter. + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface Injector { + /** + * A injector implementation that should be used by this + * {@link Schema schema} in lieu of generating one. + */ + Class> value(); +} diff --git a/table/core/src/main/java/com/riiablo/table/Injector.java b/table/core/src/main/java/com/riiablo/table/Injector.java new file mode 100644 index 00000000..db9b6358 --- /dev/null +++ b/table/core/src/main/java/com/riiablo/table/Injector.java @@ -0,0 +1,12 @@ +package com.riiablo.table; + +/** + * Defines behaviors necessary to inject a record with its required + * dependencies. + * + * @param record type + * @param manifest + */ +public interface Injector { + R inject(M manifest, R record); +} diff --git a/table/core/src/main/java/com/riiablo/table/Table.java b/table/core/src/main/java/com/riiablo/table/Table.java index 38035da6..bc86e8d7 100644 --- a/table/core/src/main/java/com/riiablo/table/Table.java +++ b/table/core/src/main/java/com/riiablo/table/Table.java @@ -20,6 +20,7 @@ public abstract class Table implements Iterable { protected IntMap records; protected Array ordered; + protected Injector injector; protected Parser parser; protected Table(Class recordClass) { @@ -41,6 +42,10 @@ public abstract class Table implements Iterable { protected abstract Parser newParser(ParserInput parser); protected abstract Serializer newSerializer(); + protected Injector newInjector() { + return null; + } + public Class recordClass() { return recordClass; } @@ -77,6 +82,12 @@ public abstract class Table implements Iterable { return null; } + protected R inject(R record) { + if (injector == null) injector = newInjector(); + if (injector != null) return injector.inject(null, record); + return record; + } + protected Parser parser() { return parser; } @@ -94,7 +105,9 @@ public abstract class Table implements Iterable { public R get(int id) { R record = records.get(id); if (record == null && parser != null) { - records.put(id, record = parser.parseRecord(id, newRecord())); + record = parser.parseRecord(id, newRecord()); + record = inject(record); + records.put(id, record); } return record; diff --git a/table/integration/src/main/java/com/riiablo/table/schema/MonStats.java b/table/integration/src/main/java/com/riiablo/table/schema/MonStats.java index 309f29db..2e5791ce 100644 --- a/table/integration/src/main/java/com/riiablo/table/schema/MonStats.java +++ b/table/integration/src/main/java/com/riiablo/table/schema/MonStats.java @@ -1,5 +1,6 @@ package com.riiablo.table.schema; +import com.riiablo.table.annotation.ForeignKey; import com.riiablo.table.annotation.Format; import com.riiablo.table.annotation.PrimaryKey; import com.riiablo.table.annotation.Schema; @@ -12,6 +13,9 @@ public class MonStats { return NameStr; } + @ForeignKey("MonStatsEx") + public MonStats2 monstats2; + @PrimaryKey public String Id; public int hcIdx; diff --git a/table/integration/src/main/java/com/riiablo/table/schema/MonStats2.java b/table/integration/src/main/java/com/riiablo/table/schema/MonStats2.java index 10a23428..30b749e4 100644 --- a/table/integration/src/main/java/com/riiablo/table/schema/MonStats2.java +++ b/table/integration/src/main/java/com/riiablo/table/schema/MonStats2.java @@ -1,89 +1,125 @@ package com.riiablo.table.schema; +import com.riiablo.table.annotation.Format; +import com.riiablo.table.annotation.PrimaryKey; +import com.riiablo.table.annotation.Schema; + +@Schema public class MonStats2 { - // @Override - // public String toString() { - // return Id; - // } - // - // @Key - // @Column public String Id; - // @Column public int Height; - // @Column public int OverlayHeight; - // @Column public int pixHeight; - // @Column public int SizeX; - // @Column public int SizeY; - // @Column public int spawnCol; - // @Column public int MeleeRng; - // @Column public String BaseW; - // @Column public int HitClass; - // @Column(format = "%sv", endIndex = 16, values = { - // "HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8" - // }) - // public String ComponentV[]; - // @Column(endIndex = 16, values = { - // "HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8" - // }) - // public boolean Components[]; - // @Column public int TotalPieces; - // @Column(format = "m%s", endIndex = 16, values = { - // "DT", "NU", "WL", "GH", "A1", "A2", "BL", "SC", "S1", "S2", "S3", "S4", "DD", "KB", "SQ", "RN" - // }) - // public boolean mMode[]; - // @Column(format = "d%s", endIndex = 16, values = { - // "DT", "NU", "WL", "GH", "A1", "A2", "BL", "SC", "S1", "S2", "S3", "S4", "DD", "KB", "SQ", "RN" - // }) - // public int dMode[]; - // @Column(format = "%smv", endIndex = 16, values = { - // "DT", "NU", "WL", "GH", "A1", "A2", "BL", "SC", "S1", "S2", "S3", "S4", "DD", "KB", "SQ", "RN" - // }) - // public boolean Modemv[]; - // //@Column public int A1mv; - // //@Column public int A2mv; - // //@Column public int SCmv; - // //@Column public int S1mv; - // //@Column public int S2mv; - // //@Column public int S3mv; - // //@Column public int S4mv; - // @Column public boolean noGfxHitTest; - // @Column public int htTop; - // @Column public int htLeft; - // @Column public int htWidth; - // @Column public int htHeight; - // @Column public int restore; - // @Column public int automapCel; - // @Column public boolean noMap; - // @Column public boolean noOvly; - // @Column public boolean isSel; - // @Column public boolean alSel; - // @Column public boolean noSel; - // @Column public boolean shiftSel; - // @Column public boolean corpseSel; - // @Column public boolean isAtt; - // @Column public boolean revive; - // @Column public boolean critter; - // @Column public boolean small; - // @Column public boolean large; - // @Column public boolean soft; - // @Column public boolean inert; - // @Column public boolean objCol; - // @Column public boolean deadCol; - // @Column public boolean unflatDead; - // @Column public boolean Shadow; - // @Column public boolean noUniqueShift; - // @Column public boolean compositeDeath; - // @Column public int localBlood; - // @Column public int Bleed; - // @Column public int Light; - // @Column(format = "light-%s", values = {"r", "g", "b"}, endIndex = 3) - // public int light[]; - // @Column(format = "Utrans%s", values = {"", "(N)", "(H)"}, endIndex = 3) - // public int Utrans[]; - // @Column public String Heart; - // @Column public String BodyPart; - // @Column public int InfernoLen; - // @Column public int InfernoAnim; - // @Column public int InfernoRollback; - // @Column public String ResurrectMode; - // @Column public String ResurrectSkill; + @Override + public String toString() { + return Id; + } + + @PrimaryKey + public String Id; + + public int Height; + public int OverlayHeight; + public int pixHeight; + public int SizeX; + public int SizeY; + public int spawnCol; + public int MeleeRng; + public String BaseW; + public int HitClass; + + @Format( + format = "%sv", + endIndex = 16, + values = { + "HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8" + }) + public String ComponentV[]; + + @Format( + endIndex = 16, + values = { + "HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8" + }) + public boolean Components[]; + + public int TotalPieces; + + @Format( + format = "m%s", + endIndex = 16, + values = { + "DT", "NU", "WL", "GH", "A1", "A2", "BL", "SC", "S1", "S2", "S3", "S4", "DD", "KB", "SQ", "RN" + }) + public boolean mMode[]; + + @Format( + format = "d%s", + endIndex = 16, + values = { + "DT", "NU", "WL", "GH", "A1", "A2", "BL", "SC", "S1", "S2", "S3", "S4", "DD", "KB", "SQ", "RN" + }) + public int dMode[]; + + @Format( + format = "%smv", + endIndex = 16, + values = { + "DT", "NU", "WL", "GH", "A1", "A2", "BL", "SC", "S1", "S2", "S3", "S4", "DD", "KB", "SQ", "RN" + }) + public boolean Modemv[]; + + //public int A1mv; + //public int A2mv; + //public int SCmv; + //public int S1mv; + //public int S2mv; + //public int S3mv; + //public int S4mv; + public boolean noGfxHitTest; + public int htTop; + public int htLeft; + public int htWidth; + public int htHeight; + public int restore; + public int automapCel; + public boolean noMap; + public boolean noOvly; + public boolean isSel; + public boolean alSel; + public boolean noSel; + public boolean shiftSel; + public boolean corpseSel; + public boolean isAtt; + public boolean revive; + public boolean critter; + public boolean small; + public boolean large; + public boolean soft; + public boolean inert; + public boolean objCol; + public boolean deadCol; + public boolean unflatDead; + public boolean Shadow; + public boolean noUniqueShift; + public boolean compositeDeath; + public int localBlood; + public int Bleed; + public int Light; + + @Format( + format = "light-%s", + values = {"r", "g", "b"}, + endIndex = 3) + public int light[]; + + @Format( + format = "Utrans%s", + values = {"", "(N)", "(H)"}, + endIndex = 3) + public int Utrans[]; + + public String Heart; + public String BodyPart; + public int InfernoLen; + public int InfernoAnim; + public int InfernoRollback; + public String ResurrectMode; + public String ResurrectSkill; } diff --git a/table/integration/src/main/java/com/riiablo/table/schema/MonStatsInjectorImpl.java b/table/integration/src/main/java/com/riiablo/table/schema/MonStatsInjectorImpl.java new file mode 100644 index 00000000..cd7577b7 --- /dev/null +++ b/table/integration/src/main/java/com/riiablo/table/schema/MonStatsInjectorImpl.java @@ -0,0 +1,10 @@ +package com.riiablo.table.schema; + +import com.riiablo.table.Injector; + +public class MonStatsInjectorImpl implements Injector { + @Override + public MonStats inject(Object manifest, MonStats record) { + throw new UnsupportedOperationException(); + } +}