Added support for foreign keys within schemas

This commit is contained in:
Collin Smith 2020-12-19 14:20:24 -08:00
parent 4e9f356b12
commit e0b4904efe
18 changed files with 620 additions and 143 deletions

View File

@ -23,6 +23,20 @@ abstract class AnnotationElement<A extends Annotation> {
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)

View File

@ -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));
}
}

View File

@ -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<Modifier> 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<FieldElement> 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);
}

View File

@ -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<ForeignKey> {
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);
}
}

View File

@ -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<ClassName, FieldSpec> tables;
InjectorCodeGenerator(
Context context,
String injectorPackage,
ClassName tableManifest,
Map<ClassName, FieldSpec> 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();
}
}

View File

@ -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();
}
}

View File

@ -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()) {

View File

@ -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<FieldElement> collectForeignKeys(Collection<FieldElement> fields) {
return CollectionUtils.select(fields, new Predicate<FieldElement>() {
@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<FieldElement> fields;
final int numFields;
final Collection<FieldElement> 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();
}

View File

@ -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<String> schemas = new HashSet<>();
private final List<SchemaElement> schemas = new ArrayList<>();
private final Map<ClassName, FieldSpec> tables = new HashMap<>();
private final Set<TypeMirror> 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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
generateManifest();
public boolean process(Set<? extends TypeElement> 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<String> set = new LinkedHashSet<>();
set.add(Schema.class.getCanonicalName());
set.add(PrimaryKey.class.getCanonicalName());
set.add(ForeignKey.class.getCanonicalName());
return SetUtils.unmodifiableSet(set);
}

View File

@ -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, "");
}

View File

@ -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

View File

@ -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();
}

View File

@ -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<? extends com.riiablo.table.Injector<?, ?>> value();
}

View File

@ -0,0 +1,12 @@
package com.riiablo.table;
/**
* Defines behaviors necessary to inject a record with its required
* dependencies.
*
* @param <R> record type
* @param <M> manifest
*/
public interface Injector<R, M> {
R inject(M manifest, R record);
}

View File

@ -20,6 +20,7 @@ public abstract class Table<R> implements Iterable<R> {
protected IntMap<R> records;
protected Array<R> ordered;
protected Injector<R, ?> injector;
protected Parser<R> parser;
protected Table(Class<R> recordClass) {
@ -41,6 +42,10 @@ public abstract class Table<R> implements Iterable<R> {
protected abstract Parser<R> newParser(ParserInput parser);
protected abstract Serializer<R> newSerializer();
protected Injector<R, ?> newInjector() {
return null;
}
public Class<R> recordClass() {
return recordClass;
}
@ -77,6 +82,12 @@ public abstract class Table<R> implements Iterable<R> {
return null;
}
protected R inject(R record) {
if (injector == null) injector = newInjector();
if (injector != null) return injector.inject(null, record);
return record;
}
protected Parser<R> parser() {
return parser;
}
@ -94,7 +105,9 @@ public abstract class Table<R> implements Iterable<R> {
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;

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,10 @@
package com.riiablo.table.schema;
import com.riiablo.table.Injector;
public class MonStatsInjectorImpl implements Injector<MonStats, Object> {
@Override
public MonStats inject(Object manifest, MonStats record) {
throw new UnsupportedOperationException();
}
}