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