/* * Copyright (c) 2017 * * 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 org.acra; import com.google.auto.service.AutoService; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; import org.acra.annotation.Configuration; import org.acra.annotation.ConfigurationBuilder; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.processing.AbstractProcessor; 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.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import static org.acra.ModelUtils.*; /** * Creates the BaseConfigurationBuilder class based on the annotation annotated with {@link Configuration}. * Creates the ACRAConfiguration class based on the BaseConfigurationBuilder and the class annotated with {@link ConfigurationBuilder} * * @author F43nd1r * @since 18.03.2017 */ @AutoService(Processor.class) @SupportedSourceVersion(SourceVersion.RELEASE_6) public class AcraAnnotationProcessor extends AbstractProcessor { private Elements elementUtils; private Types typeUtils; private ModelUtils utils; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); typeUtils = processingEnv.getTypeUtils(); utils = new ModelUtils(processingEnv); } @Override public Set<String> getSupportedAnnotationTypes() { return new HashSet<>(Arrays.asList(Configuration.class.getName(), ConfigurationBuilder.class.getName())); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { try { final Set<MethodDefinition> methodDefinitions = process(roundEnv, Configuration.class.getName(), ElementKind.ANNOTATION_TYPE, new HashSet<>(), this::createBuilderClass); process(roundEnv, ConfigurationBuilder.class.getName(), ElementKind.CLASS, null, type -> createConfigClass(type, methodDefinitions)); } catch (Exception e) { e.printStackTrace(); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate acra classes"); } return true; } private <T> T process(RoundEnvironment roundEnv, String annotationName, ElementKind kind, T defaultValue, CheckedFunction<TypeElement, T> function) throws IOException { final TypeElement annotation = elementUtils.getTypeElement(annotationName); final ArrayList<? extends Element> annotatedElements = new ArrayList<>(roundEnv.getElementsAnnotatedWith(annotation)); if (annotatedElements.size() > 1) { for (Element e : annotatedElements) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("Only one %s can be annotated with %s", kind.name(), annotationName), e); } } else if (!annotatedElements.isEmpty()) { final Element e = annotatedElements.get(0); if (e.getKind() == kind) { return function.apply((TypeElement) e); } else { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("%s is only supported on %s", annotationName, kind.name()), e); } } return defaultValue; } /** * Creates the ACRAConfiguration class * * @param builder type of the builder which will be used to determine methods to generate * @param methodDefinitions additional methods to be included in the configuration (e.g. from the builder base class) * @return null * @throws IOException if the class file can't be written */ private Void createConfigClass(TypeElement builder, Set<MethodDefinition> methodDefinitions) throws IOException { final Set<MethodDefinition> methods = getRelevantMethods(builder, methodDefinitions); final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(ACRA_CONFIGURATION) .addSuperinterface(Serializable.class) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); utils.addClassJavadoc(classBuilder, builder); final CodeBlock.Builder constructor = CodeBlock.builder(); for (MethodDefinition method : methods) { final String name = method.getName(); final TypeMirror type = utils.getImmutableType(method.getType()); if (type != method.getType()) { constructor.addStatement("$1L = new $2T($3L.$1L())", name, type, PARAM_BUILDER); } else { constructor.addStatement("$1L = $2L.$1L()", name, PARAM_BUILDER); } final TypeName typeName = TypeName.get(type); classBuilder.addField(FieldSpec.builder(typeName, name, Modifier.PRIVATE).addAnnotations(method.getAnnotations()).build()); classBuilder.addMethod(MethodSpec.methodBuilder(name) .returns(typeName) .addModifiers(Modifier.PUBLIC) .addAnnotations(method.getAnnotations()) .addStatement("return $L", name) .build()); } classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC) .addParameter(ParameterSpec.builder(TypeName.get(builder.asType()), PARAM_BUILDER) .addAnnotation(AnnotationSpec.builder(ANNOTATION_NON_NULL).build()) .build()) .addCode(constructor.build()) .build()); utils.write(classBuilder.build()); return null; } /** * Collects all relevant methods from a type. * For a definition of relevant methods, see {@link ModelUtils#shouldRetain(MethodDefinition)}. * * @param builder the type to collect methods from * @return relevant methods in the type */ private Set<MethodDefinition> getRelevantMethods(TypeElement builder, Set<MethodDefinition> methodDefinitions) { final Set<MethodDefinition> result = builder.getEnclosedElements().stream().filter(e -> e.getKind() == ElementKind.METHOD && !e.getModifiers().contains(Modifier.PRIVATE)) .map(ExecutableElement.class::cast).map(MethodDefinition::from).collect(Collectors.toCollection(HashSet::new)); result.addAll(methodDefinitions); return result.stream().filter(utils::shouldRetain).collect(Collectors.toSet()); } /** * Creates the BaseConfigurationBuilder class * * @param config the configuration annotation type which will be used to determine methods to generate * @return all generated getters * @throws IOException if the class file can't be written */ private Set<MethodDefinition> createBuilderClass(TypeElement config) throws IOException { final TypeVariableName returnType = TypeVariableName.get("T", ClassName.get(CONFIGURATION_PACKAGE, CONFIGURATION_BUILDER)); final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(CONFIGURATION_BUILDER) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) .addTypeVariable(returnType); utils.addClassJavadoc(classBuilder, config); final CodeBlock.Builder constructor = CodeBlock.builder() .addStatement("final $1T $2L = $3L.getClass().getAnnotation($1T.class)", config.asType(), VAR_ANNOTATION_CONFIG, PARAM_APP) .beginControlFlow("if ($L != null)", VAR_ANNOTATION_CONFIG); final Set<MethodDefinition> result = config.getEnclosedElements().stream().filter(element -> element.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast).filter(utils::isNotDeprecated).map(e -> handleMethod(e, classBuilder, constructor, returnType)).collect(Collectors.toSet()); constructor.endControlFlow(); classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC) .addParameter(ParameterSpec.builder(APPLICATION, PARAM_APP) .addAnnotation(AnnotationSpec.builder(ANNOTATION_NON_NULL).build()) .build()) .addCode(constructor.build()) .build()); utils.write(classBuilder.build()); return result; } /** * Derives all code from one method: A setter, a getter, a field and a line in the constructor * * @param method the method to derive from * @param classBuilder the class to add methods to * @param constructor the constructor in which the field will be initialized * @return the generated getter */ private MethodDefinition handleMethod(ExecutableElement method, TypeSpec.Builder classBuilder, CodeBlock.Builder constructor, TypeName returnType) { final String name = utils.getName(method); final TypeMirror type = method.getReturnType(); final TypeName typeName = TypeName.get(type); final TypeName boxedType = TypeName.get(utils.getBoxedType(type)); final List<AnnotationSpec> annotations = ModelUtils.getAnnotations(method); classBuilder.addField(FieldSpec.builder(boxedType, name, Modifier.PRIVATE) .addAnnotations(annotations) .build()); classBuilder.addMethod(utils.addMethodJavadoc(MethodSpec.methodBuilder(PREFIX_SETTER + utils.capitalizeFirst(name)), method) .returns(returnType) .addParameter(ParameterSpec.builder(typeName, name).addAnnotations(annotations).build()) .varargs(type.getKind() == TypeKind.ARRAY) .addModifiers(Modifier.PUBLIC) .addStatement("this.$1L = $1L", name) .addStatement("return ($T) this", returnType) .build()); final CodeBlock.Builder code = CodeBlock.builder() .beginControlFlow("if ($L != null)", name) .addStatement("return $L", name) .endControlFlow(); if (type.getKind() == TypeKind.ARRAY) { code.addStatement("return new $T$L", typeUtils.erasure(type), method.getDefaultValue()); } else { code.addStatement("return $L", method.getDefaultValue()); } classBuilder.addMethod(MethodSpec.methodBuilder(name) .returns(typeName) .addAnnotations(annotations) .addCode(code.build()) .build()); constructor.addStatement("$L = $L.$L()", name, VAR_ANNOTATION_CONFIG, method.getSimpleName().toString()); return new MethodDefinition(name, type, annotations); } @FunctionalInterface interface CheckedFunction<T, R> { R apply(T t) throws IOException; } }