blob: 7b04645e9f44a734cf24c40aa6f421a93d796a20 [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 Rose0b671da2019-01-25 15:41:29 -050019import android.processor.view.inspector.InspectableClassModel.IntEnumEntry;
20import android.processor.view.inspector.InspectableClassModel.IntFlagEntry;
Ashley Rose171a7232018-12-11 17:32:58 -050021import android.processor.view.inspector.InspectableClassModel.Property;
22
Ashley Rosede080eb2018-12-07 17:20:25 -050023import com.squareup.javapoet.ClassName;
Ashley Rose171a7232018-12-11 17:32:58 -050024import com.squareup.javapoet.CodeBlock;
25import com.squareup.javapoet.FieldSpec;
Ashley Rosede080eb2018-12-07 17:20:25 -050026import com.squareup.javapoet.JavaFile;
27import com.squareup.javapoet.MethodSpec;
Ashley Rose171a7232018-12-11 17:32:58 -050028import com.squareup.javapoet.NameAllocator;
Ashley Rosede080eb2018-12-07 17:20:25 -050029import com.squareup.javapoet.ParameterizedTypeName;
Ashley Rose171a7232018-12-11 17:32:58 -050030import com.squareup.javapoet.TypeName;
Ashley Rosede080eb2018-12-07 17:20:25 -050031import com.squareup.javapoet.TypeSpec;
32
33import java.io.IOException;
Ashley Rose171a7232018-12-11 17:32:58 -050034import java.util.ArrayList;
35import java.util.Comparator;
36import java.util.List;
37import java.util.NoSuchElementException;
Ashley Rosede080eb2018-12-07 17:20:25 -050038import java.util.Optional;
39
40import javax.annotation.processing.Filer;
41import javax.lang.model.element.Modifier;
42
43/**
44 * Generates a source file defining a {@link android.view.inspector.InspectionCompanion}.
45 */
46public final class InspectionCompanionGenerator {
47 private final Filer mFiler;
48 private final Class mRequestingClass;
49
50 /**
Ashley Rose171a7232018-12-11 17:32:58 -050051 * The class name for {@code R.java}.
52 */
53 private static final ClassName R_CLASS_NAME = ClassName.get("android", "R");
54
55 /**
Ashley Rose171a7232018-12-11 17:32:58 -050056 * The class name of {@link android.view.inspector.InspectionCompanion}.
57 */
58 private static final ClassName INSPECTION_COMPANION = ClassName.get(
59 "android.view.inspector", "InspectionCompanion");
60
61 /**
62 * The class name of {@link android.view.inspector.PropertyMapper}.
63 */
64 private static final ClassName PROPERTY_MAPPER = ClassName.get(
65 "android.view.inspector", "PropertyMapper");
66
67 /**
68 * The class name of {@link android.view.inspector.PropertyReader}.
69 */
70 private static final ClassName PROPERTY_READER = ClassName.get(
71 "android.view.inspector", "PropertyReader");
72
73 /**
Ashley Rose0b671da2019-01-25 15:41:29 -050074 * The class name of {@link android.view.inspector.IntEnumMapping}.
75 */
76 private static final ClassName INT_ENUM_MAPPING = ClassName.get(
77 "android.view.inspector", "IntEnumMapping");
78
79 /**
80 * The class name of {@link android.view.inspector.IntFlagMapping}.
81 */
82 private static final ClassName INT_FLAG_MAPPING = ClassName.get(
83 "android.view.inspector", "IntFlagMapping");
84
85 /**
Ashley Rose171a7232018-12-11 17:32:58 -050086 * The {@code mPropertiesMapped} field.
87 */
88 private static final FieldSpec M_PROPERTIES_MAPPED = FieldSpec
89 .builder(TypeName.BOOLEAN, "mPropertiesMapped", Modifier.PRIVATE)
90 .initializer("false")
91 .addJavadoc(
92 "Set by {@link #mapProperties($T)} once properties have been mapped.\n",
93 PROPERTY_MAPPER)
94 .build();
95
96 /**
97 * The suffix of the generated class name after the class's binary name.
98 */
99 private static final String GENERATED_CLASS_SUFFIX = "$$InspectionCompanion";
100
101 /**
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500102 * The null resource ID, copied to avoid a host dependency on platform code.
Ashley Rose171a7232018-12-11 17:32:58 -0500103 *
Aurimas Liutikasd8ebfef2019-01-16 12:46:42 -0800104 * @see android.content.res.Resources#ID_NULL
Ashley Rose171a7232018-12-11 17:32:58 -0500105 */
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500106 private static final int ID_NULL = 0;
Ashley Rose171a7232018-12-11 17:32:58 -0500107
108 /**
Ashley Rosede080eb2018-12-07 17:20:25 -0500109 * @param filer A filer to write the generated source to
110 * @param requestingClass A class object representing the class that invoked the generator
111 */
Ashley Rose171a7232018-12-11 17:32:58 -0500112 public InspectionCompanionGenerator(Filer filer, Class requestingClass) {
Ashley Rosede080eb2018-12-07 17:20:25 -0500113 mFiler = filer;
114 mRequestingClass = requestingClass;
115 }
116
117 /**
118 * Generate and write an inspection companion.
119 *
120 * @param model The model to generated
121 * @throws IOException From the Filer
122 */
123 public void generate(InspectableClassModel model) throws IOException {
124 generateFile(model).writeTo(mFiler);
125 }
126
127 /**
128 * Generate a {@link JavaFile} from a model.
129 *
130 * This is package-public for testing.
131 *
132 * @param model The model to generate from
133 * @return A generated file of an {@link android.view.inspector.InspectionCompanion}
134 */
135 JavaFile generateFile(InspectableClassModel model) {
136 return JavaFile
137 .builder(model.getClassName().packageName(), generateTypeSpec(model))
138 .indent(" ")
139 .build();
140 }
141
142 /**
143 * Generate a {@link TypeSpec} for the {@link android.view.inspector.InspectionCompanion}
144 * for the supplied model.
145 *
146 * @param model The model to generate from
147 * @return A TypeSpec of the inspection companion
148 */
149 private TypeSpec generateTypeSpec(InspectableClassModel model) {
Ashley Rose171a7232018-12-11 17:32:58 -0500150 final List<PropertyIdField> propertyIdFields = generatePropertyIdFields(model);
151
Ashley Rosede080eb2018-12-07 17:20:25 -0500152 TypeSpec.Builder builder = TypeSpec
153 .classBuilder(generateClassName(model))
154 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
155 .addSuperinterface(ParameterizedTypeName.get(
Ashley Rose171a7232018-12-11 17:32:58 -0500156 INSPECTION_COMPANION, model.getClassName()))
Ashley Rosede080eb2018-12-07 17:20:25 -0500157 .addJavadoc("Inspection companion for {@link $T}.\n\n", model.getClassName())
158 .addJavadoc("Generated by {@link $T}\n", getClass())
159 .addJavadoc("on behalf of {@link $T}.\n", mRequestingClass)
Ashley Rose171a7232018-12-11 17:32:58 -0500160 .addField(M_PROPERTIES_MAPPED);
161
162 for (PropertyIdField propertyIdField : propertyIdFields) {
163 builder.addField(propertyIdField.mFieldSpec);
164 }
165
166 builder.addMethod(generateMapProperties(propertyIdFields))
167 .addMethod(generateReadProperties(model, propertyIdFields));
Ashley Rosede080eb2018-12-07 17:20:25 -0500168
169 generateGetNodeName(model).ifPresent(builder::addMethod);
170
171 return builder.build();
172 }
173
174 /**
Ashley Rose171a7232018-12-11 17:32:58 -0500175 * Build a list of {@link PropertyIdField}'s for a model.
176 *
177 * To insure idempotency of the generated code, this method sorts the list of properties
178 * alphabetically by name.
179 *
180 * A {@link NameAllocator} is used to ensure that the field names are valid Java identifiers,
181 * and it prevents overlaps in names by suffixing them as needed.
182 *
183 * @param model The model to get properties from
184 * @return A list of properties and fields
185 */
186 private List<PropertyIdField> generatePropertyIdFields(InspectableClassModel model) {
187 final NameAllocator nameAllocator = new NameAllocator();
188 final List<Property> sortedProperties = new ArrayList<>(model.getAllProperties());
189 final List<PropertyIdField> propertyIdFields = new ArrayList<>(sortedProperties.size());
190
191 sortedProperties.sort(Comparator.comparing(Property::getName));
192
193 for (Property property : sortedProperties) {
194 // Format a property to a member field name like "someProperty" -> "mSomePropertyId"
195 final String memberName = String.format(
196 "m%s%sId",
197 property.getName().substring(0, 1).toUpperCase(),
198 property.getName().substring(1));
199 final FieldSpec fieldSpec = FieldSpec
200 .builder(TypeName.INT, nameAllocator.newName(memberName), Modifier.PRIVATE)
201 .addJavadoc("Property ID of {@code $L}.\n", property.getName())
202 .build();
203
204 propertyIdFields.add(new PropertyIdField(fieldSpec, property));
205 }
206
207 return propertyIdFields;
208 }
209
210 /**
Ashley Rosede080eb2018-12-07 17:20:25 -0500211 * Generate a method definition for
212 * {@link android.view.inspector.InspectionCompanion#getNodeName()}, if needed.
213 *
214 * If {@link InspectableClassModel#getNodeName()} is empty, This method returns an empty
215 * optional, otherwise, it generates a simple method that returns the string value of the
216 * node name.
217 *
218 * @param model The model to generate from
219 * @return The method definition or an empty Optional
220 */
221 private Optional<MethodSpec> generateGetNodeName(InspectableClassModel model) {
222 return model.getNodeName().map(nodeName -> MethodSpec.methodBuilder("getNodeName")
223 .addAnnotation(Override.class)
224 .addModifiers(Modifier.PUBLIC)
225 .returns(String.class)
226 .addStatement("return $S", nodeName)
227 .build());
228 }
229
230 /**
231 * Generate a method definition for
232 * {@link android.view.inspector.InspectionCompanion#mapProperties(
233 * android.view.inspector.PropertyMapper)}.
234 *
Ashley Rose171a7232018-12-11 17:32:58 -0500235 * @param propertyIdFields A list of properties to map to ID fields
Ashley Rosede080eb2018-12-07 17:20:25 -0500236 * @return The method definition
237 */
Ashley Rose171a7232018-12-11 17:32:58 -0500238 private MethodSpec generateMapProperties(List<PropertyIdField> propertyIdFields) {
239 final MethodSpec.Builder builder = MethodSpec.methodBuilder("mapProperties")
Ashley Rosede080eb2018-12-07 17:20:25 -0500240 .addAnnotation(Override.class)
241 .addModifiers(Modifier.PUBLIC)
Ashley Rose171a7232018-12-11 17:32:58 -0500242 .addParameter(PROPERTY_MAPPER, "propertyMapper");
243
244 propertyIdFields.forEach(p -> builder.addStatement(generatePropertyMapperInvocation(p)));
245 builder.addStatement("$N = true", M_PROPERTIES_MAPPED);
246
247 return builder.build();
Ashley Rosede080eb2018-12-07 17:20:25 -0500248 }
249
250 /**
251 * Generate a method definition for
252 * {@link android.view.inspector.InspectionCompanion#readProperties(
253 * Object, android.view.inspector.PropertyReader)}.
254 *
Ashley Rosede080eb2018-12-07 17:20:25 -0500255 * @param model The model to generate from
Ashley Rose171a7232018-12-11 17:32:58 -0500256 * @param propertyIdFields A list of properties and ID fields to read from
Ashley Rosede080eb2018-12-07 17:20:25 -0500257 * @return The method definition
258 */
Ashley Rose171a7232018-12-11 17:32:58 -0500259 private MethodSpec generateReadProperties(
260 InspectableClassModel model,
261 List<PropertyIdField> propertyIdFields) {
262 final MethodSpec.Builder builder = MethodSpec.methodBuilder("readProperties")
Ashley Rosede080eb2018-12-07 17:20:25 -0500263 .addAnnotation(Override.class)
264 .addModifiers(Modifier.PUBLIC)
Ashley Rose0b671da2019-01-25 15:41:29 -0500265 .addParameter(model.getClassName(), "node")
Ashley Rose171a7232018-12-11 17:32:58 -0500266 .addParameter(PROPERTY_READER, "propertyReader")
267 .addCode(generatePropertyMapInitializationCheck());
268
269 for (PropertyIdField propertyIdField : propertyIdFields) {
270 builder.addStatement(
Ashley Rose0b671da2019-01-25 15:41:29 -0500271 "propertyReader.read$L($N, node.$L())",
Ashley Rose171a7232018-12-11 17:32:58 -0500272 methodSuffixForPropertyType(propertyIdField.mProperty.getType()),
273 propertyIdField.mFieldSpec,
274 propertyIdField.mProperty.getGetter());
275 }
276
277 return builder.build();
278 }
279
280 /**
281 * Generate a statement maps a property with a {@link android.view.inspector.PropertyMapper}.
282 *
283 * @param propertyIdField The property model and ID field to generate from
284 * @return A statement that invokes property mapper method
285 */
286 private CodeBlock generatePropertyMapperInvocation(PropertyIdField propertyIdField) {
287 final CodeBlock.Builder builder = CodeBlock.builder();
288 final Property property = propertyIdField.mProperty;
289 final FieldSpec fieldSpec = propertyIdField.mFieldSpec;
290
291 builder.add(
292 "$N = propertyMapper.map$L($S,$W",
293 fieldSpec,
294 methodSuffixForPropertyType(property.getType()),
295 property.getName());
296
297 if (property.isAttributeIdInferrableFromR()) {
298 builder.add("$T.attr.$L", R_CLASS_NAME, property.getName());
299 } else {
Ashley Rosec1a4dec2018-12-13 18:06:30 -0500300 if (property.getAttributeId() == ID_NULL) {
301 builder.add("$L", ID_NULL);
Ashley Rose171a7232018-12-11 17:32:58 -0500302 } else {
Ashley Rose0b671da2019-01-25 15:41:29 -0500303 builder.add("$L", hexLiteral(property.getAttributeId()));
Ashley Rose171a7232018-12-11 17:32:58 -0500304 }
305 }
306
307 switch (property.getType()) {
308 case INT_ENUM:
Ashley Rose0b671da2019-01-25 15:41:29 -0500309 builder.add(",$W");
310 builder.add(generateIntEnumMappingBuilder(property.getIntEnumEntries()));
311 break;
Ashley Rose171a7232018-12-11 17:32:58 -0500312 case INT_FLAG:
Ashley Rose0b671da2019-01-25 15:41:29 -0500313 builder.add(",$W");
314 builder.add(generateIntFlagMappingBuilder(property.getIntFlagEntries()));
Ashley Rose171a7232018-12-11 17:32:58 -0500315 break;
316 }
317
Ashley Rose0b671da2019-01-25 15:41:29 -0500318 return builder.add(")").build();
Ashley Rose171a7232018-12-11 17:32:58 -0500319 }
320
321 /**
322 * Generate a check that throws
323 * {@link android.view.inspector.InspectionCompanion.UninitializedPropertyMapException}
324 * if the properties haven't been initialized.
325 *
326 * <pre>
327 * if (!mPropertiesMapped) {
328 * throw new InspectionCompanion.UninitializedPropertyMapException();
329 * }
330 * </pre>
331 *
332 * @return A codeblock containing the property map initialization check
333 */
334 private CodeBlock generatePropertyMapInitializationCheck() {
335 return CodeBlock.builder()
336 .beginControlFlow("if (!$N)", M_PROPERTIES_MAPPED)
337 .addStatement(
338 "throw new $T()",
339 INSPECTION_COMPANION.nestedClass("UninitializedPropertyMapException"))
340 .endControlFlow()
Ashley Rosede080eb2018-12-07 17:20:25 -0500341 .build();
342 }
343
344 /**
Ashley Rose0b671da2019-01-25 15:41:29 -0500345 * Generate an invocation of {@link android.view.inspector.IntEnumMapping.Builder}.
346 *
347 * <pre>
348 * new IntEnumMapping.Builder()
349 * .addValue("ONE", 1)
350 * .build()
351 * </pre>
352 *
353 * @return A codeblock containing the an int enum mapping builder
354 */
355 private CodeBlock generateIntEnumMappingBuilder(List<IntEnumEntry> intEnumEntries) {
356 final ArrayList<IntEnumEntry> sortedEntries = new ArrayList<>(intEnumEntries);
357 sortedEntries.sort(Comparator.comparing(IntEnumEntry::getValue));
358
359 final CodeBlock.Builder builder = CodeBlock.builder()
360 .add("new $T()$>", INT_ENUM_MAPPING.nestedClass("Builder"));
361
362 for (IntEnumEntry entry : sortedEntries) {
363 builder.add("\n.addValue($S, $L)", entry.getName(), entry.getValue());
364 }
365
366 return builder.add("\n.build()$<").build();
367 }
368
369 private CodeBlock generateIntFlagMappingBuilder(List<IntFlagEntry> intFlagEntries) {
370 final ArrayList<IntFlagEntry> sortedEntries = new ArrayList<>(intFlagEntries);
371 sortedEntries.sort(Comparator.comparing(IntFlagEntry::getName));
372
373 final CodeBlock.Builder builder = CodeBlock.builder()
374 .add("new $T()$>", INT_FLAG_MAPPING.nestedClass("Builder"));
375
376 for (IntFlagEntry entry : sortedEntries) {
377 if (entry.hasMask()) {
378 builder.add(
379 "\n.addFlag($S, $L, $L)",
380 entry.getName(),
381 hexLiteral(entry.getTarget()),
382 hexLiteral(entry.getMask()));
383 } else {
384 builder.add(
385 "\n.addFlag($S, $L)",
386 entry.getName(),
387 hexLiteral(entry.getTarget()));
388 }
389 }
390
391 return builder.add("\n.build()$<").build();
392 }
393
394 /**
Ashley Rosede080eb2018-12-07 17:20:25 -0500395 * Generate the final class name for the inspection companion from the model's class name.
396 *
397 * 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 -0500398 * model is a nested class, the nested class names are joined with {@code "$"}. The suffix
399 * {@code "$$InspectionCompanion"} is always added the the generated name. E.g.: For modeled
400 * class {@code com.example.Outer.Inner}, the generated class name will be
401 * {@code com.example.Outer$Inner$$InspectionCompanion}.
Ashley Rosede080eb2018-12-07 17:20:25 -0500402 *
403 * @param model The model to generate from
404 * @return A class name for the generated inspection companion class
405 */
Ashley Rose171a7232018-12-11 17:32:58 -0500406 private static ClassName generateClassName(InspectableClassModel model) {
Ashley Rosede080eb2018-12-07 17:20:25 -0500407 final ClassName className = model.getClassName();
408
409 return ClassName.get(
410 className.packageName(),
Ashley Rose171a7232018-12-11 17:32:58 -0500411 String.join("$", className.simpleNames()) + GENERATED_CLASS_SUFFIX);
412 }
413
414 /**
415 * Get the suffix for a {@code map} or {@code read} method for a property type.
416 *
417 * @param type The requested property type
418 * @return A method suffix
419 */
420 private static String methodSuffixForPropertyType(Property.Type type) {
421 switch (type) {
422 case BOOLEAN:
423 return "Boolean";
424 case BYTE:
425 return "Byte";
426 case CHAR:
427 return "Char";
428 case DOUBLE:
429 return "Double";
430 case FLOAT:
431 return "Float";
432 case INT:
433 return "Int";
434 case LONG:
435 return "Long";
436 case SHORT:
437 return "Short";
438 case OBJECT:
439 return "Object";
440 case COLOR:
441 return "Color";
442 case GRAVITY:
443 return "Gravity";
444 case INT_ENUM:
445 return "IntEnum";
446 case INT_FLAG:
447 return "IntFlag";
448 default:
449 throw new NoSuchElementException(String.format("No such property type, %s", type));
450 }
451 }
452
Ashley Rose0b671da2019-01-25 15:41:29 -0500453 private static String hexLiteral(int value) {
454 return String.format("0x%08x", value);
455 }
456
Ashley Rose171a7232018-12-11 17:32:58 -0500457 /**
458 * Value class that holds a {@link Property} and a {@link FieldSpec} for that property.
459 */
460 private static final class PropertyIdField {
461 private final FieldSpec mFieldSpec;
462 private final Property mProperty;
463
464 private PropertyIdField(FieldSpec fieldSpec, Property property) {
465 mFieldSpec = fieldSpec;
466 mProperty = property;
467 }
Ashley Rosede080eb2018-12-07 17:20:25 -0500468 }
469}