package io.trygvis.container.compiler; import io.trygvis.container.compiler.model.ClassG; import io.trygvis.container.compiler.model.FieldRef; 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.TypeHandler; import io.trygvis.persistence.sql.SqlEntityDesc; 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.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 javax.tools.JavaFileObject; import java.io.IOException; import java.io.PrintWriter; 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.toFieldName; import static io.trygvis.container.compiler.Utils.toJavaString; 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 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.capitalize; import static org.apache.commons.lang.StringUtils.join; import static org.apache.commons.lang.StringUtils.stripEnd; public class EntityHandler extends AbstractHandler { private GeneratorConfiguration generatorConfiguration = new GeneratorConfiguration(); private SqlUnitModel sqlUnit = new SqlUnitModel(); private PackageElement packageElement; public EntityHandler(ProcessingEnvironment processingEnv) { super(processingEnv); } 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 = packages.iterator().next(); if (packages.size() != 1) { throw new CompilerException(packageElement, "There can only be one @SqlEntitySet annotated package."); } 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); } } // Iterate over all fields. Consume relevant getters and setters. 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."); } 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(); switch (field) { case "name": name = v.getValue().getValue().toString(); break; case "sequenceName": sequenceName = v.getValue().getValue().toString(); break; case "initialValue": initialValue = Integer.valueOf(v.getValue().getValue().toString()); break; case "allocationSize": allocationSize = Integer.valueOf(v.getValue().getValue().toString()); 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); TypeRef stringType = g.imports.add(String.class); TypeRef sqlEntityDescType = g.imports.add(SqlEntityDesc.class); g.addPublicStaticFinalField(stringType, "createTableSql"). value(toJavaString(entityMirror.createTableSql(sqlUnit))); g.addPublicStaticFinalField(stringType, "dropTableSql"). value(toJavaString(entityMirror.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()) + ")"; g.addPublicStaticFinalField(sqlEntityDescType, "desc").value(desc); ClassG.InnerClassG typedQuery = g.addInnerClass(entityMirror.queryType(g.imports)); typedQuery.inner.addMethod(entityMirror.fromResultSet(g.imports)); g.addMethod(entityMirror.insertInto(sqlUnit, g.imports)); g.addMethod(entityMirror.delete(g.imports)); g.addMethod(entityMirror.deleteById(g.imports)); g.addMethod(entityMirror.query(g.imports)); 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; String javaName; String sqlName; boolean id; if (var != null) { if (var.getModifiers().contains(Modifier.STATIC)) { return null; } accessorType = FIELD; type = new TypeRef(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()); 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); boolean notNull = false; boolean unique = false; boolean primitive = generatorConfiguration.isPrimitive(type); if (id && !primitive) { throw new CompilerException(var, "A @Id field has to be a primitive or embedded."); } FieldMirror field; if (primitive) { 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 { System.out.println("errorRaised = " + errorRaised); try { for (EntityMirror entity : sqlUnit.getEntities().values()) { writeFile(phase3(entity), sqlUnit.element(entity)); } writeFile(generateSequences(sqlUnit), null); writeFile(generateSession(), null); } catch (CompilerException | InternalErrorException e) { // Ignore any exceptions if we had an error from before if (errorRaised) { return; } throw e; } } private ClassG generateSequences(SqlUnitModel unit) { TypeRef sequences = new TypeRef(unit.getPackageName() + ".Sequences"); ClassG g = new ClassG(PUBLIC, sequences); List creates = new ArrayList<>(); List drops = new ArrayList<>(); for (SequenceMirror sequence : unit.getSequences().values()) { TypeRef stringType = g.imports.add(String.class); String value = "CREATE SEQUENCE " + sequence.sequenceName + ";"; FieldRef f = g.addPublicStaticFinalField(stringType, "create" + capitalize(sequence.name)). value(toJavaString(value)); creates.add(f.name); f = g.addPublicStaticFinalField(stringType, "drop" + capitalize(sequence.name)). value(toJavaString("DROP SEQUENCE " + sequence.sequenceName) + ";"); drops.add(f.name); } g.addPublicStaticFinalField(new TypeRef(String[].class), "createSequences"). value("new String[]{" + join(creates, ", ") + "}"); g.addPublicStaticFinalField(new TypeRef(String[].class), "dropSequences"). value("new String[]{" + join(drops, ", ") + "}"); return g; } private void writeFile(ClassG g, Element element) throws IOException { JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(g.type.fqName, element); try (PrintWriter w = new PrintWriter(sourceFile.openWriter())) { for (String s : g.generate()) { w.println(stripEnd(s, " ")); } } } private ClassG generateSession() throws IOException { String p = packageElement.getQualifiedName().toString(); // TODO: Support a name prefix from @SqlEntitySet TypeRef type = new TypeRef(p + ".Session"); /* TypeRef conType = g.add(Connection.class); Parameters parameters = new Parameters(); ParameterRef c = parameters.addParameter(conType, "c"); List body = new ArrayList<>(); for (EntityMirror entity : entities) { FieldRef fieldRef = g.addField(entity.javaName.asElement().asType(), toFieldName(entity.daoName)); body.add("this." + fieldRef.name + " = new " + entity.daoName + "(" + c.name + ");"); } g.addConstructor(parameters, body); */ return new ClassG(PUBLIC, type); } 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, TypeElement type) { AnnotationMirror annotation = findAnnotation(c, type); if (annotation != null) { return annotation; } throw new CompilerException(type, "Could not find annotation " + c.getSimpleName()); } }