package fr.lteconsulting.hexa.databinding.annotation.processor; import fr.lteconsulting.hexa.client.tools.StringUtils; import fr.lteconsulting.hexa.databinding.annotation.Observable; import fr.lteconsulting.hexa.databinding.annotation.processor.modules.ProcessorModule; import java.io.IOException; import java.io.Writer; import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic.Kind; import static fr.lteconsulting.hexa.databinding.annotation.processor.Tags.*; @SupportedAnnotationTypes({"fr.lteconsulting.hexa.databinding.annotation.Observable"}) @SupportedSourceVersion(SourceVersion.RELEASE_7) public class ObservableAnnotationProcessor extends BaseAnnotationProcessor { private static final String TEMPLATE_CLASS = "fr/lteconsulting/hexa/databinding/annotation/processor/TemplateClass.txt"; static String[] getterPrefixes = new String[] { "get", "is" }; static String[] setterPrefixes = new String[] { "set" }; protected static final int BEGIN_INDEX = 0; protected static final int CONSTRUCTOR_INDEX = 1; protected static final int COPY_CONSTRUCTOR_INDEX = 2; protected static final int FIELD_GETTER_INDEX = 3; protected static final int METHOD_SETTER_INDEX = 4; protected static final int FIELD_SETTER_INDEX = 5; @Override protected String getTargetTypeName(TypeElement typeElement) { String sourceTypeName = typeElement.getSimpleName().toString(); String targetTypeName; String SUFFIX = "Internal"; if( sourceTypeName.endsWith( SUFFIX ) ) targetTypeName = StringUtils.capitalizeFirstLetter(sourceTypeName) .substring(0, sourceTypeName.length() - SUFFIX.length()); else targetTypeName = "Observable" + StringUtils.capitalizeFirstLetter(sourceTypeName); return targetTypeName; } protected int getInheritDepth(Annotation annotation) { if(annotation instanceof Observable) { Observable observable = (Observable) annotation; int depth = ((Observable) annotation).inheritDepth(); return observable.inherit() ? depth : (depth != Observable.INHERIT_MAX ? depth : 0); } return -1; } protected boolean canUseCopyConstructor(Annotation annotation) { return annotation instanceof Observable && ((Observable)annotation).copyConstructor(); } @Override protected void doProcess(ProcInfo procInfo, Writer writer) throws IOException { int inheritDepth = getInheritDepth(procInfo.annotation); Template template = writeClassIntro(procInfo); template.replace(CONSTRUCTORS, generateConstructors(procInfo, inheritDepth)); template.replace(FIELDS_AND_METHODS, generateFieldsAndMethods(procInfo, inheritDepth)); writer.write(template.toString()); } protected Template writeClassIntro(ProcInfo procInfo) { Template template = Template.fromResource(TEMPLATE_CLASS, BEGIN_INDEX); template.replace(EXTRA_IMPORTS, generateExtraImports(procInfo)); template.replace(CLASS_ENTRY, generateClassEntry(procInfo).toString()); parseGeneralTags(template, procInfo); return template; } protected String generateExtraImports(ProcInfo procInfo) { String result = ""; for(ProcessorModule module : getProcessorModules()) { for(String newImport : module.getImports(procInfo)) { if(!procInfo.getExtraImports().contains(newImport)) { String formatted = (!newImport.startsWith("import") ? "import " : "") + newImport + (!newImport.contains(";") ? ";" : "") + "\n"; procInfo.addExtraImport(formatted); result += formatted; } } } return result; } protected StringBuilder generateClassEntry(ProcInfo procInfo) { StringBuilder sb = new StringBuilder(); for(ProcessorModule module : getProcessorModules()) { String classEntry = module.getClassEntry(procInfo); if(!classEntry.endsWith("\n")) { classEntry += "\n"; } sb.append(classEntry); } return sb; } private String generateConstructors(ProcInfo procInfo, int inheritDepth) { StringBuilder sb = new StringBuilder(); boolean hasDefault = false; for(ExecutableElement constr : ElementFilter.constructorsIn(procInfo.typeElement.getEnclosedElements())) { if(constr.getModifiers().contains(Modifier.PRIVATE)) { continue; } Template ctr = Template.fromResource(TEMPLATE_CLASS, CONSTRUCTOR_INDEX); ctr.replace( TARGET_CLASS_NAME, procInfo.implName); // Verify default constructor if(!hasDefault) { hasDefault = constr.getParameters().isEmpty(); } // Build the constructors parameters StringBuilder formalParameters = new StringBuilder(); boolean addComa = false; for(VariableElement parameter : constr.getParameters()) { String paramTypeName = TypeSimplifier.getTypeQualifiedName(parameter.asType()); if(addComa) formalParameters.append(", "); else addComa = true; formalParameters.append(paramTypeName); formalParameters.append(" "); formalParameters.append(parameter.getSimpleName()); } ctr.replace(FORMAL_PARAMETERS, formalParameters.toString()); StringBuilder parameters = new StringBuilder(); addComa = false; for(VariableElement parameter : constr.getParameters()) { if(addComa) parameters.append(", "); else addComa = true; parameters.append(parameter.getSimpleName()); } ctr.replace(PARAMETERS, parameters.toString()); sb.append(ctr.toString()); } // Only create the copy constructor when there is // a default constructor present if(hasDefault && canUseCopyConstructor(procInfo.annotation)) { Template copyCtr = Template.fromResource(TEMPLATE_CLASS, COPY_CONSTRUCTOR_INDEX); copyCtr.replace(TARGET_CLASS_NAME, procInfo.implName); copyCtr.replace(SOURCE_CLASS_NAME, procInfo.typeElement.getSimpleName().toString()); copyCtr.replace(MAP_FIELDS, generateCopyConstructor(procInfo, procInfo.typeElement, inheritDepth)); sb.append(copyCtr.toString()); } else { sb.append("\n\t// Cannot generate copy constructor when there is no default constructor"); sb.append("\n\t// Either add a default constructor or manually add this constructor.\n"); } return sb.toString(); } private String generateCopyConstructor(ProcInfo procInfo, TypeElement typeElement, int inheritDepth) { StringBuilder sb = new StringBuilder(); boolean indent = false; for(VariableElement field : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) { Set<Modifier> mods = field.getModifiers(); if(mods.contains(Modifier.STATIC) || mods.contains(Modifier.FINAL)) { // We should be ignoring static/final fields since they // shouldn't be modified by a local object call. continue; } String fieldName = field.getSimpleName().toString(); if(!isFieldAccessible(field, procInfo)) { // Ensure the method exists since this field is inaccessible ExecutableElement getter = getFieldGetterMethod(typeElement, fieldName); if(getter != null) { // Generate the setter method using the getter to copy // the field data from source to the target class ExecutableElement setter = getFieldSetterMethod(typeElement, fieldName); if(setter != null) { if (indent) { sb.append("\n\t\t"); } else { indent = true; } sb.append(setter.getSimpleName()) .append("(").append("o.").append(getter.getSimpleName()) .append("()").append(")").append(";"); } } else { msg.printMessage(Kind.WARNING, "Field cannot be accessed " + "and has no getter method: " + fieldName, field); } } else { // Simply make the copy directly via field access if(indent){ sb.append("\n\t\t"); } else { indent = true; } sb.append("this.").append(field.getSimpleName()).append(" = ").append("o.") .append(field.getSimpleName()).append(";"); } } if(inheritDepth > 0) { // Process the superclass TypeMirror superMirror = typeElement.getSuperclass(); if(superMirror instanceof DeclaredType) { TypeElement superType = (TypeElement)((DeclaredType)superMirror).asElement(); // Don't process base java.lang.Object types if(!superType.getQualifiedName().toString().equals("java.lang.Object")) { String copyCtr = generateCopyConstructor(procInfo, superType, inheritDepth - 1); if(!copyCtr.isEmpty()) { // Only append if there was a result sb.append("\n\t\t").append(copyCtr); } } } } return sb.toString(); } private String generateFieldsAndMethods(ProcInfo procInfo, int inheritDepth) { Set<String> settersDone = new HashSet<>(); Set<String> gettersDone = new HashSet<>(); String methodsFromMethods = generateMethodFromMethods(procInfo.typeElement, settersDone, gettersDone, inheritDepth); String methodsFromFields = generateMethodFromFields(procInfo, procInfo.typeElement, settersDone, gettersDone, inheritDepth); return methodsFromFields + methodsFromMethods; } private String generateMethodFromFields(ProcInfo procInfo, TypeElement typeElement, Set<String> settersDone, Set<String> gettersDone, int inheritDepth) { StringBuilder sb = new StringBuilder(); for (VariableElement field : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) { if(!isFieldAccessible(field, procInfo)) { continue; } Set<Modifier> mods = field.getModifiers(); if(mods.contains(Modifier.STATIC) || mods.contains(Modifier.FINAL)) { // We should be ignoring static/final fields since they // shouldn't be modified by a local object call. continue; } String fieldName = field.getSimpleName().toString(); TypeMirror fieldType = field.asType(); // Process setting field generation if(!settersDone.contains(fieldName)) { String methodName = "set" + StringUtils.capitalizeFirstLetter(fieldName); try { sb.append(generateFieldSetterStub(methodName, fieldName, fieldType)); } catch(CodeGenerationIncompleteException ex) { throw new CodeGenerationIncompleteException(typeElement.getQualifiedName() + " setter stub generation failed.", ex); } settersDone.add(fieldName); } // Process getter field generation if(!gettersDone.contains(fieldName)) { String methodName; if(fieldType.getKind().equals(TypeKind.BOOLEAN)) { methodName = "is"; } else { methodName = "get"; } methodName += StringUtils.capitalizeFirstLetter(fieldName); try { sb.append(generateFieldGetterStub(methodName, fieldName, fieldType)); } catch(CodeGenerationIncompleteException ex) { throw new CodeGenerationIncompleteException(typeElement.getQualifiedName() + " getter stub generation failed.", ex); } gettersDone.add(fieldName); } } if(inheritDepth > 0) { // Process the superclass TypeMirror superMirror = typeElement.getSuperclass(); if(superMirror instanceof DeclaredType) { TypeElement superType = (TypeElement)((DeclaredType)superMirror).asElement(); // Don't process base java.lang.Object types if(!superType.getQualifiedName().toString().equals("java.lang.Object")) { sb.append(generateMethodFromFields(procInfo, superType, settersDone, gettersDone, inheritDepth - 1)); } } } return sb.toString(); } private String generateMethodFromMethods(TypeElement typeElement, Set<String> settersDone, Set<String> gettersDone, int inheritDepth) { StringBuilder sb = new StringBuilder(); for(ExecutableElement method : ElementFilter.methodsIn(typeElement.getEnclosedElements())) { String methodName = method.getSimpleName().toString(); Set<Modifier> mods = method.getModifiers(); if(mods.contains(Modifier.PRIVATE)) { continue; } String fieldName = null; for(String prefix : setterPrefixes) { if(methodName.startsWith(prefix) && Character.isUpperCase(methodName.charAt(prefix.length()))) { fieldName = StringUtils.lowerFirstLetter(methodName.substring(prefix.length())); } } if(fieldName != null) { if(settersDone.contains(fieldName)) { // We have already processed this field method. continue; } else if(mods.contains(Modifier.FINAL)) { msg.printMessage(Kind.ERROR, "Setter for '" + fieldName + "' field is marked " + "as 'final' cannot override this method.", method); continue; } else if(mods.contains(Modifier.ABSTRACT)) { // Gracefully ignore abstract setter methods definitions // We have probably already found this method anyway. continue; } sb.append(generateMethodSetterStub(method, fieldName)); settersDone.add( fieldName ); } else { for(String prefix : getterPrefixes) { if( methodName.startsWith(prefix) && Character.isUpperCase(methodName.charAt(prefix.length()))) { fieldName = StringUtils.lowerFirstLetter(methodName.substring(prefix.length())); } } if(fieldName != null) { gettersDone.add(fieldName); } } } if(inheritDepth > 0) { // Process the superclass TypeMirror superMirror = typeElement.getSuperclass(); if(superMirror instanceof DeclaredType) { TypeElement superType = (TypeElement)((DeclaredType)superMirror).asElement(); // Don't process base java.lang.Object types if(!superType.getQualifiedName().toString().equals("java.lang.Object")) { sb.append(generateMethodFromMethods(superType, settersDone, gettersDone, inheritDepth - 1)); } } } return sb.toString(); } private String generateFieldSetterStub(String methodName, String fieldName, TypeMirror fieldType) { Template setter = Template.fromResource(TEMPLATE_CLASS, FIELD_SETTER_INDEX); setter.replace(MODIFIERS, "public"); setter.replace(METHOD_NAME, methodName); try { setter.replace(PROPERTY_CLASS, TypeSimplifier.getTypeQualifiedName(fieldType)); } catch(CodeGenerationIncompleteException ex) { throw new CodeGenerationIncompleteException("Unable to generate field setter stub for '" + fieldName + "' (" + methodName + ")", ex); } setter.replace(PROPERTY, fieldName); return setter.toString(); } private String generateFieldGetterStub(String methodName, String fieldName, TypeMirror fieldType) { Template getter = Template.fromResource(TEMPLATE_CLASS, FIELD_GETTER_INDEX); try { getter.replace(PROPERTY_CLASS, TypeSimplifier.getTypeQualifiedName(fieldType)); } catch(CodeGenerationIncompleteException ex) { throw new CodeGenerationIncompleteException("Unable to generate field getter stub for '" + fieldName + "' (" + methodName + ")", ex); } getter.replace(METHOD_NAME, methodName); getter.replace(PROPERTY, fieldName); return getter.toString(); } private String generateMethodSetterStub(ExecutableElement method, String fieldName) { Template setter = Template.fromResource(TEMPLATE_CLASS, METHOD_SETTER_INDEX); StringBuilder modifiersBuilder = new StringBuilder(); for( Modifier mod : method.getModifiers() ) { modifiersBuilder.append(mod.toString()); } setter.replace(MODIFIERS, modifiersBuilder.toString()); setter.replace(METHOD_NAME, method.getSimpleName().toString()); setter.replace(PROPERTY_CLASS, TypeSimplifier.getTypeQualifiedName(method.getParameters().get(0).asType())); setter.replace(PROPERTY, fieldName); return setter.toString(); } private ExecutableElement getFieldSetterMethod(TypeElement typeElement, String fieldName) { return getMethodForField(typeElement, fieldName, setterPrefixes); } private ExecutableElement getFieldGetterMethod(TypeElement typeElement, String fieldName) { return getMethodForField(typeElement, fieldName, getterPrefixes); } private ExecutableElement getMethodForField(TypeElement typeElement, String fieldName, String... startsWith) { for( ExecutableElement method : ElementFilter.methodsIn(typeElement.getEnclosedElements()) ) { if (method.getModifiers().contains(Modifier.PRIVATE)) continue; String methodName = method.getSimpleName().toString(); for(String prefix : startsWith) { if (methodName.startsWith(prefix) && Character.isUpperCase(methodName.charAt(prefix.length()))) { String propName = StringUtils.lowerFirstLetter(methodName.substring(prefix.length())); if (!propName.equals(fieldName)) continue; return method; } } } return null; } private boolean isFieldAccessible(VariableElement field, ProcInfo procInfo) { Element enclosingType = field.getEnclosingElement(); // Check for matching package to ensure type // protected accessibility must be package enclosed String typePkg = elementUtils.getPackageOf(enclosingType).getQualifiedName().toString(); boolean foreignEnclosedType = !typePkg.equals(procInfo.packageName); // Private fields are inaccessible, protected // fields must not be foreign fields boolean isPrivate = field.getModifiers().contains(Modifier.PRIVATE); boolean isProtected = field.getModifiers().contains(Modifier.PROTECTED) || !field.getModifiers().contains(Modifier.PUBLIC); // Check field access conditions return !isPrivate && (!isProtected || !foreignEnclosedType); } }