1 /* 2 * Copyright 2019 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 17 package android.processor.view.inspector; 18 19 import android.processor.view.inspector.InspectableClassModel.IntEnumEntry; 20 import android.processor.view.inspector.InspectableClassModel.IntFlagEntry; 21 import android.processor.view.inspector.InspectableClassModel.Property; 22 23 import androidx.annotation.NonNull; 24 25 import com.squareup.javapoet.ClassName; 26 import com.squareup.javapoet.CodeBlock; 27 import com.squareup.javapoet.FieldSpec; 28 import com.squareup.javapoet.JavaFile; 29 import com.squareup.javapoet.MethodSpec; 30 import com.squareup.javapoet.NameAllocator; 31 import com.squareup.javapoet.ParameterizedTypeName; 32 import com.squareup.javapoet.TypeName; 33 import com.squareup.javapoet.TypeSpec; 34 35 import java.io.IOException; 36 import java.util.ArrayList; 37 import java.util.Comparator; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.NoSuchElementException; 42 import java.util.stream.Collectors; 43 44 import javax.annotation.processing.Filer; 45 import javax.lang.model.element.Modifier; 46 47 /** 48 * Generates a source file defining a {@link android.view.inspector.InspectionCompanion}. 49 */ 50 public final class InspectionCompanionGenerator { 51 private final @NonNull Filer mFiler; 52 private final @NonNull Class mRequestingClass; 53 54 /** 55 * The class name for {@code R.java}. 56 */ 57 private static final ClassName R_CLASS_NAME = ClassName.get("android", "R"); 58 59 /** 60 * The class name of {@link android.view.inspector.InspectionCompanion}. 61 */ 62 private static final ClassName INSPECTION_COMPANION = ClassName.get( 63 "android.view.inspector", "InspectionCompanion"); 64 65 /** 66 * The class name of {@link android.view.inspector.PropertyMapper}. 67 */ 68 private static final ClassName PROPERTY_MAPPER = ClassName.get( 69 "android.view.inspector", "PropertyMapper"); 70 71 /** 72 * The class name of {@link android.view.inspector.PropertyReader}. 73 */ 74 private static final ClassName PROPERTY_READER = ClassName.get( 75 "android.view.inspector", "PropertyReader"); 76 77 /** 78 * The class name of {@link android.util.SparseArray}. 79 */ 80 private static final ClassName SPARSE_ARRAY = ClassName.get("android.util", "SparseArray"); 81 82 /** 83 * The class name of {@link android.view.inspector.IntFlagMapping}. 84 */ 85 private static final ClassName INT_FLAG_MAPPING = ClassName.get( 86 "android.view.inspector", "IntFlagMapping"); 87 88 /** 89 * The suffix of the generated class name after the class's binary name. 90 */ 91 private static final String GENERATED_CLASS_SUFFIX = "$InspectionCompanion"; 92 93 /** 94 * The null resource ID, copied to avoid a host dependency on platform code. 95 * 96 * @see android.content.res.Resources#ID_NULL 97 */ 98 private static final int ID_NULL = 0; 99 100 /** 101 * @param filer A filer to write the generated source to 102 * @param requestingClass A class object representing the class that invoked the generator 103 */ InspectionCompanionGenerator(@onNull Filer filer, @NonNull Class requestingClass)104 public InspectionCompanionGenerator(@NonNull Filer filer, @NonNull Class requestingClass) { 105 mFiler = filer; 106 mRequestingClass = requestingClass; 107 } 108 109 /** 110 * Generate and write an inspection companion. 111 * 112 * @param model The model to generated 113 * @throws IOException From the Filer 114 */ generate(@onNull InspectableClassModel model)115 public void generate(@NonNull InspectableClassModel model) throws IOException { 116 generateFile(model).writeTo(mFiler); 117 } 118 119 /** 120 * Generate a {@link JavaFile} from a model. 121 * 122 * This is package-public for testing. 123 * 124 * @param model The model to generate from 125 * @return A generated file of an {@link android.view.inspector.InspectionCompanion} 126 */ 127 @NonNull generateFile(@onNull InspectableClassModel model)128 JavaFile generateFile(@NonNull InspectableClassModel model) { 129 return JavaFile 130 .builder(model.getClassName().packageName(), generateTypeSpec(model)) 131 .indent(" ") 132 .build(); 133 } 134 135 /** 136 * Generate a {@link TypeSpec} for the {@link android.view.inspector.InspectionCompanion} 137 * for the supplied model. 138 * 139 * @param model The model to generate from 140 * @return A TypeSpec of the inspection companion 141 */ 142 @NonNull generateTypeSpec(@onNull InspectableClassModel model)143 private TypeSpec generateTypeSpec(@NonNull InspectableClassModel model) { 144 final List<Property> properties = new ArrayList<>(model.getAllProperties()); 145 properties.sort(Comparator.comparing(Property::getName)); 146 147 final Map<Property, FieldSpec> fields = generateIdFieldSpecs(properties); 148 149 TypeSpec.Builder builder = TypeSpec 150 .classBuilder(generateClassName(model)) 151 .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 152 .addSuperinterface(ParameterizedTypeName.get( 153 INSPECTION_COMPANION, model.getClassName())) 154 .addJavadoc("Inspection companion for {@link $T}.\n\n", model.getClassName()) 155 .addJavadoc("Generated by {@link $T}\n", getClass()) 156 .addJavadoc("on behalf of {@link $T}.\n", mRequestingClass) 157 .addField(FieldSpec 158 .builder(TypeName.BOOLEAN, "mPropertiesMapped", Modifier.PRIVATE) 159 .initializer("false") 160 .addJavadoc("Guards against reading properties before mapping them.\n") 161 .build()) 162 .addFields(properties.stream().map(fields::get).collect(Collectors.toList())) 163 .addMethod(generateMapProperties(properties, fields)) 164 .addMethod(generateReadProperties(properties, fields, model.getClassName())); 165 166 return builder.build(); 167 } 168 169 /** 170 * Map properties to fields to store the mapping IDs in the generated inspection companion. 171 * 172 * @param properties A list of property models 173 * @return A map of properties to their {@link FieldSpec} 174 */ 175 @NonNull generateIdFieldSpecs(@onNull List<Property> properties)176 private Map<Property, FieldSpec> generateIdFieldSpecs(@NonNull List<Property> properties) { 177 final Map<Property, FieldSpec> fields = new HashMap<>(); 178 final NameAllocator fieldNames = new NameAllocator(); 179 fieldNames.newName("mPropertiesMapped"); 180 181 for (Property property : properties) { 182 final String memberName = fieldNames.newName(String.format( 183 "m%s%sId", 184 property.getName().substring(0, 1).toUpperCase(), 185 property.getName().substring(1))); 186 187 fields.put(property, FieldSpec 188 .builder(TypeName.INT, memberName, Modifier.PRIVATE) 189 .addJavadoc("Property ID of {@code $L}.\n", property.getName()) 190 .build()); 191 } 192 193 return fields; 194 } 195 196 /** 197 * Generates an implementation of 198 * {@link android.view.inspector.InspectionCompanion#mapProperties( 199 * android.view.inspector.PropertyMapper)}. 200 * 201 * Example: 202 * <pre> 203 * @Override 204 * public void mapProperties(PropertyMapper propertyMapper) { 205 * mValueId = propertyMapper.mapInt("value", R.attr.value); 206 * mPropertiesMapped = true; 207 * } 208 * </pre> 209 * 210 * @param properties A sorted list of property models 211 * @param fields A map of properties to their ID field specs 212 * @return A method definition 213 */ 214 @NonNull generateMapProperties( @onNull List<Property> properties, @NonNull Map<Property, FieldSpec> fields)215 private MethodSpec generateMapProperties( 216 @NonNull List<Property> properties, 217 @NonNull Map<Property, FieldSpec> fields) { 218 final NameAllocator mappingVariables = new NameAllocator(); 219 220 final MethodSpec.Builder builder = MethodSpec.methodBuilder("mapProperties") 221 .addAnnotation(Override.class) 222 .addModifiers(Modifier.PUBLIC) 223 .addParameter(PROPERTY_MAPPER, "propertyMapper"); 224 225 // Reserve existing names 226 mappingVariables.newName("mPropertiesMapped"); 227 mappingVariables.newName("propertyMapper"); 228 properties.forEach(p -> mappingVariables.newName(fields.get(p).name)); 229 230 for (Property property : properties) { 231 final FieldSpec field = fields.get(property); 232 switch (property.getType()) { 233 case INT_ENUM: 234 builder.addCode(generateIntEnumPropertyMapperInvocation( 235 property, 236 field, 237 mappingVariables.newName(property.getName() + "EnumMapping"))); 238 break; 239 case INT_FLAG: 240 builder.addCode(generateIntFlagPropertyMapperInvocation( 241 property, 242 field, 243 mappingVariables.newName(property.getName() + "FlagMapping"))); 244 break; 245 default: 246 builder.addCode(generateSimplePropertyMapperInvocation(property, field)); 247 } 248 } 249 250 builder.addStatement("mPropertiesMapped = true"); 251 252 return builder.build(); 253 } 254 255 /** 256 * Generate a {@link android.view.inspector.PropertyMapper} invocation. 257 * 258 * Example: 259 * <pre> 260 * mValueId = propertyMapper.mapInt("value", R.attr.value); 261 * </pre> 262 * 263 * @param property A property model to map 264 * @param field The property ID field for the property 265 * @return A code block containing a statement 266 */ 267 @NonNull generateSimplePropertyMapperInvocation( @onNull Property property, @NonNull FieldSpec field)268 private CodeBlock generateSimplePropertyMapperInvocation( 269 @NonNull Property property, 270 @NonNull FieldSpec field) { 271 return CodeBlock 272 .builder() 273 .addStatement( 274 "$N = propertyMapper.map$L($S, $L)", 275 field, 276 methodSuffixForPropertyType(property.getType()), 277 property.getName(), 278 generateAttributeId(property)) 279 .build(); 280 } 281 282 /** 283 * Generate a {@link android.view.inspector.PropertyMapper} invocation for an int enum. 284 * 285 * Example: 286 * <pre> 287 * final SparseArray<String> valueEnumMapping = new SparseArray<>(); 288 * valueEnumMapping.put(1, "ONE"); 289 * valueEnumMapping.put(2, "TWO"); 290 * mValueId = propertyMapper.mapIntEnum("value", R.attr.value, valueEnumMapping::get); 291 * </pre> 292 * 293 * @param property A property model to map 294 * @param field The property ID field for the property 295 * @param variable The name of a local variable to use to store the mapping in 296 * @return A code block containing a series of statements 297 */ 298 @NonNull generateIntEnumPropertyMapperInvocation( @onNull Property property, @NonNull FieldSpec field, @NonNull String variable)299 private CodeBlock generateIntEnumPropertyMapperInvocation( 300 @NonNull Property property, 301 @NonNull FieldSpec field, 302 @NonNull String variable) { 303 final CodeBlock.Builder builder = CodeBlock.builder(); 304 305 final List<IntEnumEntry> enumEntries = property.getIntEnumEntries(); 306 enumEntries.sort(Comparator.comparing(IntEnumEntry::getValue)); 307 308 builder.addStatement( 309 "final $1T<$2T> $3N = new $1T<>()", 310 SPARSE_ARRAY, 311 String.class, 312 variable); 313 314 for (IntEnumEntry enumEntry : enumEntries) { 315 builder.addStatement( 316 "$N.put($L, $S)", 317 variable, 318 enumEntry.getValue(), 319 enumEntry.getName()); 320 } 321 322 builder.addStatement( 323 "$N = propertyMapper.mapIntEnum($S, $L, $N::get)", 324 field, 325 property.getName(), 326 generateAttributeId(property), 327 variable); 328 329 return builder.build(); 330 } 331 332 /** 333 * Generate a {@link android.view.inspector.PropertyMapper} invocation for an int flag. 334 * 335 * Example: 336 * <pre> 337 * final IntFlagMapping valueFlagMapping = new IntFlagMapping(); 338 * valueFlagMapping.add(0x00000003, 0x00000001, "ONE"); 339 * valueFlagMapping.add(0x00000003, 0x00000002, "TWO"); 340 * mValueId = propertyMapper.mapIntFlag("value", R.attr.value, valueFlagMapping::get); 341 * </pre> 342 * 343 * @param property A property model to map 344 * @param field The property ID field for the property 345 * @param variable The name of a local variable to use to store the mapping in 346 * @return A code block containing a series of statements 347 */ 348 @NonNull generateIntFlagPropertyMapperInvocation( @onNull Property property, @NonNull FieldSpec field, @NonNull String variable)349 private CodeBlock generateIntFlagPropertyMapperInvocation( 350 @NonNull Property property, 351 @NonNull FieldSpec field, 352 @NonNull String variable) { 353 final CodeBlock.Builder builder = CodeBlock.builder(); 354 355 final List<IntFlagEntry> flagEntries = property.getIntFlagEntries(); 356 flagEntries.sort(Comparator.comparing(IntFlagEntry::getName)); 357 358 builder.addStatement( 359 "final $1T $2N = new $1T()", 360 INT_FLAG_MAPPING, 361 variable); 362 363 for (IntFlagEntry flagEntry : flagEntries) { 364 builder.addStatement( 365 "$N.add($L, $L, $S)", 366 variable, 367 hexLiteral(flagEntry.getMask()), 368 hexLiteral(flagEntry.getTarget()), 369 flagEntry.getName()); 370 } 371 372 builder.addStatement( 373 "$N = propertyMapper.mapIntFlag($S, $L, $N::get)", 374 field, 375 property.getName(), 376 generateAttributeId(property), 377 variable); 378 379 return builder.build(); 380 } 381 382 /** 383 * Generate a literal attribute ID or reference to {@link android.R.attr}. 384 * 385 * Example: {@code R.attr.value} or {@code 0xdecafbad}. 386 * 387 * @param property A property model 388 * @return A code block containing the attribute ID 389 */ 390 @NonNull generateAttributeId(@onNull Property property)391 private CodeBlock generateAttributeId(@NonNull Property property) { 392 if (property.isAttributeIdInferrableFromR()) { 393 return CodeBlock.of("$T.attr.$L", R_CLASS_NAME, property.getName()); 394 } else { 395 if (property.getAttributeId() == ID_NULL) { 396 return CodeBlock.of("$L", ID_NULL); 397 } else { 398 return CodeBlock.of("$L", hexLiteral(property.getAttributeId())); 399 } 400 } 401 } 402 403 /** 404 * Generate an implementation of 405 * {@link android.view.inspector.InspectionCompanion#readProperties(Object, 406 * android.view.inspector.PropertyReader)}. 407 * 408 * Example: 409 * <pre> 410 * @Override 411 * public void readProperties(MyNode node, PropertyReader propertyReader) { 412 * if (!mPropertiesMapped) { 413 * throw new InspectionCompanion.UninitializedPropertyMapException(); 414 * } 415 * propertyReader.readInt(mValueId, node.getValue()); 416 * } 417 * </pre> 418 * 419 * @param properties An ordered list of property models 420 * @param fields A map from properties to their field specs 421 * @param nodeClass The class of the node, used for the parameter type 422 * @return A method definition 423 */ 424 @NonNull generateReadProperties( @onNull List<Property> properties, @NonNull Map<Property, FieldSpec> fields, @NonNull ClassName nodeClass)425 private MethodSpec generateReadProperties( 426 @NonNull List<Property> properties, 427 @NonNull Map<Property, FieldSpec> fields, 428 @NonNull ClassName nodeClass) { 429 final MethodSpec.Builder builder = MethodSpec.methodBuilder("readProperties") 430 .addAnnotation(Override.class) 431 .addModifiers(Modifier.PUBLIC) 432 .addParameter(nodeClass, "node") 433 .addParameter(PROPERTY_READER, "propertyReader") 434 .beginControlFlow("if (!mPropertiesMapped)") 435 .addStatement( 436 "throw new $T()", 437 INSPECTION_COMPANION.nestedClass("UninitializedPropertyMapException")) 438 .endControlFlow(); 439 440 for (Property property : properties) { 441 builder.addStatement( 442 "propertyReader.read$L($N, node.$L)", 443 methodSuffixForPropertyType(property.getType()), 444 fields.get(property), 445 property.getAccessor().invocation()); 446 } 447 448 return builder.build(); 449 } 450 451 /** 452 * Generate the final class name for the inspection companion from the model's class name. 453 * 454 * The generated class is added to the same package as the source class. If the class in the 455 * model is a nested class, the nested class names are joined with {@code "$"}. The suffix 456 * {@code "$InspectionCompanion"} is always added to the generated name. E.g.: For modeled 457 * class {@code com.example.Outer.Inner}, the generated class name will be 458 * {@code com.example.Outer$Inner$InspectionCompanion}. 459 * 460 * @param model The model to generate from 461 * @return A class name for the generated inspection companion class 462 */ 463 @NonNull generateClassName(@onNull InspectableClassModel model)464 private static ClassName generateClassName(@NonNull InspectableClassModel model) { 465 final ClassName className = model.getClassName(); 466 467 return ClassName.get( 468 className.packageName(), 469 String.join("$", className.simpleNames()) + GENERATED_CLASS_SUFFIX); 470 } 471 472 /** 473 * Get the suffix for a {@code map} or {@code read} method for a property type. 474 * 475 * @param type The requested property type 476 * @return A method suffix 477 */ 478 @NonNull methodSuffixForPropertyType(@onNull Property.Type type)479 private static String methodSuffixForPropertyType(@NonNull Property.Type type) { 480 switch (type) { 481 case BOOLEAN: 482 return "Boolean"; 483 case BYTE: 484 return "Byte"; 485 case CHAR: 486 return "Char"; 487 case DOUBLE: 488 return "Double"; 489 case FLOAT: 490 return "Float"; 491 case INT: 492 return "Int"; 493 case LONG: 494 return "Long"; 495 case SHORT: 496 return "Short"; 497 case OBJECT: 498 return "Object"; 499 case COLOR: 500 return "Color"; 501 case GRAVITY: 502 return "Gravity"; 503 case INT_ENUM: 504 return "IntEnum"; 505 case INT_FLAG: 506 return "IntFlag"; 507 case RESOURCE_ID: 508 return "ResourceId"; 509 default: 510 throw new NoSuchElementException(String.format("No such property type, %s", type)); 511 } 512 } 513 514 /** 515 * Format an int as an 8 digit hex literal 516 * 517 * @param value The value to format 518 * @return A string representation of the hex literal 519 */ 520 @NonNull hexLiteral(int value)521 private static String hexLiteral(int value) { 522 return String.format("0x%08x", value); 523 } 524 } 525