• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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