blob: 44d88bb4b73d073da38fd3ea615a8f04194e5151 [file] [log] [blame]
Ashley Rosede080eb2018-12-07 17:20:25 -05001/*
Ashley Rosec1a4dec2018-12-13 18:06:30 -05002 * Copyright 2019 The Android Open Source Project
Ashley Rosede080eb2018-12-07 17:20:25 -05003 *
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
17package android.processor.view.inspector;
18
Ashley Rose89d6bce2019-03-01 19:24:50 -050019
Ashley Rose0b671da2019-01-25 15:41:29 -050020import android.processor.view.inspector.InspectableClassModel.IntEnumEntry;
21import android.processor.view.inspector.InspectableClassModel.IntFlagEntry;
Ashley Rose171a7232018-12-11 17:32:58 -050022import android.processor.view.inspector.InspectableClassModel.Property;
23
Ashley Rosede080eb2018-12-07 17:20:25 -050024import com.squareup.javapoet.ClassName;
Ashley Rose171a7232018-12-11 17:32:58 -050025import com.squareup.javapoet.CodeBlock;
26import com.squareup.javapoet.FieldSpec;
Ashley Rosede080eb2018-12-07 17:20:25 -050027import com.squareup.javapoet.JavaFile;
28import com.squareup.javapoet.MethodSpec;
Ashley Rose171a7232018-12-11 17:32:58 -050029import com.squareup.javapoet.NameAllocator;
Ashley Rosede080eb2018-12-07 17:20:25 -050030import com.squareup.javapoet.ParameterizedTypeName;
Ashley Rose171a7232018-12-11 17:32:58 -050031import com.squareup.javapoet.TypeName;
Ashley Rosede080eb2018-12-07 17:20:25 -050032import com.squareup.javapoet.TypeSpec;
33
34import java.io.IOException;
Ashley Rose171a7232018-12-11 17:32:58 -050035import java.util.ArrayList;
36import java.util.Comparator;
37import java.util.List;
38import java.util.NoSuchElementException;
Ashley Rosede080eb2018-12-07 17:20:25 -050039import java.util.Optional;
40
41import javax.annotation.processing.Filer;
42import javax.lang.model.element.Modifier;
43
44/**
45 * Generates a source file defining a {@link android.view.inspector.InspectionCompanion}.
46 */
47public final class InspectionCompanionGenerator {
48 private final Filer mFiler;
49 private final Class mRequestingClass;
50
51 /**
Ashley Rose171a7232018-12-11 17:32:58 -050052 * The class name for {@code R.java}.
53 */
54 private static final ClassName R_CLASS_NAME = ClassName.get("android", "R");
55
56 /**
Ashley Rose171a7232018-12-11 17:32:58 -050057 * The class name of {@link android.view.inspector.InspectionCompanion}.
58 */
59 private static final ClassName INSPECTION_COMPANION = ClassName.get(
60 "android.view.inspector", "InspectionCompanion");
61
62 /**
63 * The class name of {@link android.view.inspector.PropertyMapper}.
64 */
65 private static final ClassName PROPERTY_MAPPER = ClassName.get(
66 "android.view.inspector", "PropertyMapper");
67
68 /**
69 * The class name of {@link android.view.inspector.PropertyReader}.
70 */
71 private static final ClassName PROPERTY_READER = ClassName.get(
72 "android.view.inspector", "PropertyReader");
73
74 /**
Ashley Rose0b671da2019-01-25 15:41:29 -050075 * The class name of {@link android.view.inspector.IntEnumMapping}.
76 */
77 private static final ClassName INT_ENUM_MAPPING = ClassName.get(
78 "android.view.inspector", "IntEnumMapping");
79
80 /**
81 * The class name of {@link android.view.inspector.IntFlagMapping}.
82 */
83 private static final ClassName INT_FLAG_MAPPING = ClassName.get(
84 "android.view.inspector", "IntFlagMapping");
85
86 /**
Ashley Rose171a7232018-12-11 17:32:58 -050087 * The {@code mPropertiesMapped} field.
88 */
89 private static final FieldSpec M_PROPERTIES_MAPPED = FieldSpec
90 .builder(TypeName.BOOLEAN, "mPropertiesMapped", Modifier.PRIVATE)
91 .initializer("false")
92 .addJavadoc(
93 "Set by {@link #mapProperties($T)} once properties have been mapped.\n",
94 PROPERTY_MAPPER)
95 .build();
96
97 /**
98 * The suffix of the generated class name after the class's binary name.
99 */
100 private static final String GENERATED_CLASS_SUFFIX = "$$InspectionCompanion";
101
102 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500103 * The null resource ID, copied to avoid a host dependency on platform code.
Ashley Rose171a7232018-12-11 17:32:58 -0500104 *
Aurimas Liutikasd8ebfef2019-01-16 12:46:42 -0800105 * @see android.content.res.Resources#ID_NULL
Ashley Rose171a7232018-12-11 17:32:58 -0500106 */
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500107 private static final int ID_NULL = 0;
Ashley Rose171a7232018-12-11 17:32:58 -0500108
109 /**
Ashley Rosede080eb2018-12-07 17:20:25 -0500110 * @param filer A filer to write the generated source to
111 * @param requestingClass A class object representing the class that invoked the generator
112 */
Ashley Rose171a7232018-12-11 17:32:58 -0500113 public InspectionCompanionGenerator(Filer filer, Class requestingClass) {
Ashley Rosede080eb2018-12-07 17:20:25 -0500114 mFiler = filer;
115 mRequestingClass = requestingClass;
116 }
117
118 /**
119 * Generate and write an inspection companion.
120 *
121 * @param model The model to generated
122 * @throws IOException From the Filer
123 */
124 public void generate(InspectableClassModel model) throws IOException {
125 generateFile(model).writeTo(mFiler);
126 }
127
128 /**
129 * Generate a {@link JavaFile} from a model.
130 *
131 * This is package-public for testing.
132 *
133 * @param model The model to generate from
134 * @return A generated file of an {@link android.view.inspector.InspectionCompanion}
135 */
136 JavaFile generateFile(InspectableClassModel model) {
137 return JavaFile
138 .builder(model.getClassName().packageName(), generateTypeSpec(model))
139 .indent(" ")
140 .build();
141 }
142
143 /**
144 * Generate a {@link TypeSpec} for the {@link android.view.inspector.InspectionCompanion}
145 * for the supplied model.
146 *
147 * @param model The model to generate from
148 * @return A TypeSpec of the inspection companion
149 */
150 private TypeSpec generateTypeSpec(InspectableClassModel model) {
Ashley Rose171a7232018-12-11 17:32:58 -0500151 final List<PropertyIdField> propertyIdFields = generatePropertyIdFields(model);
152
Ashley Rosede080eb2018-12-07 17:20:25 -0500153 TypeSpec.Builder builder = TypeSpec
154 .classBuilder(generateClassName(model))
155 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
156 .addSuperinterface(ParameterizedTypeName.get(
Ashley Rose171a7232018-12-11 17:32:58 -0500157 INSPECTION_COMPANION, model.getClassName()))
Ashley Rosede080eb2018-12-07 17:20:25 -0500158 .addJavadoc("Inspection companion for {@link $T}.\n\n", model.getClassName())
159 .addJavadoc("Generated by {@link $T}\n", getClass())
160 .addJavadoc("on behalf of {@link $T}.\n", mRequestingClass)
Ashley Rose171a7232018-12-11 17:32:58 -0500161 .addField(M_PROPERTIES_MAPPED);
162
163 for (PropertyIdField propertyIdField : propertyIdFields) {
164 builder.addField(propertyIdField.mFieldSpec);
165 }
166
167 builder.addMethod(generateMapProperties(propertyIdFields))
168 .addMethod(generateReadProperties(model, propertyIdFields));
Ashley Rosede080eb2018-12-07 17:20:25 -0500169
170 generateGetNodeName(model).ifPresent(builder::addMethod);
171
172 return builder.build();
173 }
174
175 /**
Ashley Rose171a7232018-12-11 17:32:58 -0500176 * Build a list of {@link PropertyIdField}'s for a model.
177 *
178 * To insure idempotency of the generated code, this method sorts the list of properties
179 * alphabetically by name.
180 *
181 * A {@link NameAllocator} is used to ensure that the field names are valid Java identifiers,
182 * and it prevents overlaps in names by suffixing them as needed.
183 *
184 * @param model The model to get properties from
185 * @return A list of properties and fields
186 */
187 private List<PropertyIdField> generatePropertyIdFields(InspectableClassModel model) {
188 final NameAllocator nameAllocator = new NameAllocator();
189 final List<Property> sortedProperties = new ArrayList<>(model.getAllProperties());
190 final List<PropertyIdField> propertyIdFields = new ArrayList<>(sortedProperties.size());
191
192 sortedProperties.sort(Comparator.comparing(Property::getName));
193
194 for (Property property : sortedProperties) {
195 // Format a property to a member field name like "someProperty" -> "mSomePropertyId"
196 final String memberName = String.format(
197 "m%s%sId",
198 property.getName().substring(0, 1).toUpperCase(),
199 property.getName().substring(1));
200 final FieldSpec fieldSpec = FieldSpec
201 .builder(TypeName.INT, nameAllocator.newName(memberName), Modifier.PRIVATE)
202 .addJavadoc("Property ID of {@code $L}.\n", property.getName())
203 .build();
204
205 propertyIdFields.add(new PropertyIdField(fieldSpec, property));
206 }
207
208 return propertyIdFields;
209 }
210
211 /**
Ashley Rosede080eb2018-12-07 17:20:25 -0500212 * Generate a method definition for
213 * {@link android.view.inspector.InspectionCompanion#getNodeName()}, if needed.
214 *
215 * If {@link InspectableClassModel#getNodeName()} is empty, This method returns an empty
216 * optional, otherwise, it generates a simple method that returns the string value of the
217 * node name.
218 *
219 * @param model The model to generate from
220 * @return The method definition or an empty Optional
221 */
222 private Optional<MethodSpec> generateGetNodeName(InspectableClassModel model) {
223 return model.getNodeName().map(nodeName -> MethodSpec.methodBuilder("getNodeName")
224 .addAnnotation(Override.class)
225 .addModifiers(Modifier.PUBLIC)
226 .returns(String.class)
227 .addStatement("return $S", nodeName)
228 .build());
229 }
230
231 /**
232 * Generate a method definition for
233 * {@link android.view.inspector.InspectionCompanion#mapProperties(
234 * android.view.inspector.PropertyMapper)}.
235 *
Ashley Rose171a7232018-12-11 17:32:58 -0500236 * @param propertyIdFields A list of properties to map to ID fields
Ashley Rosede080eb2018-12-07 17:20:25 -0500237 * @return The method definition
238 */
Ashley Rose171a7232018-12-11 17:32:58 -0500239 private MethodSpec generateMapProperties(List<PropertyIdField> propertyIdFields) {
240 final MethodSpec.Builder builder = MethodSpec.methodBuilder("mapProperties")
Ashley Rosede080eb2018-12-07 17:20:25 -0500241 .addAnnotation(Override.class)
242 .addModifiers(Modifier.PUBLIC)
Ashley Rose171a7232018-12-11 17:32:58 -0500243 .addParameter(PROPERTY_MAPPER, "propertyMapper");
244
245 propertyIdFields.forEach(p -> builder.addStatement(generatePropertyMapperInvocation(p)));
246 builder.addStatement("$N = true", M_PROPERTIES_MAPPED);
247
248 return builder.build();
Ashley Rosede080eb2018-12-07 17:20:25 -0500249 }
250
251 /**
252 * Generate a method definition for
253 * {@link android.view.inspector.InspectionCompanion#readProperties(
254 * Object, android.view.inspector.PropertyReader)}.
255 *
Ashley Rosede080eb2018-12-07 17:20:25 -0500256 * @param model The model to generate from
Ashley Rose171a7232018-12-11 17:32:58 -0500257 * @param propertyIdFields A list of properties and ID fields to read from
Ashley Rosede080eb2018-12-07 17:20:25 -0500258 * @return The method definition
259 */
Ashley Rose171a7232018-12-11 17:32:58 -0500260 private MethodSpec generateReadProperties(
261 InspectableClassModel model,
262 List<PropertyIdField> propertyIdFields) {
263 final MethodSpec.Builder builder = MethodSpec.methodBuilder("readProperties")
Ashley Rosede080eb2018-12-07 17:20:25 -0500264 .addAnnotation(Override.class)
265 .addModifiers(Modifier.PUBLIC)
Ashley Rose0b671da2019-01-25 15:41:29 -0500266 .addParameter(model.getClassName(), "node")
Ashley Rose171a7232018-12-11 17:32:58 -0500267 .addParameter(PROPERTY_READER, "propertyReader")
268 .addCode(generatePropertyMapInitializationCheck());
269
270 for (PropertyIdField propertyIdField : propertyIdFields) {
271 builder.addStatement(
Ashley Rose89d6bce2019-03-01 19:24:50 -0500272 "propertyReader.read$L($N, node.$L)",
Ashley Rose171a7232018-12-11 17:32:58 -0500273 methodSuffixForPropertyType(propertyIdField.mProperty.getType()),
274 propertyIdField.mFieldSpec,
Ashley Rose89d6bce2019-03-01 19:24:50 -0500275 propertyIdField.mProperty.getAccessor().invocation());
Ashley Rose171a7232018-12-11 17:32:58 -0500276 }
277
278 return builder.build();
279 }
280
281 /**
282 * Generate a statement maps a property with a {@link android.view.inspector.PropertyMapper}.
283 *
284 * @param propertyIdField The property model and ID field to generate from
285 * @return A statement that invokes property mapper method
286 */
287 private CodeBlock generatePropertyMapperInvocation(PropertyIdField propertyIdField) {
288 final CodeBlock.Builder builder = CodeBlock.builder();
289 final Property property = propertyIdField.mProperty;
290 final FieldSpec fieldSpec = propertyIdField.mFieldSpec;
291
292 builder.add(
293 "$N = propertyMapper.map$L($S,$W",
294 fieldSpec,
295 methodSuffixForPropertyType(property.getType()),
296 property.getName());
297
298 if (property.isAttributeIdInferrableFromR()) {
299 builder.add("$T.attr.$L", R_CLASS_NAME, property.getName());
300 } else {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500301 if (property.getAttributeId() == ID_NULL) {
302 builder.add("$L", ID_NULL);
Ashley Rose171a7232018-12-11 17:32:58 -0500303 } else {
Ashley Rose0b671da2019-01-25 15:41:29 -0500304 builder.add("$L", hexLiteral(property.getAttributeId()));
Ashley Rose171a7232018-12-11 17:32:58 -0500305 }
306 }
307
308 switch (property.getType()) {
309 case INT_ENUM:
Ashley Rose0b671da2019-01-25 15:41:29 -0500310 builder.add(",$W");
311 builder.add(generateIntEnumMappingBuilder(property.getIntEnumEntries()));
312 break;
Ashley Rose171a7232018-12-11 17:32:58 -0500313 case INT_FLAG:
Ashley Rose0b671da2019-01-25 15:41:29 -0500314 builder.add(",$W");
315 builder.add(generateIntFlagMappingBuilder(property.getIntFlagEntries()));
Ashley Rose171a7232018-12-11 17:32:58 -0500316 break;
317 }
318
Ashley Rose0b671da2019-01-25 15:41:29 -0500319 return builder.add(")").build();
Ashley Rose171a7232018-12-11 17:32:58 -0500320 }
321
322 /**
323 * Generate a check that throws
324 * {@link android.view.inspector.InspectionCompanion.UninitializedPropertyMapException}
325 * if the properties haven't been initialized.
326 *
327 * <pre>
328 * if (!mPropertiesMapped) {
329 * throw new InspectionCompanion.UninitializedPropertyMapException();
330 * }
331 * </pre>
332 *
333 * @return A codeblock containing the property map initialization check
334 */
335 private CodeBlock generatePropertyMapInitializationCheck() {
336 return CodeBlock.builder()
337 .beginControlFlow("if (!$N)", M_PROPERTIES_MAPPED)
338 .addStatement(
339 "throw new $T()",
340 INSPECTION_COMPANION.nestedClass("UninitializedPropertyMapException"))
341 .endControlFlow()
Ashley Rosede080eb2018-12-07 17:20:25 -0500342 .build();
343 }
344
345 /**
Ashley Rose0b671da2019-01-25 15:41:29 -0500346 * Generate an invocation of {@link android.view.inspector.IntEnumMapping.Builder}.
347 *
348 * <pre>
349 * new IntEnumMapping.Builder()
350 * .addValue("ONE", 1)
351 * .build()
352 * </pre>
353 *
354 * @return A codeblock containing the an int enum mapping builder
355 */
356 private CodeBlock generateIntEnumMappingBuilder(List<IntEnumEntry> intEnumEntries) {
357 final ArrayList<IntEnumEntry> sortedEntries = new ArrayList<>(intEnumEntries);
358 sortedEntries.sort(Comparator.comparing(IntEnumEntry::getValue));
359
360 final CodeBlock.Builder builder = CodeBlock.builder()
361 .add("new $T()$>", INT_ENUM_MAPPING.nestedClass("Builder"));
362
363 for (IntEnumEntry entry : sortedEntries) {
364 builder.add("\n.addValue($S, $L)", entry.getName(), entry.getValue());
365 }
366
367 return builder.add("\n.build()$<").build();
368 }
369
370 private CodeBlock generateIntFlagMappingBuilder(List<IntFlagEntry> intFlagEntries) {
371 final ArrayList<IntFlagEntry> sortedEntries = new ArrayList<>(intFlagEntries);
372 sortedEntries.sort(Comparator.comparing(IntFlagEntry::getName));
373
374 final CodeBlock.Builder builder = CodeBlock.builder()
375 .add("new $T()$>", INT_FLAG_MAPPING.nestedClass("Builder"));
376
377 for (IntFlagEntry entry : sortedEntries) {
378 if (entry.hasMask()) {
379 builder.add(
380 "\n.addFlag($S, $L, $L)",
381 entry.getName(),
382 hexLiteral(entry.getTarget()),
383 hexLiteral(entry.getMask()));
384 } else {
385 builder.add(
386 "\n.addFlag($S, $L)",
387 entry.getName(),
388 hexLiteral(entry.getTarget()));
389 }
390 }
391
392 return builder.add("\n.build()$<").build();
393 }
394
395 /**
Ashley Rosede080eb2018-12-07 17:20:25 -0500396 * Generate the final class name for the inspection companion from the model's class name.
397 *
398 * The generated class is added to the same package as the source class. If the class in the
Ashley Rose171a7232018-12-11 17:32:58 -0500399 * model is a nested class, the nested class names are joined with {@code "$"}. The suffix
400 * {@code "$$InspectionCompanion"} is always added the the generated name. E.g.: For modeled
401 * class {@code com.example.Outer.Inner}, the generated class name will be
402 * {@code com.example.Outer$Inner$$InspectionCompanion}.
Ashley Rosede080eb2018-12-07 17:20:25 -0500403 *
404 * @param model The model to generate from
405 * @return A class name for the generated inspection companion class
406 */
Ashley Rose171a7232018-12-11 17:32:58 -0500407 private static ClassName generateClassName(InspectableClassModel model) {
Ashley Rosede080eb2018-12-07 17:20:25 -0500408 final ClassName className = model.getClassName();
409
410 return ClassName.get(
411 className.packageName(),
Ashley Rose171a7232018-12-11 17:32:58 -0500412 String.join("$", className.simpleNames()) + GENERATED_CLASS_SUFFIX);
413 }
414
415 /**
416 * Get the suffix for a {@code map} or {@code read} method for a property type.
417 *
418 * @param type The requested property type
419 * @return A method suffix
420 */
421 private static String methodSuffixForPropertyType(Property.Type type) {
422 switch (type) {
423 case BOOLEAN:
424 return "Boolean";
425 case BYTE:
426 return "Byte";
427 case CHAR:
428 return "Char";
429 case DOUBLE:
430 return "Double";
431 case FLOAT:
432 return "Float";
433 case INT:
434 return "Int";
435 case LONG:
436 return "Long";
437 case SHORT:
438 return "Short";
439 case OBJECT:
440 return "Object";
441 case COLOR:
442 return "Color";
443 case GRAVITY:
444 return "Gravity";
445 case INT_ENUM:
446 return "IntEnum";
447 case INT_FLAG:
448 return "IntFlag";
Ashley Rosee8914812019-03-05 17:12:00 -0500449 case RESOURCE_ID:
450 return "ResourceId";
Ashley Rose171a7232018-12-11 17:32:58 -0500451 default:
452 throw new NoSuchElementException(String.format("No such property type, %s", type));
453 }
454 }
455
Ashley Rose0b671da2019-01-25 15:41:29 -0500456 private static String hexLiteral(int value) {
457 return String.format("0x%08x", value);
458 }
459
Ashley Rose171a7232018-12-11 17:32:58 -0500460 /**
461 * Value class that holds a {@link Property} and a {@link FieldSpec} for that property.
462 */
463 private static final class PropertyIdField {
464 private final FieldSpec mFieldSpec;
465 private final Property mProperty;
466
467 private PropertyIdField(FieldSpec fieldSpec, Property property) {
468 mFieldSpec = fieldSpec;
469 mProperty = property;
470 }
Ashley Rosede080eb2018-12-07 17:20:25 -0500471 }
472}