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