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