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