• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.asllib.util;
18 
19 
20 import org.w3c.dom.Document;
21 import org.w3c.dom.Element;
22 import org.w3c.dom.Node;
23 import org.w3c.dom.NodeList;
24 
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.stream.Collectors;
31 
32 public class XmlUtils {
33     public static final String DATA_TYPE_SEPARATOR = "_data_type_";
34 
35     public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
36     public static final String HR_TAG_SYSTEM_APP_SAFETY_LABEL = "system-app-safety-label";
37     public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
38     public static final String HR_TAG_TRANSPARENCY_INFO = "transparency-info";
39     public static final String HR_TAG_DEVELOPER_INFO = "developer-info";
40     public static final String HR_TAG_APP_INFO = "app-info";
41     public static final String HR_TAG_DATA_LABELS = "data-labels";
42     public static final String HR_TAG_SECURITY_LABELS = "security-labels";
43     public static final String HR_TAG_THIRD_PARTY_VERIFICATION = "third-party-verification";
44     public static final String HR_TAG_DATA_ACCESSED = "data-accessed";
45     public static final String HR_TAG_DATA_COLLECTED = "data-collected";
46     public static final String HR_TAG_DATA_COLLECTED_EPHEMERAL = "data-collected-ephemeral";
47     public static final String HR_TAG_DATA_SHARED = "data-shared";
48     public static final String HR_TAG_ITEM = "item";
49     public static final String HR_ATTR_NAME = "name";
50     public static final String HR_ATTR_EMAIL = "email";
51     public static final String HR_ATTR_ADDRESS = "address";
52     public static final String HR_ATTR_COUNTRY_REGION = "countryRegion";
53     public static final String HR_ATTR_DEVELOPER_RELATIONSHIP = "relationship";
54     public static final String HR_ATTR_WEBSITE = "website";
55     public static final String HR_ATTR_APP_DEVELOPER_REGISTRY_ID = "registryId";
56     public static final String HR_ATTR_DATA_CATEGORY = "dataCategory";
57     public static final String HR_ATTR_DATA_TYPE = "dataType";
58     public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional";
59     public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional";
60     public static final String HR_ATTR_IS_DATA_DELETABLE = "isDataDeletable";
61     public static final String HR_ATTR_IS_DATA_ENCRYPTED = "isDataEncrypted";
62     // public static final String HR_ATTR_EPHEMERAL = "ephemeral";
63     public static final String HR_ATTR_PURPOSES = "purposes";
64     public static final String HR_ATTR_VERSION = "version";
65     public static final String HR_ATTR_URL = "url";
66     public static final String HR_ATTR_DECLARATION = "declaration";
67     public static final String HR_ATTR_TITLE = "title";
68     public static final String HR_ATTR_DESCRIPTION = "description";
69     public static final String HR_ATTR_CONTAINS_ADS = "containsAds";
70     public static final String HR_ATTR_OBEY_APS = "obeyAps";
71     public static final String HR_ATTR_APS_COMPLIANT = "apsCompliant";
72     public static final String HR_ATTR_ADS_FINGERPRINTING = "adsFingerprinting";
73     public static final String HR_ATTR_SECURITY_FINGERPRINTING = "securityFingerprinting";
74     public static final String HR_ATTR_PRIVACY_POLICY = "privacyPolicy";
75     public static final String HR_ATTR_DEVELOPER_ID = "developerId";
76     public static final String HR_ATTR_APPLICATION_ID = "applicationId";
77     public static final String HR_ATTR_SECURITY_ENDPOINTS = "securityEndpoints";
78     public static final String HR_TAG_FIRST_PARTY_ENDPOINTS = "first-party-endpoints";
79     public static final String HR_TAG_SERVICE_PROVIDER_ENDPOINTS = "service-provider-endpoints";
80     public static final String HR_ATTR_CATEGORY = "category";
81 
82     public static final String OD_TAG_BUNDLE = "bundle";
83     public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map";
84     public static final String OD_TAG_BOOLEAN = "boolean";
85     public static final String OD_TAG_LONG = "long";
86     public static final String OD_TAG_STRING = "string";
87     public static final String OD_TAG_INT_ARRAY = "int-array";
88     public static final String OD_TAG_STRING_ARRAY = "string-array";
89     public static final String OD_TAG_ITEM = "item";
90     public static final String OD_ATTR_NAME = "name";
91     public static final String OD_ATTR_VALUE = "value";
92     public static final String OD_ATTR_NUM = "num";
93     public static final String OD_NAME_SAFETY_LABELS = "safety_labels";
94     public static final String OD_NAME_TRANSPARENCY_INFO = "transparency_info";
95     public static final String OD_NAME_DEVELOPER_INFO = "developer_info";
96     public static final String OD_NAME_NAME = "name";
97     public static final String OD_NAME_EMAIL = "email";
98     public static final String OD_NAME_ADDRESS = "address";
99     public static final String OD_NAME_COUNTRY_REGION = "country_region";
100     public static final String OD_NAME_DEVELOPER_RELATIONSHIP = "relationship";
101     public static final String OD_NAME_WEBSITE = "website";
102     public static final String OD_NAME_APP_DEVELOPER_REGISTRY_ID = "app_developer_registry_id";
103     public static final String OD_NAME_APP_INFO = "app_info";
104     public static final String OD_NAME_TITLE = "title";
105     public static final String OD_NAME_DESCRIPTION = "description";
106     public static final String OD_NAME_CONTAINS_ADS = "contains_ads";
107     public static final String OD_NAME_OBEY_APS = "obey_aps";
108     public static final String OD_NAME_APS_COMPLIANT = "aps_compliant";
109     public static final String OD_NAME_DEVELOPER_ID = "developer_id";
110     public static final String OD_NAME_APPLICATION_ID = "application_id";
111     public static final String OD_NAME_ADS_FINGERPRINTING = "ads_fingerprinting";
112     public static final String OD_NAME_SECURITY_FINGERPRINTING = "security_fingerprinting";
113     public static final String OD_NAME_PRIVACY_POLICY = "privacy_policy";
114     public static final String OD_NAME_SECURITY_ENDPOINTS = "security_endpoints";
115     public static final String OD_NAME_FIRST_PARTY_ENDPOINTS = "first_party_endpoints";
116     public static final String OD_NAME_SERVICE_PROVIDER_ENDPOINTS = "service_provider_endpoints";
117     public static final String OD_NAME_CATEGORY = "category";
118     public static final String OD_NAME_VERSION = "version";
119     public static final String OD_NAME_URL = "url";
120     public static final String OD_NAME_DECLARATION = "declaration";
121     public static final String OD_NAME_SYSTEM_APP_SAFETY_LABEL = "system_app_safety_label";
122     public static final String OD_NAME_SECURITY_LABELS = "security_labels";
123     public static final String OD_NAME_THIRD_PARTY_VERIFICATION = "third_party_verification";
124     public static final String OD_NAME_DATA_LABELS = "data_labels";
125     public static final String OD_NAME_DATA_ACCESSED = "data_accessed";
126     public static final String OD_NAME_DATA_COLLECTED = "data_collected";
127     public static final String OD_NAME_DATA_SHARED = "data_shared";
128     public static final String OD_NAME_PURPOSES = "purposes";
129     public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional";
130     public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional";
131     public static final String OD_NAME_IS_DATA_DELETABLE = "is_data_deletable";
132     public static final String OD_NAME_IS_DATA_ENCRYPTED = "is_data_encrypted";
133     public static final String OD_NAME_EPHEMERAL = "ephemeral";
134 
135     public static final String TRUE_STR = "true";
136     public static final String FALSE_STR = "false";
137 
138     /** Gets the top-level children with the tag name.. */
getChildrenByTagName(Node parentEle, String tagName)139     public static List<Element> getChildrenByTagName(Node parentEle, String tagName) {
140         var elements = XmlUtils.asElementList(parentEle.getChildNodes());
141         return elements.stream()
142                 .filter(e -> e.getTagName().equals(tagName))
143                 .collect(Collectors.toList());
144     }
145 
146     /**
147      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
148      */
getSingleChildElement( Node parentEle, String tagName, Set<String> requiredStrings)149     public static Element getSingleChildElement(
150             Node parentEle, String tagName, Set<String> requiredStrings)
151             throws MalformedXmlException {
152         return getSingleChildElement(parentEle, tagName, requiredStrings.contains(tagName));
153     }
154 
155     /**
156      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
157      */
getSingleChildElement(Node parentEle, String tagName, boolean required)158     public static Element getSingleChildElement(Node parentEle, String tagName, boolean required)
159             throws MalformedXmlException {
160         String parentTagNameForErrorMsg =
161                 (parentEle instanceof Element) ? ((Element) parentEle).getTagName() : "Node";
162         var elements = getChildrenByTagName(parentEle, tagName);
163 
164         if (elements.size() > 1) {
165             throw new MalformedXmlException(
166                     String.format(
167                             "Expected 1 %s in %s but got %s.",
168                             tagName, parentTagNameForErrorMsg, elements.size()));
169         } else if (elements.isEmpty()) {
170             if (required) {
171                 throw new MalformedXmlException(
172                         String.format(
173                                 "Expected 1 %s in %s but got 0.",
174                                 tagName, parentTagNameForErrorMsg));
175             } else {
176                 return null;
177             }
178         }
179         return elements.get(0);
180     }
181 
182     /** Gets the single {@link Element} within {@param elements}. */
getSingleElement(List<Element> elements)183     public static Element getSingleElement(List<Element> elements) {
184         if (elements.size() != 1) {
185             throw new IllegalStateException(
186                     String.format("Expected 1 element in list but got %s.", elements.size()));
187         }
188         return elements.get(0);
189     }
190 
191     /** Converts {@param nodeList} into List of {@link Element}. */
asElementList(NodeList nodeList)192     public static List<Element> asElementList(NodeList nodeList) {
193         List<Element> elementList = new ArrayList<Element>();
194         for (int i = 0; i < nodeList.getLength(); i++) {
195             var elementAsNode = nodeList.item(i);
196             if (elementAsNode instanceof Element) {
197                 elementList.add(((Element) elementAsNode));
198             }
199         }
200         return elementList;
201     }
202 
203     /** Appends {@param children} to the {@param ele}. */
appendChildren(Element ele, List<Element> children)204     public static void appendChildren(Element ele, List<Element> children) {
205         for (Element c : children) {
206             ele.appendChild(c);
207         }
208     }
209 
210     /** Gets the Boolean from the String value. */
fromString(String s)211     private static Boolean fromString(String s) {
212         if (s == null) {
213             return null;
214         }
215         if (s.equals(TRUE_STR)) {
216             return true;
217         } else if (s.equals(FALSE_STR)) {
218             return false;
219         }
220         return null;
221     }
222 
223     /** Creates an on-device PBundle DOM Element with the given attribute name. */
createPbundleEleWithName(Document doc, String name)224     public static Element createPbundleEleWithName(Document doc, String name) {
225         var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP);
226         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
227         return ele;
228     }
229 
230     /** Create an on-device Boolean DOM Element with the given attribute name. */
createOdBooleanEle(Document doc, String name, boolean b)231     public static Element createOdBooleanEle(Document doc, String name, boolean b) {
232         var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN);
233         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
234         ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b));
235         return ele;
236     }
237 
238     /** Sets human-readable bool attribute if non-null. */
maybeSetHrBoolAttr(Element ele, String attrName, Boolean b)239     public static void maybeSetHrBoolAttr(Element ele, String attrName, Boolean b) {
240         if (b != null) {
241             ele.setAttribute(attrName, String.valueOf(b));
242         }
243     }
244 
245     /** Create an on-device Long DOM Element with the given attribute name. */
createOdLongEle(Document doc, String name, long l)246     public static Element createOdLongEle(Document doc, String name, long l) {
247         var ele = doc.createElement(XmlUtils.OD_TAG_LONG);
248         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
249         ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(l));
250         return ele;
251     }
252 
253     /** Create an on-device Long DOM Element with the given attribute name. */
createOdStringEle(Document doc, String name, String val)254     public static Element createOdStringEle(Document doc, String name, String val) {
255         var ele = doc.createElement(XmlUtils.OD_TAG_STRING);
256         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
257         ele.setAttribute(XmlUtils.OD_ATTR_VALUE, val);
258         return ele;
259     }
260 
261     /** Create HR style array DOM Element. */
createHrArray(Document doc, String arrayTagName, List<String> arrayVals)262     public static Element createHrArray(Document doc, String arrayTagName, List<String> arrayVals) {
263         Element arrEle = doc.createElement(arrayTagName);
264         for (String s : arrayVals) {
265             Element itemEle = doc.createElement(XmlUtils.HR_TAG_ITEM);
266             itemEle.setTextContent(s);
267             arrEle.appendChild(itemEle);
268         }
269         return arrEle;
270     }
271 
272     /** Create OD style array DOM Element, which can represent any type but is stored as Strings. */
createOdArray( Document doc, String arrayTag, String arrayName, List<String> arrayVals)273     public static Element createOdArray(
274             Document doc, String arrayTag, String arrayName, List<String> arrayVals) {
275         Element arrEle = doc.createElement(arrayTag);
276         arrEle.setAttribute(XmlUtils.OD_ATTR_NAME, arrayName);
277         arrEle.setAttribute(XmlUtils.OD_ATTR_NUM, String.valueOf(arrayVals.size()));
278         for (String s : arrayVals) {
279             Element itemEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
280             itemEle.setAttribute(XmlUtils.OD_ATTR_VALUE, s);
281             arrEle.appendChild(itemEle);
282         }
283         return arrEle;
284     }
285 
286     /** Returns whether the String is null or empty. */
isNullOrEmpty(String s)287     public static boolean isNullOrEmpty(String s) {
288         return s == null || s.isEmpty();
289     }
290 
291     /** Tries getting required version attribute and throws exception if it doesn't exist */
tryGetVersion(Element ele)292     public static Long tryGetVersion(Element ele) throws MalformedXmlException {
293         long version;
294         try {
295             version = Long.parseLong(ele.getAttribute(XmlUtils.HR_ATTR_VERSION));
296         } catch (Exception e) {
297             throw new MalformedXmlException(
298                     String.format(
299                             "Malformed or missing required version in: %s", ele.getTagName()));
300         }
301         return version;
302     }
303 
304     /** Gets a pipeline-split attribute. */
getPipelineSplitAttr( Element ele, String attrName, Set<String> requiredNames)305     public static List<String> getPipelineSplitAttr(
306             Element ele, String attrName, Set<String> requiredNames) throws MalformedXmlException {
307         return getPipelineSplitAttr(ele, attrName, requiredNames.contains(attrName));
308     }
309 
310     /** Gets a pipeline-split attribute. */
getPipelineSplitAttr(Element ele, String attrName, boolean required)311     public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required)
312             throws MalformedXmlException {
313         List<String> list =
314                 Arrays.stream(ele.getAttribute(attrName).split("\\|")).collect(Collectors.toList());
315         if ((list.isEmpty() || list.get(0).isEmpty())) {
316             if (required) {
317                 throw new MalformedXmlException(
318                         String.format(
319                                 "Delimited string %s was required but missing, in %s.",
320                                 attrName, ele.getTagName()));
321             }
322             return null;
323         }
324         return list;
325     }
326 
327     /**
328      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
329      */
getBoolAttr(Element ele, String attrName, Set<String> requiredStrings)330     public static Boolean getBoolAttr(Element ele, String attrName, Set<String> requiredStrings)
331             throws MalformedXmlException {
332         return getBoolAttr(ele, attrName, requiredStrings.contains(attrName));
333     }
334 
335     /** Gets a Boolean attribute. */
getBoolAttr(Element ele, String attrName, boolean required)336     public static Boolean getBoolAttr(Element ele, String attrName, boolean required)
337             throws MalformedXmlException {
338         Boolean b = XmlUtils.fromString(ele.getAttribute(attrName));
339         if (b == null && required) {
340             throw new MalformedXmlException(
341                     String.format(
342                             "Boolean %s was required but missing, in %s.",
343                             attrName, ele.getTagName()));
344         }
345         return b;
346     }
347 
348     /** Gets a Boolean attribute. */
getOdBoolEle(Element ele, String nameName, Set<String> requiredNames)349     public static Boolean getOdBoolEle(Element ele, String nameName, Set<String> requiredNames)
350             throws MalformedXmlException {
351         return getOdBoolEle(ele, nameName, requiredNames.contains(nameName));
352     }
353 
354     /** Gets a Boolean attribute. */
getOdBoolEle(Element ele, String nameName, boolean required)355     public static Boolean getOdBoolEle(Element ele, String nameName, boolean required)
356             throws MalformedXmlException {
357         List<Element> boolEles =
358                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_BOOLEAN).stream()
359                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
360                         .collect(Collectors.toList());
361         if (boolEles.size() > 1) {
362             throw new MalformedXmlException(
363                     String.format(
364                             "Found more than one boolean %s in %s.", nameName, ele.getTagName()));
365         }
366         if (boolEles.isEmpty()) {
367             if (required) {
368                 throw new MalformedXmlException(
369                         String.format("Found no boolean %s in %s.", nameName, ele.getTagName()));
370             }
371             return null;
372         }
373         Element boolEle = boolEles.get(0);
374 
375         Boolean b = XmlUtils.fromString(boolEle.getAttribute(XmlUtils.OD_ATTR_VALUE));
376         if (b == null && required) {
377             throw new MalformedXmlException(
378                     String.format(
379                             "Boolean %s was required but missing, in %s.",
380                             nameName, ele.getTagName()));
381         }
382         return b;
383     }
384 
385     /** Gets an on-device Long attribute. */
getOdLongEle(Element ele, String nameName, boolean required)386     public static Long getOdLongEle(Element ele, String nameName, boolean required)
387             throws MalformedXmlException {
388         List<Element> longEles =
389                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_LONG).stream()
390                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
391                         .collect(Collectors.toList());
392         if (longEles.size() > 1) {
393             throw new MalformedXmlException(
394                     String.format(
395                             "Found more than one long %s in %s.", nameName, ele.getTagName()));
396         }
397         if (longEles.isEmpty()) {
398             if (required) {
399                 throw new MalformedXmlException(
400                         String.format("Found no long %s in %s.", nameName, ele.getTagName()));
401             }
402             return null;
403         }
404         Element longEle = longEles.get(0);
405         Long l = null;
406         try {
407             l = Long.parseLong(longEle.getAttribute(XmlUtils.OD_ATTR_VALUE));
408         } catch (NumberFormatException e) {
409             throw new MalformedXmlException(
410                     String.format(
411                             "%s in %s was not formatted as long", nameName, ele.getTagName()));
412         }
413         return l;
414     }
415 
416     /** Gets an on-device String attribute. */
getOdStringEle(Element ele, String nameName, Set<String> requiredNames)417     public static String getOdStringEle(Element ele, String nameName, Set<String> requiredNames)
418             throws MalformedXmlException {
419         return getOdStringEle(ele, nameName, requiredNames.contains(nameName));
420     }
421 
422     /** Gets an on-device String attribute. */
getOdStringEle(Element ele, String nameName, boolean required)423     public static String getOdStringEle(Element ele, String nameName, boolean required)
424             throws MalformedXmlException {
425         List<Element> eles =
426                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING).stream()
427                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
428                         .collect(Collectors.toList());
429         if (eles.size() > 1) {
430             throw new MalformedXmlException(
431                     String.format(
432                             "Found more than one string %s in %s.", nameName, ele.getTagName()));
433         }
434         if (eles.isEmpty()) {
435             if (required) {
436                 throw new MalformedXmlException(
437                         String.format("Found no string %s in %s.", nameName, ele.getTagName()));
438             }
439             return null;
440         }
441         String str = eles.get(0).getAttribute(XmlUtils.OD_ATTR_VALUE);
442         if (XmlUtils.isNullOrEmpty(str) && required) {
443             throw new MalformedXmlException(
444                     String.format(
445                             "%s in %s was empty or missing value", nameName, ele.getTagName()));
446         }
447         return str;
448     }
449 
450     /** Gets a OD Pbundle Element attribute with the specified name. */
getOdPbundleWithName( Element ele, String nameName, Set<String> requiredStrings)451     public static Element getOdPbundleWithName(
452             Element ele, String nameName, Set<String> requiredStrings)
453             throws MalformedXmlException {
454         return getOdPbundleWithName(ele, nameName, requiredStrings.contains(nameName));
455     }
456 
457     /** Gets a OD Pbundle Element attribute with the specified name. */
getOdPbundleWithName(Element ele, String nameName, boolean required)458     public static Element getOdPbundleWithName(Element ele, String nameName, boolean required)
459             throws MalformedXmlException {
460         List<Element> eles =
461                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_PBUNDLE_AS_MAP).stream()
462                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
463                         .collect(Collectors.toList());
464         if (eles.size() > 1) {
465             throw new MalformedXmlException(
466                     String.format(
467                             "Found more than one pbundle %s in %s.", nameName, ele.getTagName()));
468         }
469         if (eles.isEmpty()) {
470             if (required) {
471                 throw new MalformedXmlException(
472                         String.format("Found no pbundle %s in %s.", nameName, ele.getTagName()));
473             }
474             return null;
475         }
476         return eles.get(0);
477     }
478 
479     /**
480      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
481      */
getStringAttr(Element ele, String attrName, Set<String> requiredStrings)482     public static String getStringAttr(Element ele, String attrName, Set<String> requiredStrings)
483             throws MalformedXmlException {
484         return getStringAttr(ele, attrName, requiredStrings.contains(attrName));
485     }
486 
487     /** Gets a required String attribute. */
getStringAttr(Element ele, String attrName)488     public static String getStringAttr(Element ele, String attrName) throws MalformedXmlException {
489         return getStringAttr(ele, attrName, true);
490     }
491 
492     /** Gets a String attribute; throws exception if required and non-existent. */
getStringAttr(Element ele, String attrName, boolean required)493     public static String getStringAttr(Element ele, String attrName, boolean required)
494             throws MalformedXmlException {
495         String s = ele.getAttribute(attrName);
496         if (isNullOrEmpty(s)) {
497             if (required) {
498                 throw new MalformedXmlException(
499                         String.format(
500                                 "Malformed or missing required %s in: %s",
501                                 attrName, ele.getTagName()));
502             } else {
503                 return null;
504             }
505         }
506         return s;
507     }
508 
509     /** Gets on-device style int array. */
getOdIntArray(Element ele, String nameName, boolean required)510     public static List<Integer> getOdIntArray(Element ele, String nameName, boolean required)
511             throws MalformedXmlException {
512         List<Element> intArrayEles =
513                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_INT_ARRAY).stream()
514                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
515                         .collect(Collectors.toList());
516         if (intArrayEles.size() > 1) {
517             throw new MalformedXmlException(
518                     String.format("Found more than one %s in %s.", nameName, ele.getTagName()));
519         }
520         if (intArrayEles.isEmpty()) {
521             if (required) {
522                 throw new MalformedXmlException(
523                         String.format("Found no %s in %s.", nameName, ele.getTagName()));
524             }
525             return null;
526         }
527         Element intArrayEle = intArrayEles.get(0);
528         List<Element> itemEles = XmlUtils.getChildrenByTagName(intArrayEle, XmlUtils.OD_TAG_ITEM);
529         List<Integer> ints = new ArrayList<Integer>();
530         for (Element itemEle : itemEles) {
531             ints.add(Integer.parseInt(XmlUtils.getStringAttr(itemEle, XmlUtils.OD_ATTR_VALUE)));
532         }
533         return ints;
534     }
535 
536     /** Gets human-readable style String array. */
getHrItemsAsStrings( Element parent, String elementName, Set<String> requiredNames)537     public static List<String> getHrItemsAsStrings(
538             Element parent, String elementName, Set<String> requiredNames)
539             throws MalformedXmlException {
540         return getHrItemsAsStrings(parent, elementName, requiredNames.contains(elementName));
541     }
542 
543     /** Gets human-readable style String array. */
getHrItemsAsStrings( Element parent, String elementName, boolean required)544     public static List<String> getHrItemsAsStrings(
545             Element parent, String elementName, boolean required) throws MalformedXmlException {
546 
547         List<Element> arrayEles = XmlUtils.getChildrenByTagName(parent, elementName);
548         if (arrayEles.size() > 1) {
549             throw new MalformedXmlException(
550                     String.format(
551                             "Found more than one %s in %s.", elementName, parent.getTagName()));
552         }
553         if (arrayEles.isEmpty()) {
554             if (required) {
555                 throw new MalformedXmlException(
556                         String.format("Found no %s in %s.", elementName, parent.getTagName()));
557             }
558             return null;
559         }
560         Element arrayEle = arrayEles.get(0);
561         List<Element> itemEles = XmlUtils.getChildrenByTagName(arrayEle, XmlUtils.HR_TAG_ITEM);
562         List<String> strs = new ArrayList<String>();
563         for (Element itemEle : itemEles) {
564             strs.add(itemEle.getTextContent());
565         }
566         return strs;
567     }
568 
569     /** Gets on-device style String array. */
getOdStringArray( Element ele, String nameName, Set<String> requiredNames)570     public static List<String> getOdStringArray(
571             Element ele, String nameName, Set<String> requiredNames) throws MalformedXmlException {
572         return getOdStringArray(ele, nameName, requiredNames.contains(nameName));
573     }
574 
575     /** Gets on-device style String array. */
getOdStringArray(Element ele, String nameName, boolean required)576     public static List<String> getOdStringArray(Element ele, String nameName, boolean required)
577             throws MalformedXmlException {
578         List<Element> arrayEles =
579                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING_ARRAY).stream()
580                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
581                         .collect(Collectors.toList());
582         if (arrayEles.size() > 1) {
583             throw new MalformedXmlException(
584                     String.format(
585                             "Found more than one string array %s in %s.",
586                             nameName, ele.getTagName()));
587         }
588         if (arrayEles.isEmpty()) {
589             if (required) {
590                 throw new MalformedXmlException(
591                         String.format(
592                                 "Found no string array %s in %s.", nameName, ele.getTagName()));
593             }
594             return null;
595         }
596         Element arrayEle = arrayEles.get(0);
597         List<Element> itemEles = XmlUtils.getChildrenByTagName(arrayEle, XmlUtils.OD_TAG_ITEM);
598         List<String> strs = new ArrayList<String>();
599         for (Element itemEle : itemEles) {
600             strs.add(XmlUtils.getStringAttr(itemEle, XmlUtils.OD_ATTR_VALUE, true));
601         }
602         return strs;
603     }
604 
605     /** Throws if extraneous child elements detected */
throwIfExtraneousChildrenHr(Element ele, Set<String> expectedChildNames)606     public static void throwIfExtraneousChildrenHr(Element ele, Set<String> expectedChildNames)
607             throws MalformedXmlException {
608         var childEles = XmlUtils.asElementList(ele.getChildNodes());
609         List<Element> extraneousEles =
610                 childEles.stream()
611                         .filter(e -> !expectedChildNames.contains(e.getTagName()))
612                         .collect(Collectors.toList());
613         if (!extraneousEles.isEmpty()) {
614             throw new MalformedXmlException(
615                     String.format(
616                             "Unexpected element(s) %s in %s.",
617                             extraneousEles.stream()
618                                     .map(Element::getTagName)
619                                     .collect(Collectors.joining(",")),
620                             ele.getTagName()));
621         }
622     }
623 
624     /** Throws if extraneous child elements detected */
throwIfExtraneousChildrenOd(Element ele, Set<String> expectedChildNames)625     public static void throwIfExtraneousChildrenOd(Element ele, Set<String> expectedChildNames)
626             throws MalformedXmlException {
627         var allChildElements = XmlUtils.asElementList(ele.getChildNodes());
628         List<Element> extraneousEles =
629                 allChildElements.stream()
630                         .filter(
631                                 e ->
632                                         !e.getAttribute(XmlUtils.OD_ATTR_NAME).isEmpty()
633                                                 && !expectedChildNames.contains(
634                                                         e.getAttribute(XmlUtils.OD_ATTR_NAME)))
635                         .collect(Collectors.toList());
636         if (!extraneousEles.isEmpty()) {
637             throw new MalformedXmlException(
638                     String.format(
639                             "Unexpected element(s) in %s: %s",
640                             ele.getTagName(),
641                             extraneousEles.stream()
642                                     .map(
643                                             e ->
644                                                     String.format(
645                                                             "%s name=%s",
646                                                             e.getTagName(),
647                                                             e.getAttribute(XmlUtils.OD_ATTR_NAME)))
648                                     .collect(Collectors.joining(","))));
649         }
650     }
651 
652     /** Throws if extraneous attributes detected */
throwIfExtraneousAttributes(Element ele, Set<String> expectedAttrNames)653     public static void throwIfExtraneousAttributes(Element ele, Set<String> expectedAttrNames)
654             throws MalformedXmlException {
655         var attrs = ele.getAttributes();
656         List<String> attrNames = new ArrayList<>();
657         for (int i = 0; i < attrs.getLength(); i++) {
658             attrNames.add(attrs.item(i).getNodeName());
659         }
660         List<String> extraneousAttrs =
661                 attrNames.stream()
662                         .filter(s -> !expectedAttrNames.contains(s))
663                         .collect(Collectors.toList());
664         if (!extraneousAttrs.isEmpty()) {
665             throw new MalformedXmlException(
666                     String.format(
667                             "Unexpected attr(s) %s in %s.",
668                             String.join(",", extraneousAttrs), ele.getTagName()));
669         }
670     }
671 
672     /**
673      * Utility method for making a List from one element, to support easier refactoring if needed.
674      * For example, List.of() doesn't support null elements.
675      */
listOf(Element e)676     public static List<Element> listOf(Element e) {
677         return Arrays.asList(e);
678     }
679 
680     /**
681      * Gets the most recent version of fields in the mapping. This way when a new version is
682      * released, we only need to update the mappings that were modified. The rest will fall back to
683      * the most recent previous version.
684      */
getMostRecentVersion( Map<Long, Set<String>> versionToFieldsMapping, long version)685     public static Set<String> getMostRecentVersion(
686             Map<Long, Set<String>> versionToFieldsMapping, long version)
687             throws MalformedXmlException {
688         long bestVersion = 0;
689         Set<String> bestSet = null;
690         for (Map.Entry<Long, Set<String>> entry : versionToFieldsMapping.entrySet()) {
691             if (entry.getKey() > bestVersion && entry.getKey() <= version) {
692                 bestVersion = entry.getKey();
693                 bestSet = entry.getValue();
694             }
695         }
696         if (bestSet == null) {
697             throw new MalformedXmlException("Unexpected version: " + version);
698         }
699         return bestSet;
700     }
701 }
702