1 /*
2  * Copyright (C) 2024 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 com.android.car.tool;
18 
19 import com.github.javaparser.StaticJavaParser;
20 import com.github.javaparser.ast.CompilationUnit;
21 import com.github.javaparser.ast.body.AnnotationDeclaration;
22 import com.github.javaparser.ast.body.FieldDeclaration;
23 import com.github.javaparser.ast.body.VariableDeclarator;
24 import com.github.javaparser.ast.comments.Comment;
25 import com.github.javaparser.ast.expr.AnnotationExpr;
26 import com.github.javaparser.ast.expr.ArrayInitializerExpr;
27 import com.github.javaparser.ast.expr.Expression;
28 import com.github.javaparser.ast.expr.NormalAnnotationExpr;
29 import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
30 import com.github.javaparser.ast.expr.UnaryExpr;
31 import com.github.javaparser.ast.type.ClassOrInterfaceType;
32 import com.github.javaparser.javadoc.Javadoc;
33 import com.github.javaparser.javadoc.JavadocBlockTag;
34 import com.github.javaparser.javadoc.description.JavadocDescription;
35 import com.github.javaparser.javadoc.description.JavadocDescriptionElement;
36 import com.github.javaparser.javadoc.description.JavadocInlineTag;
37 import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
38 import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
39 import com.github.javaparser.symbolsolver.JavaSymbolSolver;
40 import com.github.javaparser.symbolsolver.javaparsermodel.declarations.JavaParserFieldDeclaration;
41 import com.github.javaparser.symbolsolver.model.resolution.TypeSolver;
42 import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
43 import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
44 import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
45 import java.io.BufferedReader;
46 import java.io.File;
47 import java.io.FileOutputStream;
48 import java.io.FileReader;
49 import java.lang.reflect.Field;
50 import java.text.Collator;
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.HashSet;
54 import java.util.LinkedHashMap;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Optional;
58 import java.util.Set;
59 import org.json.JSONArray;
60 import org.json.JSONObject;
61 
62 public final class EmuMetadataGenerator {
63     private static final String DEFAULT_PACKAGE_NAME = "android.hardware.automotive.vehicle";
64     private static final String INPUT_DIR_OPTION = "--input_dir";
65     private static final String INPUT_FILES_OPTION = "--input_files";
66     private static final String PACKAGE_NAME_OPTION = "--package_name";
67     private static final String OUTPUT_JSON_OPTION = "--output_json";
68     private static final String OUTPUT_EMPTY_FILE_OPTION = "--output_empty_file";
69     private static final String CHECK_AGAINST_OPTION = "--check_against";
70     private static final String USAGE = "EnumMetadataGenerator " + INPUT_DIR_OPTION
71             + " [path_to_aidl_gen_dir] " + INPUT_FILES_OPTION + " [input_files] "
72             + PACKAGE_NAME_OPTION + " [package_name] " + OUTPUT_JSON_OPTION + " [output_json] "
73             + OUTPUT_EMPTY_FILE_OPTION + " [output_header_file] " + CHECK_AGAINST_OPTION
74             + " [json_file_to_check_against]\n"
75             + "Parses the VHAL property AIDL interface generated Java files to a json file to be"
76             + " used by emulator\n"
77             + "Options: \n" + INPUT_DIR_OPTION
78             + ": the path to a directory containing AIDL interface Java files, "
79             + "either this or input_files must be specified\n" + INPUT_FILES_OPTION
80             + ": one or more Java files, this is used to decide the input "
81             + "directory\n" + PACKAGE_NAME_OPTION
82             + ": the optional package name for the interface, by default is "
83             + DEFAULT_PACKAGE_NAME + "\n" + OUTPUT_JSON_OPTION + ": The output JSON file\n"
84             + OUTPUT_EMPTY_FILE_OPTION + ": Only used for check_mode, this file will be created if "
85             + "check  passed\n" + CHECK_AGAINST_OPTION
86             + ": An optional JSON file to check against. If specified, the "
87             + ("generated output file will be checked against this file, if they are not the "
88                     + "same, ")
89             + "the script will fail, otherwise, the output_empty_file will be created\n"
90             + "For example: \n"
91             + "EnumMetadataGenerator --input_dir out/soong/.intermediates/hardware/"
92             + "interfaces/automotive/vehicle/aidl_property/android.hardware.automotive.vehicle."
93             + "property-V4-java-source/gen/ --package_name android.hardware.automotive.vehicle "
94             + "--output_json /tmp/android.hardware.automotive.vehicle-types-meta.json";
95     private static final String VEHICLE_PROPERTY_FILE = "VehicleProperty.java";
96     private static final String CHECK_FILE_PATH =
97             "${ANDROID_BUILD_TOP}/hardware/interfaces/automotive/vehicle/aidl/emu_metadata/"
98             + "android.hardware.automotive.vehicle-types-meta.json";
99     private static final List<String> ANNOTATIONS =
100             List.of("@change_mode", "@access", "@version", "@data_enum", "@unit");
101 
102     // Emulator can display at least this many characters before cutting characters.
103     private static final int MAX_PROPERTY_NAME_LENGTH = 30;
104 
105     /**
106      * Parses the enum field declaration as an int value.
107      */
parseIntEnumField(FieldDeclaration fieldDecl)108     private static int parseIntEnumField(FieldDeclaration fieldDecl) {
109         VariableDeclarator valueDecl = fieldDecl.getVariables().get(0);
110         Expression expr = valueDecl.getInitializer().get();
111         if (expr.isIntegerLiteralExpr()) {
112             return expr.asIntegerLiteralExpr().asInt();
113         }
114         // For case like -123
115         if (expr.isUnaryExpr() && expr.asUnaryExpr().getOperator() == UnaryExpr.Operator.MINUS) {
116             return -expr.asUnaryExpr().getExpression().asIntegerLiteralExpr().asInt();
117         }
118         System.out.println("Unsupported expression: " + expr);
119         System.exit(1);
120         return 0;
121     }
122 
isPublicAndStatic(FieldDeclaration fieldDecl)123     private static boolean isPublicAndStatic(FieldDeclaration fieldDecl) {
124         return fieldDecl.isPublic() && fieldDecl.isStatic();
125     }
126 
getFieldName(FieldDeclaration fieldDecl)127     private static String getFieldName(FieldDeclaration fieldDecl) {
128         VariableDeclarator valueDecl = fieldDecl.getVariables().get(0);
129         return valueDecl.getName().asString();
130     }
131 
132     private static class Enum {
Enum(String name, String packageName)133         Enum(String name, String packageName) {
134             this.name = name;
135             this.packageName = packageName;
136         }
137 
138         public String name;
139         public String packageName;
140         public final List<ValueField> valueFields = new ArrayList<>();
141     }
142 
143     private static class ValueField {
144         public String name;
145         public Integer value;
146         public final List<String> dataEnums = new ArrayList<>();
147         public String description = "";
148 
ValueField(String name, Integer value)149         ValueField(String name, Integer value) {
150             this.name = name;
151             this.value = value;
152         }
153     }
154 
parseEnumInterface( String inputDir, String dirName, String packageName, String enumName)155     private static Enum parseEnumInterface(
156             String inputDir, String dirName, String packageName, String enumName) throws Exception {
157         Enum enumIntf = new Enum(enumName, packageName);
158         CompilationUnit cu = StaticJavaParser.parse(new File(
159                 inputDir + File.separator + dirName + File.separator + enumName + ".java"));
160         AnnotationDeclaration vehiclePropertyIdsClass =
161                 cu.getAnnotationDeclarationByName(enumName).get();
162 
163         List<FieldDeclaration> variables = vehiclePropertyIdsClass.findAll(FieldDeclaration.class);
164         for (int i = 0; i < variables.size(); i++) {
165             FieldDeclaration propertyDef = variables.get(i).asFieldDeclaration();
166             if (!isPublicAndStatic(propertyDef)) {
167                 continue;
168             }
169             ValueField field =
170                     new ValueField(getFieldName(propertyDef), parseIntEnumField(propertyDef));
171             enumIntf.valueFields.add(field);
172         }
173         return enumIntf;
174     }
175 
176     // A hacky way to make the key in-order in the JSON object.
177     private static final class OrderedJSONObject extends JSONObject {
OrderedJSONObject()178         OrderedJSONObject() {
179             try {
180                 Field map = JSONObject.class.getDeclaredField("nameValuePairs");
181                 map.setAccessible(true);
182                 map.set(this, new LinkedHashMap<>());
183                 map.setAccessible(false);
184             } catch (IllegalAccessException | NoSuchFieldException e) {
185                 throw new RuntimeException(e);
186             }
187         }
188     }
189 
readFileContent(String fileName)190     private static String readFileContent(String fileName) throws Exception {
191         StringBuffer contentBuffer = new StringBuffer();
192         int bufferSize = 1024;
193         try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
194             char buffer[] = new char[bufferSize];
195             while (true) {
196                 int read = reader.read(buffer, 0, bufferSize);
197                 if (read == -1) {
198                     break;
199                 }
200                 contentBuffer.append(buffer, 0, read);
201             }
202         }
203         return contentBuffer.toString();
204     }
205 
206     private static final class Args {
207         public final String inputDir;
208         public final String pkgName;
209         public final String pkgDir;
210         public final String output;
211         public final String checkFile;
212         public final String outputEmptyFile;
213 
Args(String[] args)214         public Args(String[] args) throws IllegalArgumentException {
215             Map<String, List<String>> valuesByKey = new LinkedHashMap<>();
216             String key = null;
217             for (int i = 0; i < args.length; i++) {
218                 String arg = args[i];
219                 if (arg.startsWith("--")) {
220                     key = arg;
221                     continue;
222                 }
223                 if (key == null) {
224                     throw new IllegalArgumentException("Missing key for value: " + arg);
225                 }
226                 if (valuesByKey.get(key) == null) {
227                     valuesByKey.put(key, new ArrayList<>());
228                 }
229                 valuesByKey.get(key).add(arg);
230             }
231             String pkgName;
232             List<String> values = valuesByKey.get(PACKAGE_NAME_OPTION);
233             if (values == null) {
234                 pkgName = DEFAULT_PACKAGE_NAME;
235             } else {
236                 pkgName = values.get(0);
237             }
238             String pkgDir = pkgName.replace(".", File.separator);
239             this.pkgName = pkgName;
240             this.pkgDir = pkgDir;
241             String inputDir;
242             values = valuesByKey.get(INPUT_DIR_OPTION);
243             if (values == null) {
244                 List<String> inputFiles = valuesByKey.get(INPUT_FILES_OPTION);
245                 if (inputFiles == null) {
246                     throw new IllegalArgumentException("Either " + INPUT_DIR_OPTION + " or "
247                             + INPUT_FILES_OPTION + " must be specified");
248                 }
249                 inputDir = new File(inputFiles.get(0)).getParent().replace(pkgDir, "");
250             } else {
251                 inputDir = values.get(0);
252             }
253             this.inputDir = inputDir;
254             values = valuesByKey.get(OUTPUT_JSON_OPTION);
255             if (values == null) {
256                 throw new IllegalArgumentException(OUTPUT_JSON_OPTION + " must be specified");
257             }
258             this.output = values.get(0);
259             values = valuesByKey.get(CHECK_AGAINST_OPTION);
260             if (values != null) {
261                 this.checkFile = values.get(0);
262             } else {
263                 this.checkFile = null;
264             }
265             values = valuesByKey.get(OUTPUT_EMPTY_FILE_OPTION);
266             if (values != null) {
267                 this.outputEmptyFile = values.get(0);
268             } else {
269                 this.outputEmptyFile = null;
270             }
271         }
272     }
273 
274     /**
275      * Main function.
276      */
main(final String[] args)277     public static void main(final String[] args) throws Exception {
278         Args parsedArgs;
279         try {
280             parsedArgs = new Args(args);
281         } catch (IllegalArgumentException e) {
282             System.out.println("Invalid arguments: " + e.getMessage());
283             System.out.println(USAGE);
284             System.exit(1);
285             // Never reach here.
286             return;
287         }
288 
289         TypeSolver typeSolver = new CombinedTypeSolver(
290                 new ReflectionTypeSolver(), new JavaParserTypeSolver(parsedArgs.inputDir));
291         StaticJavaParser.getConfiguration().setSymbolResolver(new JavaSymbolSolver(typeSolver));
292 
293         Enum vehicleProperty = new Enum("VehicleProperty", parsedArgs.pkgName);
294         CompilationUnit cu = StaticJavaParser.parse(new File(parsedArgs.inputDir + File.separator
295                 + parsedArgs.pkgDir + File.separator + VEHICLE_PROPERTY_FILE));
296         AnnotationDeclaration vehiclePropertyIdsClass =
297                 cu.getAnnotationDeclarationByName("VehicleProperty").get();
298 
299         Set<String> dataEnumTypes = new HashSet<>();
300         List<FieldDeclaration> variables = vehiclePropertyIdsClass.findAll(FieldDeclaration.class);
301         for (int i = 0; i < variables.size(); i++) {
302             FieldDeclaration propertyDef = variables.get(i).asFieldDeclaration();
303             if (!isPublicAndStatic(propertyDef)) {
304                 continue;
305             }
306             String propertyName = getFieldName(propertyDef);
307             if (propertyName.equals("INVALID")) {
308                 continue;
309             }
310 
311             Optional<Comment> maybeComment = propertyDef.getComment();
312             if (!maybeComment.isPresent()) {
313                 System.out.println("missing comment for property: " + propertyName);
314                 System.exit(1);
315             }
316             Javadoc doc = maybeComment.get().asJavadocComment().parse();
317 
318             int propertyId = parseIntEnumField(propertyDef);
319             // We use the first paragraph as the property's name
320             String propertyDescription = doc.getDescription().toText();
321             String firstLine = propertyDescription.split("\n\n")[0];
322             String name = firstLine;
323             if (firstLine.indexOf("\n") != -1 || firstLine.length() > MAX_PROPERTY_NAME_LENGTH) {
324                 // The description is too long, we just use the property name.
325                 name = propertyName;
326             }
327 
328             ValueField field = new ValueField(name, propertyId);
329             String fieldDescription = "";
330             for (String line : propertyDescription.split("\n")) {
331                 String stripped = line.strip();
332                 // If this is an empty line, starts a new paragraph.
333                 if (stripped.isEmpty()) {
334                     fieldDescription += "\n";
335                 }
336                 // Ignore annotation lines.
337                 for (int j = 0; j < ANNOTATIONS.size(); j++) {
338                     if (stripped.startsWith(ANNOTATIONS.get(j))) {
339                         continue;
340                     }
341                 }
342                 // If this is a new line, we concat it with the previous line with a space.
343                 if (!fieldDescription.isEmpty()
344                         && fieldDescription.charAt(fieldDescription.length() - 1) != '\n') {
345                     fieldDescription += " ";
346                 }
347                 fieldDescription += stripped;
348             }
349             field.description = fieldDescription.strip();
350 
351             List<JavadocBlockTag> blockTags = doc.getBlockTags();
352             for (int j = 0; j < blockTags.size(); j++) {
353                 String commentTagName = blockTags.get(j).getTagName();
354                 String commentTagContent = blockTags.get(j).getContent().toText();
355                 if (!commentTagName.equals("data_enum")) {
356                     continue;
357                 }
358                 field.dataEnums.add(commentTagContent);
359                 dataEnumTypes.add(commentTagContent);
360             }
361 
362             vehicleProperty.valueFields.add(field);
363         }
364 
365         List<Enum> enumTypes = new ArrayList<>();
366         enumTypes.add(vehicleProperty);
367 
368         for (String dataEnumType : dataEnumTypes) {
369             Enum dataEnum = parseEnumInterface(
370                     parsedArgs.inputDir, parsedArgs.pkgDir, parsedArgs.pkgName, dataEnumType);
371             enumTypes.add(dataEnum);
372         }
373 
374         // Sort the enum types based on their packageName, name.
375         // Make sure VehicleProperty is always at the first.
376         Collections.sort(enumTypes.subList(1, enumTypes.size()), (Enum enum1, Enum enum2) -> {
377             var collator = Collator.getInstance();
378             if (enum1.packageName.equals(enum2.packageName)) {
379                 return collator.compare(enum1.name, enum2.name);
380             }
381             return collator.compare(enum1.packageName, enum2.packageName);
382         });
383 
384         // Output enumTypes as JSON to output.
385         JSONArray jsonEnums = new JSONArray();
386         for (int i = 0; i < enumTypes.size(); i++) {
387             Enum enumType = enumTypes.get(i);
388 
389             JSONObject jsonEnum = new OrderedJSONObject();
390             jsonEnum.put("name", enumType.name);
391             jsonEnum.put("package", enumType.packageName);
392             JSONArray values = new JSONArray();
393             jsonEnum.put("values", values);
394 
395             for (int j = 0; j < enumType.valueFields.size(); j++) {
396                 ValueField valueField = enumType.valueFields.get(j);
397                 JSONObject jsonValueField = new OrderedJSONObject();
398                 jsonValueField.put("name", valueField.name);
399                 jsonValueField.put("value", valueField.value);
400                 if (!valueField.dataEnums.isEmpty()) {
401                     JSONArray jsonDataEnums = new JSONArray();
402                     for (String dataEnum : valueField.dataEnums) {
403                         jsonDataEnums.put(dataEnum);
404                     }
405                     jsonValueField.put("data_enums", jsonDataEnums);
406                     // To be backward compatible with older format where data_enum is a single
407                     // entry.
408                     jsonValueField.put("data_enum", valueField.dataEnums.get(0));
409                 }
410                 if (!valueField.description.isEmpty()) {
411                     jsonValueField.put("description", valueField.description);
412                 }
413                 values.put(jsonValueField);
414             }
415 
416             jsonEnums.put(jsonEnum);
417         }
418 
419         try (FileOutputStream outputStream = new FileOutputStream(parsedArgs.output)) {
420             outputStream.write(jsonEnums.toString(4).getBytes());
421         }
422 
423         System.out.println("Input at folder: " + parsedArgs.inputDir
424                 + " successfully parsed. Output at: " + parsedArgs.output);
425 
426         if (parsedArgs.checkFile != null) {
427             String checkFileContent = readFileContent(parsedArgs.checkFile);
428             String generatedFileContent = readFileContent(parsedArgs.output);
429             String generatedFilePath = new File(parsedArgs.output).getAbsolutePath();
430             if (!checkFileContent.equals(generatedFileContent)) {
431                 System.out.println("The file: " + CHECK_FILE_PATH + " needs to be updated, run: "
432                         + "\n\ncp " + generatedFilePath + " " + CHECK_FILE_PATH + "\n");
433                 System.exit(1);
434             }
435 
436             if (parsedArgs.outputEmptyFile != null) {
437                 try (FileOutputStream outputStream =
438                                 new FileOutputStream(parsedArgs.outputEmptyFile)) {
439                     // Do nothing, just create the file.
440                 }
441             }
442         }
443     }
444 }
445