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