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