1 /* 2 * Copyright 2021 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.enterprise.connectedapps.processor; 17 18 import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CREATOR_CLASSNAME; 19 import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; 20 import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS; 21 import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; 22 import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS; 23 import static com.google.common.base.Preconditions.checkNotNull; 24 import static java.util.stream.Collectors.toList; 25 26 import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; 27 import com.google.android.enterprise.connectedapps.processor.containers.Context; 28 import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; 29 import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour; 30 import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; 31 import com.google.common.collect.ImmutableSet; 32 import com.google.common.collect.Iterables; 33 import com.squareup.javapoet.AnnotationSpec; 34 import com.squareup.javapoet.ArrayTypeName; 35 import com.squareup.javapoet.ClassName; 36 import com.squareup.javapoet.CodeBlock; 37 import com.squareup.javapoet.FieldSpec; 38 import com.squareup.javapoet.JavaFile; 39 import com.squareup.javapoet.MethodSpec; 40 import com.squareup.javapoet.ParameterSpec; 41 import com.squareup.javapoet.ParameterizedTypeName; 42 import com.squareup.javapoet.TypeName; 43 import com.squareup.javapoet.TypeSpec; 44 import java.io.IOException; 45 import java.io.PrintWriter; 46 import java.util.List; 47 import javax.lang.model.element.Element; 48 import javax.lang.model.element.ElementKind; 49 import javax.lang.model.element.ExecutableElement; 50 import javax.lang.model.element.Modifier; 51 import javax.lang.model.element.TypeElement; 52 import javax.lang.model.element.VariableElement; 53 import javax.lang.model.type.MirroredTypeException; 54 import javax.lang.model.type.MirroredTypesException; 55 import javax.lang.model.type.PrimitiveType; 56 import javax.lang.model.type.TypeMirror; 57 import javax.lang.model.util.Types; 58 import javax.tools.JavaFileObject; 59 60 /** Utility methods used for code generation. */ 61 public final class GeneratorUtilities { 62 63 private final Context context; 64 GeneratorUtilities(Context context)65 public GeneratorUtilities(Context context) { 66 this.context = checkNotNull(context); 67 } 68 69 /** 70 * Extract a class provided in an annotation. 71 * 72 * <p>The {@code runnable} should call the annotation method that the class is being extracted 73 * for. 74 */ extractClassFromAnnotation(Types types, Runnable runnable)75 public static TypeElement extractClassFromAnnotation(Types types, Runnable runnable) { 76 // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html 77 // "The annotation returned by this method could contain an element whose value is of type 78 // Class. This value cannot be returned directly: information necessary to locate and load a 79 // class (such as the class loader to use) is not available, and the class might not be loadable 80 // at all. Attempting to read a Class object by invoking the relevant method on the returned 81 // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror 82 // may be extracted." 83 try { 84 runnable.run(); 85 } catch (MirroredTypeException e) { 86 return e.getTypeMirrors().stream() 87 .map(t -> (TypeElement) types.asElement(t)) 88 .findFirst() 89 .get(); 90 } 91 throw new AssertionError("Could not extract class from annotation"); 92 } 93 94 /** 95 * Extract classes provided in an annotation. 96 * 97 * <p>The {@code runnable} should call the annotation method that the classes are being extracted 98 * for. 99 */ extractClassesFromAnnotation(Types types, Runnable runnable)100 public static List<TypeElement> extractClassesFromAnnotation(Types types, Runnable runnable) { 101 // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html 102 // "The annotation returned by this method could contain an element whose value is of type 103 // Class. This value cannot be returned directly: information necessary to locate and load a 104 // class (such as the class loader to use) is not available, and the class might not be loadable 105 // at all. Attempting to read a Class object by invoking the relevant method on the returned 106 // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror 107 // may be extracted." 108 try { 109 runnable.run(); 110 } catch (MirroredTypesException e) { 111 return e.getTypeMirrors().stream() 112 .map(t -> (TypeElement) types.asElement(t)) 113 .collect(toList()); 114 } 115 throw new AssertionError("Could not extract classes from annotation"); 116 } 117 findCrossProfileMethodsInClass(TypeElement clazz)118 public static ImmutableSet<ExecutableElement> findCrossProfileMethodsInClass(TypeElement clazz) { 119 ImmutableSet.Builder<ExecutableElement> result = ImmutableSet.builder(); 120 clazz.getEnclosedElements().stream() 121 .filter(e -> e instanceof ExecutableElement) 122 .map(e -> (ExecutableElement) e) 123 .filter(e -> e.getKind() == ElementKind.METHOD) 124 .filter(AnnotationFinder::hasCrossProfileAnnotation) 125 .forEach(result::add); 126 return result.build(); 127 } 128 findCrossProfileProviderMethodsInClass( TypeElement clazz)129 public static ImmutableSet<ExecutableElement> findCrossProfileProviderMethodsInClass( 130 TypeElement clazz) { 131 ImmutableSet.Builder<ExecutableElement> result = ImmutableSet.builder(); 132 clazz.getEnclosedElements().stream() 133 .filter(e -> e instanceof ExecutableElement) 134 .map(e -> (ExecutableElement) e) 135 .filter(e -> e.getKind() == ElementKind.METHOD) 136 .filter(AnnotationFinder::hasCrossProfileProviderAnnotation) 137 .forEach(result::add); 138 return result.build(); 139 } 140 141 /** Generate a {@code @link} reference to a given method. */ methodJavadocReference(ExecutableElement method)142 public static CodeBlock methodJavadocReference(ExecutableElement method) { 143 CodeBlock.Builder methodCall = CodeBlock.builder(); 144 methodCall.add("{@link $T#", method.getEnclosingElement()); 145 methodCall.add("$L(", method.getSimpleName()); 146 147 if (!method.getParameters().isEmpty()) { 148 methodCall.add("$T", method.getParameters().iterator().next().asType()); 149 150 for (VariableElement param : 151 method.getParameters().subList(1, method.getParameters().size())) { 152 methodCall.add(",$T", param.asType()); 153 } 154 } 155 156 methodCall.add(")}"); 157 return methodCall.build(); 158 } 159 writeClassToFile(String packageName, TypeSpec.Builder clazzBuilder)160 public void writeClassToFile(String packageName, TypeSpec.Builder clazzBuilder) { 161 writeClassToFile(packageName, clazzBuilder.build()); 162 } 163 writeClassToFile(String packageName, TypeSpec clazz)164 void writeClassToFile(String packageName, TypeSpec clazz) { 165 final String qualifiedClassName = 166 packageName.isEmpty() ? clazz.name : packageName + "." + clazz.name; 167 168 JavaFile javaFile = JavaFile.builder(packageName, clazz).build(); 169 try { 170 JavaFileObject builderFile = 171 context.processingEnv().getFiler().createSourceFile(qualifiedClassName); 172 try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { 173 javaFile.writeTo(out); 174 } 175 } catch (IOException e) { 176 throw new IllegalStateException("Error writing " + qualifiedClassName + " to file", e); 177 } 178 } 179 180 /** 181 * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances 182 * ready to be used with a generated method. 183 */ extractParametersFromMethod( SupportedTypes supportedTypes, ExecutableElement method, AutomaticallyResolvedParameterFilterBehaviour filterBehaviour)184 static List<ParameterSpec> extractParametersFromMethod( 185 SupportedTypes supportedTypes, 186 ExecutableElement method, 187 AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) { 188 if (filterBehaviour == LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { 189 return extractParametersFromMethod(method); 190 } else if (filterBehaviour == REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { 191 return method.getParameters().stream() 192 .filter(param -> !supportedTypes.isAutomaticallyResolved(param.asType())) 193 .map(GeneratorUtilities::convertVariableToParameterSpec) 194 .collect(toList()); 195 } else if (filterBehaviour == REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) { 196 throw new IllegalArgumentException("Can not replace parameters when extracting"); 197 } 198 throw new IllegalArgumentException("Unknown filterBehaviour " + filterBehaviour); 199 } 200 201 /** 202 * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances 203 * ready to be used with a generated method. 204 * 205 * <p>This will not filter automatically resolved parameters. For that functionality use {@link 206 * #extractParametersFromMethod(SupportedTypes, ExecutableElement, 207 * AutomaticallyResolvedParameterFilterBehaviour)}. 208 */ extractParametersFromMethod(ExecutableElement method)209 static List<ParameterSpec> extractParametersFromMethod(ExecutableElement method) { 210 return method.getParameters().stream() 211 .map(GeneratorUtilities::convertVariableToParameterSpec) 212 .collect(toList()); 213 } 214 convertVariableToParameterSpec(VariableElement variable)215 private static ParameterSpec convertVariableToParameterSpec(VariableElement variable) { 216 ParameterSpec.Builder builder = 217 ParameterSpec.builder( 218 ClassName.get(variable.asType()), variable.getSimpleName().toString()); 219 builder.addModifiers(variable.getModifiers()); 220 return builder.build(); 221 } 222 223 /** If type is primitive, return the boxed version of that type, otherwise return the type. */ boxIfNecessary(TypeMirror type)224 TypeMirror boxIfNecessary(TypeMirror type) { 225 if (!type.getKind().isPrimitive()) { 226 return type; 227 } 228 229 PrimitiveType primitiveType = (PrimitiveType) type; 230 return context.types().boxedClass(primitiveType).asType(); 231 } 232 addDefaultParcelableMethods(TypeSpec.Builder classBuilder, ClassName className)233 void addDefaultParcelableMethods(TypeSpec.Builder classBuilder, ClassName className) { 234 classBuilder.addMethod( 235 MethodSpec.methodBuilder("describeContents") 236 .addModifiers(Modifier.PUBLIC) 237 .addAnnotation(Override.class) 238 .returns(int.class) 239 .addStatement("return 0") 240 .build()); 241 242 TypeName creatorType = ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, className); 243 244 TypeSpec creator = 245 TypeSpec.anonymousClassBuilder("") 246 .addSuperinterface(creatorType) 247 .addMethod( 248 MethodSpec.methodBuilder("createFromParcel") 249 .addModifiers(Modifier.PUBLIC) 250 .addAnnotation(Override.class) 251 .returns(className) 252 .addParameter(PARCEL_CLASSNAME, "in") 253 .addStatement("return new $T(in)", className) 254 .build()) 255 .addMethod( 256 MethodSpec.methodBuilder("newArray") 257 .addModifiers(Modifier.PUBLIC) 258 .addAnnotation(Override.class) 259 .returns(ArrayTypeName.of(className)) 260 .addParameter(int.class, "size") 261 .addStatement("return new $T[size]", className) 262 .build()) 263 .build(); 264 265 classBuilder.addField( 266 FieldSpec.builder(creatorType, "CREATOR") 267 .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) 268 .addAnnotation( 269 AnnotationSpec.builder(SuppressWarnings.class) 270 .addMember("value", "$S", "rawtypes") 271 .build()) 272 .initializer("$L", creator) 273 .build()); 274 } 275 276 /** Generate a reference to a cross-profile method which can be used in javadoc. */ generateMethodReference( CrossProfileTypeInfo crossProfileType, CrossProfileMethodInfo method)277 public static CodeBlock generateMethodReference( 278 CrossProfileTypeInfo crossProfileType, CrossProfileMethodInfo method) { 279 CodeBlock.Builder reference = CodeBlock.builder(); 280 281 reference.add("$T#$L(", crossProfileType.className(), method.simpleName()); 282 283 List<TypeMirror> parameterTypes = convertParametersToTypes(method); 284 285 if (!parameterTypes.isEmpty()) { 286 for (int i = 0; i < parameterTypes.size() - 1; i++) { 287 reference.add("$T, ", TypeUtils.getRawTypeClassName(parameterTypes.get(i))); 288 } 289 reference.add("$T", TypeUtils.getRawTypeClassName(Iterables.getLast(parameterTypes))); 290 } 291 292 reference.add(")"); 293 return reference.build(); 294 } 295 convertParametersToTypes(CrossProfileMethodInfo method)296 private static List<TypeMirror> convertParametersToTypes(CrossProfileMethodInfo method) { 297 return method.methodElement().getParameters().stream().map(Element::asType).collect(toList()); 298 } 299 } 300