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