package com.ftinc.scoop; import com.google.auto.common.SuperficialValidation; import com.google.auto.service.AutoService; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; 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.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import static javax.lang.model.element.ElementKind.CLASS; import static javax.lang.model.element.ElementKind.INTERFACE; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.STATIC; import static javax.tools.Diagnostic.Kind.ERROR; @AutoService(Processor.class) public final class ScoopsProcesssor extends AbstractProcessor{ static final String VIEW_TYPE = "android.view.View"; static final String ACTIVITY_TYPE = "android.app.Activity"; private Filer filer; private Messager messager; private Elements elementUtils; private Types typeUtils; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnv.getFiler(); messager = processingEnv.getMessager(); elementUtils = processingEnv.getElementUtils(); typeUtils = processingEnv.getTypeUtils(); } @Override public Set<String> getSupportedAnnotationTypes() { return new HashSet<>( Arrays.asList( BindTopping.class.getCanonicalName(), BindToppingStatus.class.getCanonicalName() ) ); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(roundEnvironment); for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingClass bindingClass = entry.getValue(); JavaFile javaFile = bindingClass.brewJava(); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write view binder for type %s: %s", typeElement, e.getMessage()); } } return true; } private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>(); // Process each @BindTopping class for (Element element : env.getElementsAnnotatedWith(BindTopping.class)) { if (!SuperficialValidation.validateElement(element)) continue; try { parseBindTopping(element, targetClassMap); } catch (Exception e) { logParsingError(element, BindTopping.class, e); } } // Process each @BindToppingStatus class for (Element element : env.getElementsAnnotatedWith(BindToppingStatus.class)) { if(!SuperficialValidation.validateElement(element)) continue; try{ parseBindToppingStatus(element, targetClassMap); }catch (Exception e){ logParsingError(element, BindToppingStatus.class, e); } } return targetClassMap; } private void parseBindTopping(Element element, Map<TypeElement, BindingClass> targetClassMap){ TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Start by verifying common generated code restrictions. boolean hasError = isInaccessibleViaGeneratedCode(BindTopping.class, "fields", element) || isBindingInWrongPackage(BindTopping.class, element); // Verify that the target type extends from View. TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindTopping.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } if (hasError) { return; } BindTopping annotation = element.getAnnotation(BindTopping.class); // Start assembling binding information BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); String name = element.getSimpleName().toString(); TypeName type = TypeName.get(elementType); ClassName adapter = className(asTypeElement(getAdapterTypeMirror(annotation))); ClassName interpolator = className(asTypeElement(getInterpolatorTypeMirror(annotation))); FieldViewBinding binding = new FieldViewBinding(annotation.value(), name, adapter, interpolator, annotation.duration()); bindingClass.addViewBinding(binding); } private void parseBindToppingStatus(Element element, Map<TypeElement, BindingClass> targetClassMap){ // Verify that element is of type Class if(element.getKind() != ElementKind.CLASS){ error(element, "Only classes can be annotated with %s", BindToppingStatus.class.getSimpleName()); }else{ // Start by verifying common generated code restrictions. boolean hasError = false; // Verify that the target type extends from View. TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } if (!isSubtypeOfType(elementType, ACTIVITY_TYPE) && !isInterface(elementType)) { error(element, "@%s classes must extend from Activity(%s.%s)", BindToppingStatus.class.getSimpleName(), element.getSimpleName()); hasError = true; } if (hasError) { return; } BindToppingStatus annotation = element.getAnnotation(BindToppingStatus.class); // Start assembling binding information BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, (TypeElement) element); ClassName interpolator = className(asTypeElement(getInterpolatorTypeMirror(annotation))); ClassStatusBarBinding binding = new ClassStatusBarBinding(annotation.value(), interpolator, annotation.duration()); bindingClass.setStatusBarBinding(binding); } } private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap, TypeElement enclosingElement) { BindingClass bindingClass = targetClassMap.get(enclosingElement); if (bindingClass == null) { TypeName targetType = TypeName.get(enclosingElement.asType()); if (targetType instanceof ParameterizedTypeName) { targetType = ((ParameterizedTypeName) targetType).rawType; } String packageName = getPackageName(enclosingElement); String className = getClassName(enclosingElement, packageName); ClassName binderClassName = ClassName.get(packageName, className + "_ToppingBinder"); bindingClass = new BindingClass(targetType, binderClassName); targetClassMap.put(enclosingElement, bindingClass); } return bindingClass; } /*********************************************************************************************** * * Helper Methods * */ private ClassName className(TypeElement element){ if(element != null){ return ClassName.get(element); } return null; } private TypeElement asTypeElement(TypeMirror typeMirror) { if(typeMirror != null) { return (TypeElement) typeUtils.asElement(typeMirror); } return null; } private TypeMirror getAdapterTypeMirror(BindTopping annotation){ TypeMirror value = null; try { annotation.adapter(); }catch (MirroredTypeException e){ value = e.getTypeMirror(); } return value; } private TypeMirror getInterpolatorTypeMirror(BindTopping annotation){ TypeMirror value = null; try{ annotation.interpolator(); }catch (MirroredTypeException e){ value = e.getTypeMirror(); } return value; } private TypeMirror getInterpolatorTypeMirror(BindToppingStatus annotation){ TypeMirror value = null; try{ annotation.interpolator(); }catch (MirroredTypeException e){ value = e.getTypeMirror(); } return value; } private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass, String targetThing, Element element) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify method modifiers. Set<Modifier> modifiers = element.getModifiers(); if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) { error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify containing type. if (enclosingElement.getKind() != CLASS) { error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify containing class visibility is not private. if (enclosingElement.getModifiers().contains(PRIVATE)) { error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } return hasError; } private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass, Element element) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); String qualifiedName = enclosingElement.getQualifiedName().toString(); if (qualifiedName.startsWith("android.")) { error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return true; } if (qualifiedName.startsWith("java.")) { error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return true; } return false; } private void logParsingError(Element element, Class<? extends Annotation> annotation, Exception e) { StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); error(element, "Unable to parse @%s binding.\n\n%s", annotation.getSimpleName(), stackTrace); } private void error(Element element, String message, Object... args) { if (args.length > 0) { message = String.format(message, args); } processingEnv.getMessager().printMessage(ERROR, message, element); } private static String getClassName(TypeElement type, String packageName) { int packageLen = packageName.length() + 1; return type.getQualifiedName().toString().substring(packageLen).replace('.', '$'); } private String getPackageName(TypeElement type) { return elementUtils.getPackageOf(type).getQualifiedName().toString(); } private boolean isInterface(TypeMirror typeMirror) { return typeMirror instanceof DeclaredType && ((DeclaredType) typeMirror).asElement().getKind() == INTERFACE; } private boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) { if (otherType.equals(typeMirror.toString())) { return true; } if (typeMirror.getKind() != TypeKind.DECLARED) { return false; } DeclaredType declaredType = (DeclaredType) typeMirror; List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() > 0) { StringBuilder typeString = new StringBuilder(declaredType.asElement().toString()); typeString.append('<'); for (int i = 0; i < typeArguments.size(); i++) { if (i > 0) { typeString.append(','); } typeString.append('?'); } typeString.append('>'); if (typeString.toString().equals(otherType)) { return true; } } Element element = declaredType.asElement(); if (!(element instanceof TypeElement)) { return false; } TypeElement typeElement = (TypeElement) element; TypeMirror superType = typeElement.getSuperclass(); if (isSubtypeOfType(superType, otherType)) { return true; } for (TypeMirror interfaceType : typeElement.getInterfaces()) { if (isSubtypeOfType(interfaceType, otherType)) { return true; } } return false; } }