1 /*
2  * Copyright 2018 The Android Open Source Project
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  *      http://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 androidx.versionedparcelable.compiler;
17 
18 import com.squareup.javapoet.AnnotationSpec;
19 import com.squareup.javapoet.ClassName;
20 import com.squareup.javapoet.FieldSpec;
21 import com.squareup.javapoet.JavaFile;
22 import com.squareup.javapoet.MethodSpec;
23 import com.squareup.javapoet.TypeName;
24 import com.squareup.javapoet.TypeSpec;
25 
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Objects;
36 import java.util.Set;
37 import java.util.regex.Pattern;
38 
39 import javax.annotation.processing.AbstractProcessor;
40 import javax.annotation.processing.Messager;
41 import javax.annotation.processing.ProcessingEnvironment;
42 import javax.annotation.processing.RoundEnvironment;
43 import javax.annotation.processing.SupportedAnnotationTypes;
44 import javax.annotation.processing.SupportedSourceVersion;
45 import javax.lang.model.SourceVersion;
46 import javax.lang.model.element.AnnotationMirror;
47 import javax.lang.model.element.AnnotationValue;
48 import javax.lang.model.element.Element;
49 import javax.lang.model.element.ElementKind;
50 import javax.lang.model.element.ExecutableElement;
51 import javax.lang.model.element.Modifier;
52 import javax.lang.model.element.TypeElement;
53 import javax.lang.model.element.VariableElement;
54 import javax.lang.model.type.TypeMirror;
55 import javax.tools.Diagnostic;
56 
57 /**
58  * Processes annotations from VersionedParcelables.
59  */
60 @SupportedAnnotationTypes({
61         VersionedParcelProcessor.VERSIONED_PARCELIZE,
62         VersionedParcelProcessor.PARCEL_FIELD,
63         VersionedParcelProcessor.NON_PARCEL_FIELD
64 })
65 @SupportedSourceVersion(SourceVersion.RELEASE_8)
66 public class VersionedParcelProcessor extends AbstractProcessor {
67 
68     static final String VERSIONED_PARCELIZE = "androidx.versionedparcelable.VersionedParcelize";
69     static final String PARCEL_FIELD = "androidx.versionedparcelable.ParcelField";
70     static final String NON_PARCEL_FIELD = "androidx.versionedparcelable.NonParcelField";
71 
72     private static final ClassName RESTRICT_TO = ClassName.get("androidx.annotation", "RestrictTo");
73     private static final ClassName RESTRICT_TO_SCOPE = RESTRICT_TO.nestedClass("Scope");
74     private static final ClassName VERSIONED_PARCEL =
75             ClassName.get("androidx.versionedparcelable", "VersionedParcel");
76 
77     private static final String GEN_SUFFIX = "Parcelizer";
78     private static final String READ = "read";
79     private static final String WRITE = "write";
80 
81     private Messager mMessager;
82     private ProcessingEnvironment mEnv;
83     private Map<Pattern, String> mMethodLookup = new HashMap<>();
84 
85     @Override
init(ProcessingEnvironment processingEnvironment)86     public synchronized void init(ProcessingEnvironment processingEnvironment) {
87         super.init(processingEnvironment);
88         mEnv = processingEnvironment;
89         mMessager = processingEnvironment.getMessager();
90         mMethodLookup.put(Pattern.compile("^boolean$"), "Boolean");
91         mMethodLookup.put(Pattern.compile("^int$"), "Int");
92         mMethodLookup.put(Pattern.compile("^long$"), "Long");
93         mMethodLookup.put(Pattern.compile("^float$"), "Float");
94         mMethodLookup.put(Pattern.compile("^double$"), "Double");
95         mMethodLookup.put(Pattern.compile("^java.lang.CharSequence$"), "CharSequence");
96         mMethodLookup.put(Pattern.compile("^java.lang.String$"), "String");
97         mMethodLookup.put(Pattern.compile("^android.os.IBinder$"), "StrongBinder");
98         mMethodLookup.put(Pattern.compile("^byte\\[\\]$"), "ByteArray");
99         mMethodLookup.put(Pattern.compile("^android.os.Bundle$"), "Bundle");
100         mMethodLookup.put(Pattern.compile("^android.os.PersistableBundle$"), "PersistableBundle");
101         mMethodLookup.put(Pattern.compile("^boolean\\[\\]$"), "BooleanArray");
102         mMethodLookup.put(Pattern.compile("^char\\[\\]$"), "CharArray");
103         mMethodLookup.put(Pattern.compile("^int\\[\\]$"), "IntArray");
104         mMethodLookup.put(Pattern.compile("^long\\[\\]$"), "LongArray");
105         mMethodLookup.put(Pattern.compile("^float\\[\\]$"), "FloatArray");
106         mMethodLookup.put(Pattern.compile("^double\\[\\]$"), "DoubleArray");
107         mMethodLookup.put(Pattern.compile("^java.lang.Exception$"), "Exception");
108         mMethodLookup.put(Pattern.compile("^byte$"), "Byte");
109         mMethodLookup.put(Pattern.compile("^android.util.Size$"), "Size");
110         mMethodLookup.put(Pattern.compile("^android.util.SizeF$"), "SizeF");
111         mMethodLookup.put(Pattern.compile("^android.util.SparseBooleanArray$"),
112                 "SparseBooleanArray");
113         mMethodLookup.put(Pattern.compile("^android.os.Parcelable$"), "Parcelable");
114         mMethodLookup.put(Pattern.compile("^java.util.List<.*>$"), "List");
115         mMethodLookup.put(Pattern.compile("^java.util.Set<.*>$"), "Set");
116         mMethodLookup.put(Pattern.compile("^java.util.Map<.*>$"), "Map");
117         mMethodLookup.put(Pattern.compile("^androidx.versionedparcelable.VersionedParcelable$"),
118                 "VersionedParcelable");
119     }
120 
121     @Override
process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)122     public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
123         if (set.isEmpty()) return true;
124         TypeElement cls = findAnnotation(set, VERSIONED_PARCELIZE);
125         TypeElement field = findAnnotation(set, PARCEL_FIELD);
126         TypeElement nonField = findAnnotation(set, NON_PARCEL_FIELD);
127         List<Element> versionedParcelables = new ArrayList<>();
128         Map<String, Set<Element>> fields = new HashMap<>();
129 
130         if (cls == null) {
131             error("Can't find class annotation");
132             return true;
133         }
134         if (field == null) {
135             error("Can't find field annotation, no fields?");
136             return true;
137         }
138         for (Element element: roundEnvironment.getElementsAnnotatedWith(cls)) {
139             if (element.getKind() != ElementKind.CLASS) {
140                 error(cls + " can only be applied to class.");
141                 return true;
142             }
143             versionedParcelables.add(element);
144         }
145         for (Element element: roundEnvironment.getElementsAnnotatedWith(field)) {
146             if (element.getKind() != ElementKind.FIELD) {
147                 error(field + " can only be applied to field.");
148                 return true;
149             }
150             Element clsElement = findClass(element);
151             if (!versionedParcelables.contains(clsElement)) {
152                 error(cls + " must be added to classes containing " + field);
153             } else {
154                 fields.computeIfAbsent(clsElement.toString(), (s) -> new HashSet<Element>())
155                         .add(element);
156             }
157         }
158         if (nonField != null) {
159             for (Element element: roundEnvironment.getElementsAnnotatedWith(nonField)) {
160                 if (element.getKind() != ElementKind.FIELD) {
161                     error(nonField + " can only be applied to field.");
162                     return true;
163                 }
164                 Element clsElement = findClass(element);
165                 if (!versionedParcelables.contains(clsElement)) {
166                     error(cls + " must be added to classes containing " + nonField);
167                 }
168             }
169         }
170         if (versionedParcelables.isEmpty()) {
171             error("No VersionedParcels found");
172             return true;
173         }
174         for (Element versionedParcelable: versionedParcelables) {
175             ArrayList<String> takenIds = new ArrayList<>();
176             AnnotationMirror annotation = findAnnotationMirror(
177                     versionedParcelable.getAnnotationMirrors(), VERSIONED_PARCELIZE);
178             String allowSerialization = getValue(annotation, "allowSerialization", "false");
179             String ignoreParcelables = getValue(annotation, "ignoreParcelables", "false");
180             String isCustom = getValue(annotation, "isCustom", "false");
181             String deprecatedIds = getValue(annotation, "deprecatedIds", "");
182             String jetifyAs = getValue(annotation, "jetifyAs", "");
183             String factoryClass = getValue(annotation, "factory", "");
184             parseDeprecated(takenIds, deprecatedIds);
185             checkClass(typeString(versionedParcelable.asType()), versionedParcelable, takenIds);
186 
187             ArrayList<Element> f = new ArrayList<>();
188             TypeElement te = (TypeElement) mEnv.getTypeUtils().asElement(
189                     versionedParcelable.asType());
190             while (te != null) {
191                 Set<Element> collection = fields.get(te.getQualifiedName().toString());
192                 if (collection != null) {
193                     f.addAll(collection);
194                 }
195                 te = (TypeElement) mEnv.getTypeUtils().asElement(te.getSuperclass());
196             }
197             generateSerialization(versionedParcelable, f,
198                     allowSerialization, ignoreParcelables, isCustom, jetifyAs, factoryClass);
199         }
200 
201         return true;
202     }
203 
204     @SuppressWarnings("StringSplitter")
parseDeprecated(ArrayList<String> takenIds, String deprecatedIds)205     private void parseDeprecated(ArrayList<String> takenIds, String deprecatedIds) {
206         deprecatedIds = deprecatedIds.replace("{", "").replace("}", "");
207         String[] ids = deprecatedIds.split(",");
208         for (String id: ids) {
209             takenIds.add(id.trim());
210         }
211     }
212 
generateSerialization(Element versionedParcelable, List<Element> fields, String allowSerialization, String ignoreParcelables, String isCustom, String jetifyAs, String factoryClass)213     private void generateSerialization(Element versionedParcelable, List<Element> fields,
214             String allowSerialization, String ignoreParcelables, String isCustom,
215             String jetifyAs, String factoryClass) {
216         boolean custom = "true".equals(isCustom);
217         AnnotationSpec restrictTo = AnnotationSpec.builder(RESTRICT_TO)
218                 .addMember("value", "$T.LIBRARY", RESTRICT_TO_SCOPE)
219                 .build();
220         TypeSpec.Builder genClass = TypeSpec
221                 .classBuilder(versionedParcelable.getSimpleName() + GEN_SUFFIX)
222                 .addOriginatingElement(versionedParcelable)
223                 .addAnnotation(restrictTo)
224                 .addModifiers(Modifier.PUBLIC);
225         if (jetifyAs == null || jetifyAs.length() == 0) {
226             genClass.addModifiers(Modifier.FINAL);
227         }
228 
229         ArrayList<VariableElement> parcelFields = new ArrayList<>();
230         findFields(fields, parcelFields);
231 
232         TypeName type = ClassName.get((TypeElement) versionedParcelable);
233         AnnotationSpec suppressUncheckedWarning = AnnotationSpec.builder(
234                 ClassName.get("java.lang", "SuppressWarnings"))
235                 .addMember("value", "$S", "unchecked")
236                 .build();
237         MethodSpec.Builder readBuilder = MethodSpec
238                 .methodBuilder(READ)
239                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
240                 .addAnnotation(suppressUncheckedWarning)
241                 .returns(type)
242                 .addParameter(VERSIONED_PARCEL, "parcel");
243         if (factoryClass != null && factoryClass.length() != 0) {
244             // Strip the .class
245             factoryClass = factoryClass.substring(0, factoryClass.lastIndexOf('.'));
246             ClassName cls = ClassName.bestGuess(factoryClass);
247             genClass.addField(FieldSpec.builder(cls, "sBuilder")
248                     .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
249                     .initializer("new $T()", cls)
250                     .build());
251             readBuilder.addStatement("$T obj = sBuilder.get()", type);
252         } else {
253             readBuilder.addStatement("$1T obj = new $1T()", type);
254         }
255 
256         MethodSpec.Builder writeBuilder = MethodSpec
257                 .methodBuilder(WRITE)
258                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
259                 .addAnnotation(suppressUncheckedWarning)
260                 .addParameter(type, "obj")
261                 .addParameter(VERSIONED_PARCEL, "parcel")
262                 .addStatement("parcel.setSerializationFlags($L, $L)", allowSerialization,
263                         ignoreParcelables);
264         if (custom) {
265             writeBuilder.addStatement("obj.onPreParceling(parcel.isStream())");
266         }
267         parcelFields.sort(Comparator.comparing(e -> getValue(getAnnotation(e), "value", null)));
268         for (VariableElement e: parcelFields) {
269             genClass.addOriginatingElement(e);
270             AnnotationMirror annotation = getAnnotation(e);
271             String id = getValue(annotation, "value", null);
272             String defaultValue = getValue(annotation, "defaultValue", null, false);
273             String method = getMethod(e);
274             readBuilder.addStatement("obj.$L = parcel.$L(obj.$L, $L)", e.getSimpleName(),
275                     "read" + method, e.getSimpleName(), id);
276 
277             if (defaultValue != null && defaultValue.length() != 0) {
278                 if (defaultValue.equals("\"null\"")) {
279                     writeBuilder.beginControlFlow("if (obj.$L != null)", e.getSimpleName());
280                 } else {
281                     if (isNative(e)) {
282                         writeBuilder.beginControlFlow("if ($L != obj.$L)", strip(defaultValue),
283                                 e.getSimpleName());
284                     } else if (isArray(e)) {
285                         writeBuilder.beginControlFlow("if (!$T.equals($L, obj.$L))",
286                                 Arrays.class, strip(defaultValue), e.getSimpleName());
287                     } else {
288                         String v = "java.lang.String".equals(typeString(e.asType())) ? defaultValue
289                                 : strip(defaultValue);
290                         writeBuilder.beginControlFlow("if (!$L.equals(obj.$L))",
291                                 v, e.getSimpleName());
292                     }
293                 }
294             }
295             writeBuilder.addStatement("parcel.$L(obj.$L, $L)", "write" + method,
296                     e.getSimpleName(), id);
297             if (defaultValue != null && defaultValue.length() != 0) {
298                 writeBuilder.endControlFlow();
299             }
300         }
301 
302         if (custom) {
303             readBuilder.addStatement("obj.onPostParceling()");
304         }
305         readBuilder.addStatement("return obj");
306         genClass.addMethod(readBuilder.build());
307         genClass.addMethod(writeBuilder.build());
308         try {
309             TypeSpec typeSpec = genClass.build();
310             String pkg = getPkg(versionedParcelable);
311             JavaFile.builder(pkg,
312                     typeSpec).build().writeTo(mEnv.getFiler());
313             if (jetifyAs != null && jetifyAs.length() > 0) {
314                 int index = jetifyAs.lastIndexOf('.');
315                 String jetPkg = jetifyAs.substring(1, index);
316                 String superCls = pkg + "." + versionedParcelable.getSimpleName() + GEN_SUFFIX;
317                 TypeSpec.Builder jetifyClass = TypeSpec
318                         .classBuilder(jetifyAs.substring(index + 1, jetifyAs.length() - 1)
319                                 + GEN_SUFFIX)
320                         .addAnnotation(restrictTo)
321                         .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
322                         // The empty package here is a hack to avoid an import,
323                         // since the classes have the same name.
324                         .superclass(ClassName.get("", superCls));
325                 jetifyClass.addMethod(MethodSpec
326                         .methodBuilder(READ)
327                         .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
328                         .returns(type)
329                         .addParameter(VERSIONED_PARCEL, "parcel")
330                         .addStatement("return $L.read(parcel)", superCls)
331                         .build());
332                 jetifyClass.addMethod(MethodSpec
333                         .methodBuilder(WRITE)
334                         .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
335                         .addParameter(type, "obj")
336                         .addParameter(VERSIONED_PARCEL, "parcel")
337                         .addStatement("$L.write(obj, parcel)", superCls)
338                         .build());
339                 TypeSpec jetified = jetifyClass.build();
340                 JavaFile.builder(jetPkg, jetified).build().writeTo(mEnv.getFiler());
341             }
342         } catch (IOException e) {
343             error("Exception writing " + e);
344         }
345     }
346 
strip(String s)347     private String strip(String s) {
348         if (!s.startsWith("\"")) return s;
349         return s.substring(1, s.length() - 1);
350     }
351 
getPkg(Element s)352     private String getPkg(Element s) {
353         String pkg = mEnv.getElementUtils().getPackageOf(s).toString();
354         return pkg;
355     }
356 
357     /** Returns a simple string version of the type, with no annotations. */
typeString(TypeMirror type)358     private String typeString(TypeMirror type) {
359         return TypeName.get(type).toString();
360     }
361 
getMethod(VariableElement e)362     private String getMethod(VariableElement e) {
363         TypeMirror type = e.asType();
364         String m = getMethod(type);
365         if (m != null) return m;
366         TypeElement te = (TypeElement) mEnv.getTypeUtils().asElement(type);
367         while (te != null) {
368             for (TypeMirror t: te.getInterfaces()) {
369                 m = getMethod(t);
370                 if (m != null) return m;
371             }
372             te = te.getSuperclass() != null ? (TypeElement) mEnv.getTypeUtils()
373                     .asElement(te.getSuperclass()) : null;
374         }
375         // Manual handling for generic arrays to go last.
376         if (typeString(type).contains("[]")) {
377             return "Array";
378         }
379         error("Can't find type for " + e + " (type: " + type + ")");
380         return null;
381     }
382 
isArray(VariableElement e)383     private boolean isArray(VariableElement e) {
384         return typeString(e.asType()).endsWith("[]");
385     }
386 
isNative(VariableElement e)387     private boolean isNative(VariableElement e) {
388         String type = typeString(e.asType());
389         return "int".equals(type)
390                 || "byte".equals(type)
391                 || "char".equals(type)
392                 || "long".equals(type)
393                 || "double".equals(type)
394                 || "float".equals(type)
395                 || "boolean".equals(type);
396     }
397 
getMethod(TypeMirror typeMirror)398     private String getMethod(TypeMirror typeMirror) {
399         // Get an annotation-free version of the type string through TypeName
400         String typeString = typeString(typeMirror);
401         for (Pattern p: mMethodLookup.keySet()) {
402             if (p.matcher(typeString).find()) {
403                 return mMethodLookup.get(p);
404             }
405         }
406         return null;
407     }
408 
findFields(Collection<? extends Element> fields, ArrayList<VariableElement> parcelFields)409     private void findFields(Collection<? extends Element> fields,
410             ArrayList<VariableElement> parcelFields) {
411         for (Element element: fields) {
412             if (element.getKind() == ElementKind.FIELD) {
413                 if (!element.getModifiers().contains(Modifier.STATIC)) {
414                     if (fields.contains(element)) {
415                         parcelFields.add((VariableElement) element);
416                     }
417                 }
418             } else {
419                 findFields(element.getEnclosedElements(), parcelFields);
420             }
421         }
422     }
423 
checkClass(String clsName, Element element, ArrayList<String> takenIds)424     private void checkClass(String clsName, Element element, ArrayList<String> takenIds) {
425         if (element.getKind() == ElementKind.FIELD) {
426             if (!element.getModifiers().contains(Modifier.STATIC)) {
427                 int i;
428                 List<? extends AnnotationMirror> annotations = element.getAnnotationMirrors();
429                 for (i = 0; i < annotations.size(); i++) {
430                     AnnotationMirror annotation = annotations.get(i);
431                     if (typeString(annotation.getAnnotationType()).equals(PARCEL_FIELD)) {
432                         String valStr = getValue(annotation, "value", null);
433                         if (valStr == null) {
434                             return;
435                         }
436                         if (takenIds.contains(valStr)) {
437                             error("Id " + valStr + " already taken on " + element);
438                             return;
439                         }
440                         takenIds.add(valStr);
441                         break;
442                     }
443                     if (typeString(annotation.getAnnotationType()).equals(NON_PARCEL_FIELD)) {
444                         break;
445                     }
446                 }
447                 if (i == annotations.size()) {
448                     error(clsName + "." + element.getSimpleName() + " is not annotated with "
449                                     + "@ParcelField or @NonParcelField");
450                     return;
451                 }
452             }
453         } else if (element.getKind() == ElementKind.CLASS) {
454             TypeElement te = (TypeElement) mEnv.getTypeUtils().asElement(
455                     element.asType());
456             if (te != null && te.getSuperclass() != null) {
457                 Element e = (TypeElement) mEnv.getTypeUtils().asElement(te.getSuperclass());
458                 if (e != null) {
459                     checkClass(clsName, e, takenIds);
460                 }
461             }
462         }
463         for (Element e: element.getEnclosedElements()) {
464             if (e.getKind() != ElementKind.CLASS) {
465                 checkClass(clsName, e, takenIds);
466             }
467         }
468     }
469 
getAnnotation(Element e)470     private AnnotationMirror getAnnotation(Element e) {
471         List<? extends AnnotationMirror> annotations = e.getAnnotationMirrors();
472         for (int i = 0; i < annotations.size(); i++) {
473             AnnotationMirror annotation = annotations.get(i);
474             if (typeString(annotation.getAnnotationType()).equals(PARCEL_FIELD)) {
475                 return annotation;
476             }
477         }
478         return null;
479     }
480 
getValue(AnnotationMirror annotation, String name, String defValue)481     private String getValue(AnnotationMirror annotation, String name, String defValue) {
482         return getValue(annotation, name, defValue, true);
483     }
484 
getValue(AnnotationMirror annotation, String name, String defValue, boolean required)485     private String getValue(AnnotationMirror annotation, String name, String defValue,
486             boolean required) {
487         Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues =
488                 annotation.getElementValues();
489         for (ExecutableElement av: elementValues.keySet()) {
490             if (Objects.equals(av.getSimpleName().toString(), name)) {
491                 AnnotationValue v = elementValues.get(av);
492                 return v != null ? v.toString() : av.getDefaultValue().getValue().toString();
493             }
494         }
495         if (defValue != null) {
496             return defValue;
497         }
498         if (required) {
499             error("Can't find annotation value");
500         }
501         return null;
502     }
503 
findClass(Element element)504     private Element findClass(Element element) {
505         if (element != null && element.getKind() != ElementKind.CLASS) {
506             return findClass(element.getEnclosingElement());
507         }
508         return element;
509     }
510 
findAnnotationMirror(List<? extends AnnotationMirror> set, String name)511     private AnnotationMirror findAnnotationMirror(List<? extends AnnotationMirror> set,
512             String name) {
513         for (AnnotationMirror annotation: set) {
514             if (String.valueOf(annotation.getAnnotationType()).equals(name)) {
515                 return annotation;
516             }
517         }
518         return null;
519     }
520 
findAnnotation(Set<? extends TypeElement> set, String name)521     private TypeElement findAnnotation(Set<? extends TypeElement> set, String name) {
522         for (TypeElement typeElement: set) {
523             if (String.valueOf(typeElement).equals(name)) {
524                 return typeElement;
525             }
526         }
527         return null;
528     }
529 
error(String error)530     private void error(String error) {
531         mMessager.printMessage(Diagnostic.Kind.ERROR, "VersionedParcelProcessor - " + error);
532     }
533 }
534 
535