/* * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.remixer.annotation.processor; import com.google.auto.service.AutoService; import com.squareup.javapoet.JavaFile; import java.io.IOException; import java.lang.annotation.Annotation; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; 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.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; /** * The Annotation processor for Remixer. It checks that the annotations in {@code * com.google.android.libraries.remixer.annotation} are applied correctly and generates code to * support them. */ @AutoService(Processor.class) @SupportedSourceVersion(SourceVersion.RELEASE_7) public class RemixerAnnotationProcessor extends AbstractProcessor { private Elements elementUtils; private ErrorReporter errorReporter; private Filer filer; private Types typeUtils; private Set<String> alreadyProcessedClasses = new HashSet<>(); @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); errorReporter = new ErrorReporter(processingEnv); filer = processingEnv.getFiler(); typeUtils = processingEnv.getTypeUtils(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> set = new HashSet<>(); for (SupportedMethodAnnotation annotation : SupportedMethodAnnotation.values()) { set.add(annotation.getAnnotationType().getCanonicalName()); } return set; } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { try { Map<String, AnnotatedClass> annotatedClasses = new HashMap<>(); findMethodAnnotations(roundEnv, annotatedClasses); for (Map.Entry<String, AnnotatedClass> classEntry : annotatedClasses.entrySet()) { if (!alreadyProcessedClasses.contains(classEntry.getKey())) { JavaFile file = classEntry.getValue().generateJavaFile(); file.writeTo(filer); alreadyProcessedClasses.add(classEntry.getKey()); } } return true; } catch (RemixerAnnotationException ex) { errorReporter.reportError(ex.getElement(), ex.getMessage()); return false; } catch (IOException ex) { errorReporter.reportError(null, "Couldn't write file: " + ex.getMessage()); return false; } } /** * Finds all *Method annotations in the code. * * <p>Makes sure that they are applied to the right method (non-constructor, non-static, * public/default access, and that only take one parameter of the right type). */ private void findMethodAnnotations( RoundEnvironment roundEnv, Map<String, AnnotatedClass> annotatedClasses) throws RemixerAnnotationException { for (SupportedMethodAnnotation annotationType : SupportedMethodAnnotation.values()) { for (Element element : roundEnv.getElementsAnnotatedWith(annotationType.getAnnotationType())) { // We know these are Executable elements since RemixerInstances only apply to those. ExecutableElement method = (ExecutableElement) element; checkPublicOrDefault(method); checkMethod(method); checkParameter(method, annotationType.getParameterClass()); TypeElement clazz = (TypeElement) method.getEnclosingElement(); String className = clazz.getQualifiedName().toString(); if (!annotatedClasses.containsKey(className)) { annotatedClasses.put(className, new AnnotatedClass(clazz)); } Annotation annotation = method.getAnnotation(annotationType.getAnnotationType()); annotatedClasses.get(className) .addMethod(annotationType.getMethodAnnotation(clazz, method, annotation)); } } } /** * Checks that {@code method} contains the right parameters. * * @param clazz Defines what the right parameters are for this method, if it's null then * {@code method} must have no parameters, if it's not null then {@code method} must have * one parameter of type {@code clazz}. */ private void checkParameter(ExecutableElement method, Class<?> clazz) throws RemixerAnnotationException { int correctNumberOfParameters = clazz == null ? 0 : 1; if (method.getParameters().size() != correctNumberOfParameters) { throw new RemixerAnnotationException( method, String.format( Locale.getDefault(), "This method must have exactly %d parameter(s)", correctNumberOfParameters)); } if (correctNumberOfParameters == 1) { VariableElement parameter = method.getParameters().get(0); try { checkElementType(parameter, clazz.getCanonicalName()); } catch (RemixerAnnotationException ex) { // Throw the same exception with better message. throw new RemixerAnnotationException( parameter, String.format( Locale.getDefault(), "Trying to use Variable annotations on wrong parameter, must be of type %s", clazz)); } } } /** * Checks that {@code element} is a method (not a constructor) that isn't abstract or static. */ private void checkMethod(ExecutableElement element) throws RemixerAnnotationException { if (element.getKind() != ElementKind.METHOD || element.getModifiers().contains(Modifier.ABSTRACT) || element.getModifiers().contains(Modifier.STATIC)) { throw new RemixerAnnotationException( element, "Variable annotations can only be used on non-abstract, non-static, regular methods"); } } /** * Checks that {@code element} is a variable with type {@code clazz}. */ private void checkElementType(Element element, String clazz) throws RemixerAnnotationException { TypeMirror typeToCompare = elementUtils.getTypeElement(clazz).asType(); TypeMirror elementType = element.asType(); if (!typeUtils.isSubtype(elementType, typeToCompare)) { throw new RemixerAnnotationException(element, String.format(Locale.getDefault(), "Element must be of type %s to use Remixer Annotations", clazz)); } } /** * Checks that the access to {@code element} is either public or default so it can be accessed * from another class in the same package. */ private void checkPublicOrDefault(Element element) throws RemixerAnnotationException { Set<Modifier> modifiers = element.getModifiers(); if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) { throw new RemixerAnnotationException(element, "Variable annotations can only be used on public/default elements"); } } }