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