1 /* 2 * Copyright (C) 2023 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 androidx.annotation.Nullable; 20 21 import com.github.javaparser.StaticJavaParser; 22 import com.github.javaparser.ast.CompilationUnit; 23 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; 24 import com.github.javaparser.ast.body.FieldDeclaration; 25 import com.github.javaparser.ast.body.VariableDeclarator; 26 import com.github.javaparser.ast.comments.Comment; 27 import com.github.javaparser.ast.expr.AnnotationExpr; 28 import com.github.javaparser.ast.expr.ArrayInitializerExpr; 29 import com.github.javaparser.ast.expr.Expression; 30 import com.github.javaparser.ast.expr.NormalAnnotationExpr; 31 import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; 32 import com.github.javaparser.ast.expr.UnaryExpr; 33 import com.github.javaparser.ast.type.ClassOrInterfaceType; 34 import com.github.javaparser.javadoc.Javadoc; 35 import com.github.javaparser.javadoc.JavadocBlockTag; 36 import com.github.javaparser.javadoc.description.JavadocDescription; 37 import com.github.javaparser.javadoc.description.JavadocDescriptionElement; 38 import com.github.javaparser.javadoc.description.JavadocInlineTag; 39 import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; 40 import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; 41 import com.github.javaparser.symbolsolver.JavaSymbolSolver; 42 import com.github.javaparser.symbolsolver.javaparsermodel.declarations.JavaParserFieldDeclaration; 43 import com.github.javaparser.symbolsolver.model.resolution.TypeSolver; 44 import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; 45 import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; 46 import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; 47 48 import org.json.JSONArray; 49 import org.json.JSONException; 50 import org.json.JSONObject; 51 52 import java.io.File; 53 import java.io.FileOutputStream; 54 import java.lang.reflect.Field; 55 import java.util.ArrayList; 56 import java.util.HashMap; 57 import java.util.LinkedHashMap; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Optional; 61 import java.util.Set; 62 import java.util.TreeSet; 63 64 /** 65 * A parser for VehiclePropertyIds.java. 66 * 67 * It will parse the vehicle property ID definitions, comments and annotations and generate property 68 * config file. 69 */ 70 public final class VehiclePropertyIdsParser { 71 72 private static final int CONFIG_FILE_SCHEMA_VERSION = 1; 73 74 private static final String USAGE = 75 "VehiclePropertyIdsParser [path_to_CarLibSrcFolder] [output]"; 76 private static final String VEHICLE_PROPERTY_IDS_JAVA_PATH = 77 "/android/car/VehiclePropertyIds.java"; 78 79 private static final String ACCESS_MODE_READ_LINK = 80 "{@link android.car.hardware.CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_READ}"; 81 private static final String ACCESS_MODE_WRITE_LINK = 82 "{@link android.car.hardware.CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_WRITE}"; 83 private static final String ACCESS_MODE_READ_WRITE_LINK = 84 "{@link android.car.hardware.CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_READ_WRITE}"; 85 86 // A map from property name to VHAL property ID if we use different property ID in car service 87 // and in VHAL. 88 private static final Map<String, Integer> VHAL_PROP_ID_MAP = Map.ofEntries( 89 // VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS 90 Map.entry("VEHICLE_SPEED_DISPLAY_UNITS", 0x11400605) 91 ); 92 93 // A map to store permissions that are not defined in Car.java. It is not trivial to cross-ref 94 // these so just hard-code them here. 95 private static final Map<String, String> NON_CAR_PERMISSION_MAP = Map.ofEntries( 96 Map.entry("ACCESS_FINE_LOCATION", "android.permission.ACCESS_FINE_LOCATION") 97 ); 98 99 private static final class PropertyConfig { 100 public String propertyName; 101 public int propertyId; 102 public String description = ""; 103 public PermissionType readPermission; 104 public PermissionType writePermission; 105 public boolean deprecated; 106 public boolean systemApi; 107 public boolean hide; 108 public int vhalPropertyId; 109 public Set<Integer> dataEnums; 110 public Set<Integer> dataFlag; 111 public String featureFlag; 112 113 @Override toString()114 public String toString() { 115 StringBuilder s = new StringBuilder().append("PropertyConfig{") 116 .append("\n propertyName: ").append(propertyName) 117 .append("\n propertyId: ").append(propertyId) 118 .append("\n description: ").append(description) 119 .append("\n readPermission: ").append(readPermission) 120 .append("\n writePermission: ").append(writePermission) 121 .append("\n deprecated: ").append(deprecated) 122 .append("\n hide: ").append(hide) 123 .append("\n systemApi: ").append(systemApi) 124 .append("\n dataEnums: ").append(dataEnums) 125 .append("\n dataFlag: ").append(dataFlag) 126 .append("\n featureFlag: ").append(featureFlag); 127 128 if (vhalPropertyId != 0) { 129 s.append("\n vhalPropertyId: ").append(vhalPropertyId); 130 } 131 132 return s.append("\n}").toString(); 133 } 134 } 135 136 private enum ACCESS_MODE { 137 READ, WRITE, READ_WRITE 138 } 139 140 private static final class PermissionType { 141 public String type; 142 public String value; 143 public List<PermissionType> subPermissions = new ArrayList<>(); 144 toJson()145 public OrderedJSONObject toJson() throws JSONException { 146 OrderedJSONObject jsonPerm = new OrderedJSONObject(); 147 jsonPerm.put("type", type); 148 if (type.equals("single")) { 149 jsonPerm.put("value", value); 150 return jsonPerm; 151 } 152 List<OrderedJSONObject> subObjects = new ArrayList<>(); 153 for (int i = 0; i < subPermissions.size(); i++) { 154 subObjects.add(subPermissions.get(i).toJson()); 155 } 156 jsonPerm.put("value", new JSONArray(subObjects)); 157 return jsonPerm; 158 } 159 }; 160 161 /** 162 * Sets the read/write permission for the config. 163 */ setPermission(PropertyConfig config, ACCESS_MODE accessMode, PermissionType permission, boolean forRead, boolean forWrite)164 private static void setPermission(PropertyConfig config, ACCESS_MODE accessMode, 165 PermissionType permission, boolean forRead, boolean forWrite) { 166 if (forRead) { 167 if (accessMode == ACCESS_MODE.READ || accessMode == ACCESS_MODE.READ_WRITE) { 168 config.readPermission = permission; 169 } 170 } 171 if (forWrite) { 172 if (accessMode == ACCESS_MODE.WRITE || accessMode == ACCESS_MODE.READ_WRITE) { 173 config.writePermission = permission; 174 } 175 } 176 } 177 178 // A hacky way to make the key in-order in the JSON object. 179 private static final class OrderedJSONObject extends JSONObject { OrderedJSONObject()180 OrderedJSONObject() { 181 try { 182 Field map = JSONObject.class.getDeclaredField("nameValuePairs"); 183 map.setAccessible(true); 184 map.set(this, new LinkedHashMap<>()); 185 map.setAccessible(false); 186 } catch (IllegalAccessException | NoSuchFieldException e) { 187 throw new RuntimeException(e); 188 } 189 } 190 } 191 192 /** 193 * Parses the enum field declaration as an int value. 194 */ parseIntEnumField(FieldDeclaration fieldDecl)195 private static int parseIntEnumField(FieldDeclaration fieldDecl) { 196 VariableDeclarator valueDecl = fieldDecl.getVariables().get(0); 197 Expression expr = valueDecl.getInitializer().get(); 198 if (expr.isIntegerLiteralExpr()) { 199 return expr.asIntegerLiteralExpr().asInt(); 200 } 201 // For case like -123 202 if (expr.isUnaryExpr() 203 && expr.asUnaryExpr().getOperator() == UnaryExpr.Operator.MINUS) { 204 return -expr.asUnaryExpr().getExpression().asIntegerLiteralExpr().asInt(); 205 } 206 System.out.println("Unsupported expression: " + expr); 207 System.exit(1); 208 return 0; 209 } 210 getFieldName(FieldDeclaration fieldDecl)211 private static String getFieldName(FieldDeclaration fieldDecl) { 212 VariableDeclarator valueDecl = fieldDecl.getVariables().get(0); 213 return valueDecl.getName().asString(); 214 } 215 216 /** 217 * Whether this field is an internal-only hidden field. 218 */ isInternal(FieldDeclaration fieldDecl)219 private static boolean isInternal(FieldDeclaration fieldDecl) { 220 Optional<Comment> maybeComment = fieldDecl.getComment(); 221 boolean hide = false; 222 boolean systemApi = false; 223 if (maybeComment.isPresent()) { 224 Javadoc doc = maybeComment.get().asJavadocComment().parse(); 225 for (JavadocBlockTag tag : doc.getBlockTags()) { 226 if (tag.getTagName().equals("hide")) { 227 hide = true; 228 break; 229 } 230 } 231 } 232 List<AnnotationExpr> annotations = fieldDecl.getAnnotations(); 233 for (AnnotationExpr annotation : annotations) { 234 if (annotation.getName().asString().equals("SystemApi")) { 235 systemApi = true; 236 break; 237 } 238 } 239 return hide && !systemApi; 240 } 241 242 /** 243 * Gets all the int enum values for this enum type. 244 */ getEnumValues(ResolvedReferenceTypeDeclaration typeDecl)245 private static Set<Integer> getEnumValues(ResolvedReferenceTypeDeclaration typeDecl) { 246 Set<Integer> enumValues = new TreeSet<>(); 247 for (ResolvedFieldDeclaration resolvedFieldDecl : typeDecl.getAllFields()) { 248 if (!resolvedFieldDecl.isField()) { 249 continue; 250 } 251 FieldDeclaration fieldDecl = ((JavaParserFieldDeclaration) resolvedFieldDecl.asField()) 252 .getWrappedNode(); 253 if (!isPublicAndStatic(fieldDecl) || isInternal(fieldDecl)) { 254 continue; 255 } 256 enumValues.add(parseIntEnumField(fieldDecl)); 257 } 258 return enumValues; 259 } 260 isPublicAndStatic(FieldDeclaration fieldDecl)261 private static boolean isPublicAndStatic(FieldDeclaration fieldDecl) { 262 return fieldDecl.isPublic() && fieldDecl.isStatic(); 263 } 264 265 private final CompilationUnit mCu; 266 private final Map<String, String> mCarPermissionMap = new HashMap<>(); 267 VehiclePropertyIdsParser(CompilationUnit cu)268 VehiclePropertyIdsParser(CompilationUnit cu) { 269 this.mCu = cu; 270 populateCarPermissionMap(); 271 } 272 273 /** 274 * Parses the Car.java class and stores all car specific permission into a map. 275 */ populateCarPermissionMap()276 private void populateCarPermissionMap() { 277 ResolvedReferenceTypeDeclaration typeDecl = parseClassName("Car"); 278 for (ResolvedFieldDeclaration resolvedFieldDecl : typeDecl.getAllFields()) { 279 if (!resolvedFieldDecl.isField()) { 280 continue; 281 } 282 FieldDeclaration fieldDecl = ((JavaParserFieldDeclaration) resolvedFieldDecl.asField()) 283 .getWrappedNode(); 284 if (!isPublicAndStatic(fieldDecl)) { 285 continue; 286 } 287 if (!isPublicAndStatic(fieldDecl) || isInternal(fieldDecl)) { 288 continue; 289 } 290 String fieldName = getFieldName(fieldDecl); 291 if (!fieldName.startsWith("PERMISSION_")) { 292 continue; 293 } 294 VariableDeclarator valueDecl = fieldDecl.getVariables().get(0); 295 mCarPermissionMap.put("Car." + fieldName, 296 valueDecl.getInitializer().get().asStringLiteralExpr().asString()); 297 } 298 } 299 300 /** 301 * Maps the permission class to the actual permission string. 302 */ 303 @Nullable permNameToValue(String permName)304 private String permNameToValue(String permName) { 305 String permStr = mCarPermissionMap.get(permName); 306 if (permStr != null) { 307 return permStr; 308 } 309 permStr = NON_CAR_PERMISSION_MAP.get(permName); 310 if (permStr != null) { 311 return permStr; 312 } 313 System.out.println("Permission: " + permName + " unknown, if it is not defined in" 314 + " Car.java, you need to add it to NON_CAR_PERMISSION_MAP in parser"); 315 return null; 316 } 317 318 /** 319 * Parses a class name and returns the class declaration. 320 */ parseClassName(String className)321 private ResolvedReferenceTypeDeclaration parseClassName(String className) { 322 ClassOrInterfaceType type = StaticJavaParser.parseClassOrInterfaceType(className); 323 // Must associate the type with a compilation unit. 324 type.setParentNode(mCu); 325 return type.resolve().getTypeDeclaration(); 326 } 327 328 /** 329 * Parses a javadoc {@link XXX} annotation. 330 */ 331 @Nullable parseClassLink(JavadocDescription linkElement)332 private ResolvedReferenceTypeDeclaration parseClassLink(JavadocDescription linkElement) { 333 List<JavadocDescriptionElement> elements = linkElement.getElements(); 334 if (elements.size() != 1) { 335 System.out.println("expected one doc element in: " + linkElement); 336 return null; 337 } 338 JavadocInlineTag tag = (JavadocInlineTag) elements.get(0); 339 String className = tag.getContent().strip(); 340 try { 341 return parseClassName(className); 342 } catch (Exception e) { 343 System.out.println("failed to parse class name: " + className); 344 return null; 345 } 346 } 347 348 /** 349 * Parses a permission annotation. 350 */ 351 @Nullable parsePermAnnotation(AnnotationExpr annotation)352 private PermissionType parsePermAnnotation(AnnotationExpr annotation) { 353 PermissionType permission = new PermissionType(); 354 if (annotation.isSingleMemberAnnotationExpr()) { 355 permission.type = "single"; 356 SingleMemberAnnotationExpr single = 357 annotation.asSingleMemberAnnotationExpr(); 358 Expression member = single.getMemberValue(); 359 String permName = permNameToValue(member.toString()); 360 if (permName == null) { 361 return null; 362 } 363 permission.value = permName; 364 return permission; 365 } else if (annotation.isNormalAnnotationExpr()) { 366 NormalAnnotationExpr normal = annotation.asNormalAnnotationExpr(); 367 boolean any = false; 368 String name = normal.getPairs().get(0).getName().toString(); 369 if (name.equals("anyOf")) { 370 permission.type = "anyOf"; 371 } else if (name.equals("allOf")) { 372 permission.type = "allOf"; 373 } else { 374 return null; 375 } 376 ArrayInitializerExpr expr = normal.getPairs().get(0).getValue() 377 .asArrayInitializerExpr(); 378 for (Expression permExpr : expr.getValues()) { 379 PermissionType subPermission = new PermissionType(); 380 subPermission.type = "single"; 381 String permName = permNameToValue(permExpr.toString()); 382 if (permName == null) { 383 return null; 384 } 385 subPermission.value = permName; 386 permission.subPermissions.add(subPermission); 387 } 388 return permission; 389 } 390 System.out.println("The permission annotation is not single or normal expression"); 391 return null; 392 } 393 394 /** 395 * Parses the permission annotation and sets the config's permission accordingly. 396 */ parseAndSetPermAnnotation(AnnotationExpr annotation, PropertyConfig config, ACCESS_MODE accessMode, boolean forRead, boolean forWrite)397 private void parseAndSetPermAnnotation(AnnotationExpr annotation, PropertyConfig config, 398 ACCESS_MODE accessMode, boolean forRead, boolean forWrite) { 399 if (accessMode == null) { 400 return; 401 } 402 PermissionType permission = parsePermAnnotation(annotation); 403 if (permission == null) { 404 System.out.println("Invalid RequiresPermission annotation: " 405 + annotation + " for property: " + config.propertyName); 406 System.exit(1); 407 } 408 setPermission(config, accessMode, permission, forRead, forWrite); 409 } 410 411 /** 412 * Main logic for parsing VehiclePropertyIds.java to a list of property configs. 413 */ parse()414 private List<PropertyConfig> parse() { 415 List<PropertyConfig> propertyConfigs = new ArrayList<>(); 416 ClassOrInterfaceDeclaration vehiclePropertyIdsClass = 417 mCu.getClassByName("VehiclePropertyIds").get(); 418 419 List<FieldDeclaration> variables = vehiclePropertyIdsClass.findAll(FieldDeclaration.class); 420 for (int i = 0; i < variables.size(); i++) { 421 ACCESS_MODE accessMode = null; 422 PropertyConfig propertyConfig = new PropertyConfig(); 423 424 FieldDeclaration propertyDef = variables.get(i).asFieldDeclaration(); 425 if (!isPublicAndStatic(propertyDef)) { 426 continue; 427 } 428 String propertyName = getFieldName(propertyDef); 429 if (propertyName.equals("INVALID")) { 430 continue; 431 } 432 433 int propertyId = parseIntEnumField(propertyDef); 434 propertyConfig.propertyName = propertyName; 435 propertyConfig.propertyId = propertyId; 436 437 if (VHAL_PROP_ID_MAP.get(propertyName) != null) { 438 propertyConfig.vhalPropertyId = VHAL_PROP_ID_MAP.get(propertyName); 439 } 440 441 Optional<Comment> maybeComment = propertyDef.getComment(); 442 if (!maybeComment.isPresent()) { 443 System.out.println("missing comment for property: " + propertyName); 444 System.exit(1); 445 } 446 447 Javadoc doc = maybeComment.get().asJavadocComment().parse(); 448 List<JavadocBlockTag> blockTags = doc.getBlockTags(); 449 boolean deprecated = false; 450 boolean hide = false; 451 Set<Integer> dataEnums = new TreeSet<>(); 452 Set<Integer> dataFlag = new TreeSet<>(); 453 for (int j = 0; j < blockTags.size(); j++) { 454 String commentTagName = blockTags.get(j).getTagName(); 455 if (commentTagName.equals("deprecated") 456 || commentTagName.equals("to_be_deprecated")) { 457 deprecated = true; 458 } 459 if (commentTagName.equals("hide")) { 460 hide = true; 461 } 462 String commentTagContent = blockTags.get(j).getContent().toText(); 463 ResolvedReferenceTypeDeclaration enumType = null; 464 if (commentTagName.equals("data_enum") || commentTagName.equals("data_flag")) { 465 enumType = parseClassLink(blockTags.get(j).getContent()); 466 if (enumType == null) { 467 System.out.println("Invalid comment block: " + commentTagContent 468 + " for property: " + propertyName); 469 System.exit(1); 470 } 471 } 472 if (commentTagName.equals("data_enum")) { 473 dataEnums.addAll(getEnumValues(enumType)); 474 } 475 if (commentTagName.equals("data_flag")) { 476 if (dataFlag.size() != 0) { 477 System.out.println("Duplicated data_flag annotation for one property: " 478 + propertyName); 479 System.exit(1); 480 } 481 dataFlag = getEnumValues(enumType); 482 } 483 } 484 485 String docText = doc.toText(); 486 propertyConfig.description = (docText.split("\n"))[0]; 487 propertyConfig.deprecated = deprecated; 488 propertyConfig.hide = hide; 489 propertyConfig.dataEnums = dataEnums; 490 propertyConfig.dataFlag = dataFlag; 491 492 if (docText.indexOf(ACCESS_MODE_READ_WRITE_LINK) != -1) { 493 accessMode = ACCESS_MODE.READ_WRITE; 494 } else if (docText.indexOf(ACCESS_MODE_READ_LINK) != -1) { 495 accessMode = ACCESS_MODE.READ; 496 } else if (docText.indexOf(ACCESS_MODE_WRITE_LINK) != -1) { 497 accessMode = ACCESS_MODE.WRITE; 498 } else { 499 if (!deprecated) { 500 System.out.println("missing access mode for property: " + propertyName); 501 System.exit(1); 502 } 503 } 504 505 List<AnnotationExpr> annotations = propertyDef.getAnnotations(); 506 for (int j = 0; j < annotations.size(); j++) { 507 AnnotationExpr annotation = annotations.get(j); 508 String annotationName = annotation.getName().asString(); 509 if (annotationName.equals("RequiresPermission")) { 510 parseAndSetPermAnnotation(annotation, propertyConfig, accessMode, 511 /* forRead= */ true, /* forWrite= */ true); 512 } 513 if (annotationName.equals("RequiresPermission.Read")) { 514 AnnotationExpr requireAnnotation = annotation.asSingleMemberAnnotationExpr() 515 .getMemberValue().asAnnotationExpr(); 516 parseAndSetPermAnnotation(requireAnnotation, propertyConfig, accessMode, 517 /* forRead= */ true, /* forWrite= */ false); 518 } 519 if (annotationName.equals("RequiresPermission.Write")) { 520 AnnotationExpr requireAnnotation = annotation.asSingleMemberAnnotationExpr() 521 .getMemberValue().asAnnotationExpr(); 522 parseAndSetPermAnnotation(requireAnnotation, propertyConfig, accessMode, 523 /* forRead= */ false, /* forWrite= */ true); 524 } 525 if (annotationName.equals("SystemApi")) { 526 propertyConfig.systemApi = true; 527 } 528 if (annotationName.equals("FlaggedApi")) { 529 SingleMemberAnnotationExpr single = 530 annotation.asSingleMemberAnnotationExpr(); 531 Expression member = single.getMemberValue(); 532 propertyConfig.featureFlag = member.toString(); 533 } 534 } 535 if (propertyConfig.systemApi || !propertyConfig.hide) { 536 // We do not generate config for hidden APIs since they are not exposed to public. 537 propertyConfigs.add(propertyConfig); 538 } 539 } 540 return propertyConfigs; 541 } 542 543 /** 544 * Main function. 545 */ main(final String[] args)546 public static void main(final String[] args) throws Exception { 547 if (args.length < 2) { 548 System.out.println(USAGE); 549 System.exit(1); 550 } 551 String carLib = args[0]; 552 String output = args[1]; 553 String vehiclePropertyIdsJava = carLib + VEHICLE_PROPERTY_IDS_JAVA_PATH; 554 555 TypeSolver typeSolver = new CombinedTypeSolver( 556 new ReflectionTypeSolver(), 557 new JavaParserTypeSolver(carLib)); 558 StaticJavaParser.getConfiguration().setSymbolResolver(new JavaSymbolSolver(typeSolver)); 559 560 CompilationUnit cu = StaticJavaParser.parse(new File(vehiclePropertyIdsJava)); 561 List<PropertyConfig> propertyConfigs = new VehiclePropertyIdsParser(cu).parse(); 562 563 JSONObject root = new JSONObject(); 564 root.put("version", CONFIG_FILE_SCHEMA_VERSION); 565 JSONObject jsonProps = new OrderedJSONObject(); 566 root.put("properties", jsonProps); 567 for (int i = 0; i < propertyConfigs.size(); i++) { 568 JSONObject jsonProp = new OrderedJSONObject(); 569 PropertyConfig config = propertyConfigs.get(i); 570 jsonProp.put("propertyName", config.propertyName); 571 jsonProp.put("propertyId", config.propertyId); 572 jsonProp.put("description", config.description); 573 if (config.readPermission != null) { 574 jsonProp.put("readPermission", config.readPermission.toJson()); 575 } 576 if (config.writePermission != null) { 577 jsonProp.put("writePermission", config.writePermission.toJson()); 578 } 579 if (config.deprecated) { 580 jsonProp.put("deprecated", config.deprecated); 581 } 582 if (config.systemApi) { 583 jsonProp.put("systemApi", config.systemApi); 584 } 585 if (config.vhalPropertyId != 0) { 586 jsonProp.put("vhalPropertyId", config.vhalPropertyId); 587 } 588 if (config.dataEnums.size() != 0) { 589 jsonProp.put("dataEnums", new JSONArray(config.dataEnums)); 590 } 591 if (config.dataFlag.size() != 0) { 592 jsonProp.put("dataFlag", new JSONArray(config.dataFlag)); 593 } 594 if (config.featureFlag != null) { 595 jsonProp.put("featureFlag", config.featureFlag); 596 } 597 jsonProps.put(config.propertyName, jsonProp); 598 } 599 600 try (FileOutputStream outputStream = new FileOutputStream(output)) { 601 outputStream.write(root.toString(2).getBytes()); 602 } 603 System.out.println("Input: " + vehiclePropertyIdsJava 604 + " successfully parsed. Output at: " + output); 605 } 606 } 607