/* * 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.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeSpec; import org.acra.annotation.Name; import org.acra.annotation.NoPropagation; import java.io.IOException; import java.text.DateFormat; import java.util.Calendar; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; /** * Collection of constants and helper methods to generate ACRA classes * * @author F43nd1r * @since 18.03.2017 */ class ModelUtils { static final String CONFIGURATION_PACKAGE = "org.acra.config"; static final String CONFIGURATION_BUILDER = "BaseConfigurationBuilder"; static final String ACRA_CONFIGURATION = "ACRAConfiguration"; static final String PREFIX_SETTER = "set"; static final String PARAM_APP = "app"; static final String PARAM_BUILDER = "builder"; static final String VAR_ANNOTATION_CONFIG = "annotationConfig"; static final ClassName APPLICATION = ClassName.bestGuess("android.app.Application"); static final ClassName ANNOTATION_NON_NULL = ClassName.bestGuess("android.support.annotation.NonNull"); private static final String IMMUTABLE_MAP = "org.acra.collections.ImmutableMap"; private static final String IMMUTABLE_LIST = "org.acra.collections.ImmutableList"; private static final String IMMUTABLE_SET = "org.acra.collections.ImmutableSet"; private static final ClassName ANNOTATION_NO_PROPAGATION = ClassName.get(NoPropagation.class); private final Types typeUtils; private final Elements elementUtils; private final TypeMirror map; private final TypeMirror set; private final TypeElement immutableMap; private final TypeElement immutableSet; private final TypeElement immutableList; private final ProcessingEnvironment processingEnv; private final DateFormat dateFormat; ModelUtils(ProcessingEnvironment processingEnv) { this.processingEnv = processingEnv; typeUtils = processingEnv.getTypeUtils(); elementUtils = processingEnv.getElementUtils(); map = elementUtils.getTypeElement(Map.class.getName()).asType(); set = elementUtils.getTypeElement(Set.class.getName()).asType(); immutableMap = elementUtils.getTypeElement(IMMUTABLE_MAP); immutableSet = elementUtils.getTypeElement(IMMUTABLE_SET); immutableList = elementUtils.getTypeElement(IMMUTABLE_LIST); dateFormat = DateFormat.getDateTimeInstance(); } /** * Returns an immutable type extending this type, or if the type is an array a immutable list type * * @param type the type * @return the immutable counterpart (might be type, if type is already immutable or no immutable type was found) */ TypeMirror getImmutableType(TypeMirror type) { if (typeUtils.isAssignable(typeUtils.erasure(type), map)) { return getWithParams(immutableMap, type); } else if (typeUtils.isAssignable(typeUtils.erasure(type), set)) { return getWithParams(immutableSet, type); } else if (type.getKind() == TypeKind.ARRAY) { return typeUtils.getDeclaredType(immutableList, ((ArrayType) type).getComponentType()); } return type; } /** * Creates a type based on base, but with the type parameters from parameterType * * @param baseType base * @param parameterType parameterType * @return the parametrized type */ private TypeMirror getWithParams(TypeElement baseType, TypeMirror parameterType) { final List<? extends TypeMirror> parameters = ((DeclaredType) parameterType).getTypeArguments(); return typeUtils.getDeclaredType(baseType, parameters.toArray(new TypeMirror[parameters.size()])); } /** * Writes the given class to a respective file in the configuration package * * @param typeSpec the class * @throws IOException if writing fails */ void write(TypeSpec typeSpec) throws IOException { JavaFile.builder(CONFIGURATION_PACKAGE, typeSpec) .skipJavaLangImports(true) .indent(" ") .addFileComment("Copyright (c) " + Calendar.getInstance().get(Calendar.YEAR) + "\n\n" + "Licensed under the Apache License, Version 2.0 (the \"License\");\n" + "you may not use this file except in compliance with the License.\n\n" + "http://www.apache.org/licenses/LICENSE-2.0\n\n" + "Unless required by applicable law or agreed to in writing, software\n" + "distributed under the License is distributed on an \"AS IS\" BASIS,\n" + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + "See the License for the specific language governing permissions and\n" + "limitations under the License.") .build() .writeTo(processingEnv.getFiler()); } /** * @param method a method * @return annotationSpecs for all relevant annotations on the method */ static List<AnnotationSpec> getAnnotations(ExecutableElement method) { return method.getAnnotationMirrors().stream().map(AnnotationSpec::get) .filter(annotationSpec -> !ClassName.get(Name.class).equals(annotationSpec.type)).collect(Collectors.toList()); } /** * Box the type if it is primitive * * @param type the type to box * @return the boxed type or type, if it is not primitive */ TypeMirror getBoxedType(TypeMirror type) { if (type.getKind().isPrimitive()) { return typeUtils.boxedClass((PrimitiveType) type).asType(); } else { return type; } } /** * Capitalizes the first letter in the given string * * @param word the string * @return the string with a capitalized first letter */ String capitalizeFirst(final String word) { return Character.toUpperCase(word.charAt(0)) + word.substring(1); } /** * Determines if a method is relevant for ACRAConfiguration generation * A method is not relevant, if it starts with "set", or is annotated with @Hide * * @param method the method to check * @return if the method is relevant */ boolean shouldRetain(MethodDefinition method) { return !method.getName().startsWith(PREFIX_SETTER) && !method.getAnnotations().stream().anyMatch(a -> a.type.equals(ANNOTATION_NO_PROPAGATION)); } /** * @param method a method * @return false if the method is deprecated */ boolean isNotDeprecated(ExecutableElement method) { return method.getAnnotation(Deprecated.class) == null; } void addClassJavadoc(TypeSpec.Builder builder, TypeElement base) { builder.addJavadoc("Class generated based on {@link $L} ($L)\n", base.getQualifiedName(), dateFormat.format(Calendar.getInstance().getTime())); } MethodSpec.Builder addMethodJavadoc(MethodSpec.Builder builder, ExecutableElement base) { final String baseComment = elementUtils.getDocComment(base); if (baseComment == null) return builder; final String name = base.getSimpleName().toString(); return builder.addJavadoc(baseComment.replaceAll("(\n|^) ", "$1").replaceAll("@return ((.|\n)*)$", "@param " + name + " $1@return this instance\n")); } String getName(ExecutableElement method) { final Name name = method.getAnnotation(Name.class); if(name != null){ return name.value(); } return method.getSimpleName().toString(); } }