package io.trygvis.container.compiler; import io.trygvis.container.compiler.model.ClassG; import io.trygvis.container.compiler.model.Constructor; import io.trygvis.container.compiler.model.FieldRef; import io.trygvis.container.compiler.model.MethodRef; import io.trygvis.container.compiler.model.Parameters; import io.trygvis.container.compiler.model.TypeRef; import io.trygvis.persistence.EntityMirror; import io.trygvis.persistence.FieldMirror; import io.trygvis.persistence.GeneratorConfiguration; import io.trygvis.persistence.SequenceMirror; import io.trygvis.persistence.SqlEntity; import io.trygvis.persistence.SqlEntitySet; import io.trygvis.persistence.TypeHandler; import io.trygvis.persistence.sql.SqlDao; import io.trygvis.persistence.sql.SqlEntityMeta; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.persistence.Id; import javax.persistence.SequenceGenerator; import java.io.IOException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import static io.trygvis.container.compiler.Utils.*; import static io.trygvis.container.compiler.model.Parameters.ParameterRef; import static io.trygvis.container.compiler.model.TypeRef.VOID; import static io.trygvis.persistence.FieldMirror.AccessorType.FIELD; import static io.trygvis.persistence.FieldMirror.AccessorType.METHOD; import static io.trygvis.persistence.FieldMirror.FieldType.PRIMITIVE; import static io.trygvis.persistence.FieldMirror.FieldType.REFERENCE; import static io.trygvis.persistence.TypeHandler.StringEnumTypeHandler; import static java.lang.Character.isUpperCase; import static java.lang.String.format; import static java.lang.reflect.Modifier.PUBLIC; import static javax.lang.model.util.ElementFilter.fieldsIn; import static javax.lang.model.util.ElementFilter.methodsIn; import static org.apache.commons.lang.StringUtils.join; public class EntityHandler extends AbstractHandler { private GeneratorConfiguration generatorConfiguration = new GeneratorConfiguration(); private SqlUnitModel sqlUnit; public EntityHandler(ProcessingEnvironment processingEnv, SqlUnitModel sqlUnit) { super(processingEnv); this.sqlUnit = sqlUnit; } public void phase1(Set sqlEntities, Set packages) throws Exception { for (TypeElement entity : sqlEntities) { AnnotationMirror sqlEntity = getAnnotation(SqlEntity.class, entity); for (Map.Entry v : sqlEntity.getElementValues().entrySet()) { String field = v.getKey().getSimpleName().toString(); switch (field) { case "value": Class typeHandlerClass = getClass().getClassLoader().loadClass(v.getValue().getValue().toString()); TypeHandler typeHandler = (TypeHandler) typeHandlerClass.newInstance(); String type = entity.asType().toString(); generatorConfiguration.addTypeHandler(new TypeRef(type), typeHandler); System.out.println("Loaded TypeHandler for " + type + " through " + typeHandlerClass.getCanonicalName()); break; default: throw new InternalErrorException("Unknown @SqlEntity field: " + field); } } } if (packages.size() == 0) { throw new CompilerException("There has to be exactly one @SqlEntitySet annotated package."); } PackageElement packageElement = packages.iterator().next(); if (packages.size() != 1) { throw new CompilerException(packageElement, "There can only be one @SqlEntitySet annotated package."); } AnnotationMirror sqlEntitySet = getAnnotation(SqlEntitySet.class, packageElement); String name = null; for (Map.Entry v : sqlEntitySet.getElementValues().entrySet()) { String field = v.getKey().getSimpleName().toString(); String value = v.getValue().getValue().toString(); switch (field) { case "name": name = value; } } sqlUnit.setName(name); sqlUnit.setPackageName(packageElement.getQualifiedName().toString()); } public void recordEntity(TypeElement element) throws Exception { EntityMirror entity = new EntityMirror( generatorConfiguration, new TypeRef(types.getDeclaredType(element)), sqlName(element.getSimpleName().toString())); Map getters = new TreeMap<>(); Map setters = new TreeMap<>(); for (ExecutableElement m : methodsIn(elements.getAllMembers(element))) { String name = m.getSimpleName().toString(); if (name.length() < 4 || !isUpperCase(name.charAt(3))) { continue; } String declaringType = ((TypeElement) m.getEnclosingElement()).getQualifiedName().toString(); if (declaringType.equals("java.lang.Object")) { continue; } boolean isVoid = m.getReturnType().getKind().equals(TypeKind.VOID); if (name.startsWith("get") && m.getParameters().size() == 0 && !isVoid) { getters.put(toFieldName(name), m); } if (name.startsWith("set") && m.getParameters().size() == 1 && isVoid) { setters.put(toFieldName(name), m); } } for (VariableElement f : fieldsIn(elements.getAllMembers(element))) { String name = f.getSimpleName().toString(); ExecutableElement getter = getters.remove(name); ExecutableElement setter = setters.remove(name); FieldMirror field = fromElement(generatorConfiguration, f, getter, setter); if (field == null) { continue; } entity.add(field); System.out.println("Field: " + field); } for (Map.Entry e : getters.entrySet()) { String name = e.getKey(); ExecutableElement getter = e.getValue(); ExecutableElement setter = setters.remove(name); FieldMirror field = fromElement(generatorConfiguration, null, getter, setter); if (field == null) { continue; } entity.add(field); System.out.println("Field: " + field); } Iterator it = setters.values().iterator(); while (it.hasNext()) { ExecutableElement setter = it.next(); if (setter.getModifiers().contains(Modifier.STATIC)) { it.remove(); } } if (!setters.isEmpty()) { throw new CompilerException(element, "Missing getters for these setters: " + join(setters.keySet(), ", ")); } // ----------------------------------------------------------------------- // Process any extra annotations // ----------------------------------------------------------------------- processSequenceGenerators(element); // ----------------------------------------------------------------------- // Validation // ----------------------------------------------------------------------- List idFields = new ArrayList<>(); for (FieldMirror field : entity.fields) { if (field.id) { idFields.add(field); } } if (idFields.size() == 0) { throw new CompilerException(element, "An @Entity is required to have at least one @Id field."); } if (idFields.size() != 1) { throw new CompilerException(element, "This implementation only support a single @Id annotated field."); } entity.setIdType(idFields.get(0).type); sqlUnit.add(entity, element); } private void processSequenceGenerators(Element element) { AnnotationMirror sequenceGenerator = findAnnotation(SequenceGenerator.class, element); if (sequenceGenerator != null) { String name = null; String sequenceName = null; int initialValue = 0; int allocationSize = 0; for (Map.Entry v : sequenceGenerator.getElementValues().entrySet()) { String field = v.getKey().getSimpleName().toString(); String value = v.getValue().getValue().toString(); switch (field) { case "name": name = value; break; case "sequenceName": sequenceName = value; break; case "initialValue": initialValue = Integer.valueOf(value); break; case "allocationSize": allocationSize = Integer.valueOf(value); break; default: throw new InternalErrorException("Unsupported field on @SequenceGenerator: " + field); } } if (name != null) { sequenceName = sequenceName == null ? sqlName(name) : sequenceName; sqlUnit.add(new SequenceMirror(name, sequenceName, initialValue, allocationSize), element); } } } public ClassG phase3(EntityMirror entityMirror) throws IOException { ClassG g = new ClassG(PUBLIC, entityMirror.daoType). extendsType(new TypeRef(SqlDao.class).args(entityMirror.idType, entityMirror.type)); Parameters p = new Parameters(); ParameterRef c = p.addParameter(new TypeRef(Connection.class), "c"); g.add(new Constructor(p, "super(" + c.name + ");")); TypeRef stringType = g.imports.add(String.class); TypeRef sqlEntityDescType = g.imports.add(SqlEntityMeta.class); TypeRef sqlException = g.imports.add(SQLException.class); FieldRef createTableSql = g.addPublicStaticFinalField(stringType, "createTableSql"). value(toJavaString(entityMirror.createTableSql(sqlUnit))); g.add(new MethodRef(PUBLIC, stringType, "createTableSql", "return createTableSql;")); FieldRef dropTableSql = g.addPublicStaticFinalField(stringType, "dropTableSql"). value(toJavaString(entityMirror.dropTableSql())); g.add(new MethodRef(PUBLIC, stringType, "dropTableSql", "return dropTableSql;")); g.addPublicStaticFinalField(stringType, "insertIntoSql"). value(toJavaString(entityMirror.insertIntoSql(sqlUnit))); g.addPublicStaticFinalField(stringType, "deleteFromSql"). value(toJavaString(entityMirror.deleteFromSql())); String desc = "new " + sqlEntityDescType + "(" + toJavaString(entityMirror.tableName) + ", " + toJavaString(entityMirror.defaultFields()) + ", " + toJavaString(createTableSql.name) + ", " + toJavaString(dropTableSql.name) + ")"; g.addPublicStaticFinalField(sqlEntityDescType, "desc").value(desc); ClassG.InnerClassG typedQuery = g.addInnerClass(entityMirror.queryType(g.imports)); g.addInnerClass(entityMirror.utils(sqlUnit)); { p = new Parameters(); ParameterRef rs = p.addParameter(new TypeRef(ResultSet.class), "rs"); g.add(new MethodRef(PUBLIC, entityMirror.type, "fromResultSet", p, "return Utils.fromResultSet" + entityMirror.type.className + "(" + rs.name + ");"). exception(sqlException)); } { p = new Parameters(); ParameterRef entity = p.addParameter(entityMirror.type, "entity"); g.add(new MethodRef(PUBLIC, VOID, "insert", p, "Utils.insert" + entityMirror.type.className + "(super.c, " + entity.name + ");"). exception(sqlException)); } { p = new Parameters(); ParameterRef id = p.addParameter(entityMirror.idType, "id"); g.add(new MethodRef(PUBLIC, entityMirror.type, "selectById", p, "return Utils.select" + entityMirror.type.className + "ById(super.c, " + id.name + ");"). exception(sqlException)); } { p = new Parameters(); ParameterRef entity = p.addParameter(entityMirror.type, "entity"); g.add(new MethodRef(PUBLIC, VOID, "delete", p, "Utils.delete" + entityMirror.type.className + "(super.c, " + entity.name + ");"). exception(sqlException)); } { p = new Parameters(); ParameterRef id = p.addParameter(entityMirror.idType, "id"); g.add(new MethodRef(PUBLIC, VOID, "deleteById", p, "Utils.delete" + entityMirror.type.className + "ById(super.c, " + id.name + ");"). exception(sqlException)); } { p = new Parameters(); ParameterRef entity = p.addParameter(entityMirror.type, "entity"); g.add(new MethodRef(PUBLIC, VOID, "update", p, "Utils.update" + entityMirror.type.className + "(super.c, " + entity.name + ");"). exception(sqlException)); } return g; } public FieldMirror fromElement(GeneratorConfiguration generatorConfiguration, VariableElement var, ExecutableElement getter, ExecutableElement setter) { // TODO: check the setter for annotations too // TODO: check for transient and @Transient FieldMirror.AccessorType accessorType; TypeRef type; Element element; String javaName; String sqlName; boolean id; if (var != null) { if (var.getModifiers().contains(Modifier.STATIC)) { return null; } accessorType = FIELD; type = new TypeRef(var.asType()); element = types.asElement(var.asType()); javaName = var.getSimpleName().toString(); id = isId(var); processSequenceGenerators(var); } else { if (getter.getModifiers().contains(Modifier.STATIC)) { return null; } if (setter == null) { // Skipping fields is closer to what hibernate does. return null; // throw new CompilerException(getter, "Missing setter for getter: " + getter.getSimpleName()); } accessorType = METHOD; type = new TypeRef(getter.getReturnType()); element = types.asElement(getter.getReturnType()); id = isId(getter); processSequenceGenerators(getter); // TODO: this might be relaxed, just find the common type and use that. if (!types.isSameType(getter.getReturnType(), setter.getParameters().get(0).asType())) { throw new CompilerException(format("The setter and getter %s/%s must access the same types.", setter.getSimpleName(), getter.getSimpleName())); } javaName = toFieldName(getter.getSimpleName().toString()); } sqlName = sqlName(javaName); // Register type handler if this is an enum if (element != null && element.getKind() == ElementKind.ENUM) { if (!generatorConfiguration.hasTypeHandler(type)) { generatorConfiguration.addTypeHandler(type, new StringEnumTypeHandler(type)); } } boolean notNull = false; boolean unique = false; boolean primitive = generatorConfiguration.hasTypeHandler(type); if (id && !primitive) { throw new CompilerException(var, "A @Id field has to be a primitive or embedded."); } FieldMirror field; if (primitive) { System.out.println("type = " + type); TypeHandler typeHandler = generatorConfiguration.typeHandler(type); System.out.println("typeHandler = " + typeHandler); System.out.println("typeHandler.typeName() = " + typeHandler.typeName()); // TODO: check for configuration conflict notNull = !typeHandler.nullable; field = new FieldMirror(PRIMITIVE, accessorType, type, javaName, sqlName, id, notNull, unique); } else if (generatorConfiguration.hasTypeHandler(type)) { throw new CompilerException(var, "Missing type handler for type: " + type.fqName); } else { field = new FieldMirror(REFERENCE, accessorType, type, javaName, sqlName, id, notNull, unique); } return field; } public static boolean isId(Element var) { return var.getAnnotation(Id.class) != null; } public void phase3(boolean errorRaised) throws Exception { try { for (EntityMirror entity : sqlUnit.getEntities().values()) { writeFile(processingEnv, phase3(entity), sqlUnit.element(entity)); } } catch (CompilerException | InternalErrorException e) { // Ignore any exceptions if we had an error from before if (errorRaised) { return; } throw e; } } public static String sqlName(String javaName) { StringBuilder builder = new StringBuilder(); for (char c : javaName.toCharArray()) { char lower = Character.toLowerCase(c); if (isUpperCase(c) && builder.length() > 0) { builder.append("_"); } builder.append(lower); } return builder.toString(); } private AnnotationMirror findAnnotation(Class c, Element type) { TypeMirror annotationType = elements.getTypeElement(c.getCanonicalName()).asType(); for (AnnotationMirror a : type.getAnnotationMirrors()) { if (types.isSameType(a.getAnnotationType(), annotationType)) { return a; } } return null; } public AnnotationMirror getAnnotation(Class c, Element type) { AnnotationMirror annotation = findAnnotation(c, type); if (annotation != null) { return annotation; } throw new CompilerException(type, "Could not find annotation " + c.getSimpleName()); } }