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