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