1 package org.unicode.cldr.unittest; 2 3 import com.google.common.collect.ImmutableSet; 4 import java.io.File; 5 import java.util.ArrayList; 6 import java.util.Arrays; 7 import java.util.Collection; 8 import java.util.HashMap; 9 import java.util.HashSet; 10 import java.util.Iterator; 11 import java.util.LinkedHashSet; 12 import java.util.Map; 13 import java.util.Map.Entry; 14 import java.util.Set; 15 import java.util.TreeSet; 16 import org.unicode.cldr.util.CLDRConfig; 17 import org.unicode.cldr.util.CLDRFile; 18 import org.unicode.cldr.util.CLDRFile.Status; 19 import org.unicode.cldr.util.CLDRPaths; 20 import org.unicode.cldr.util.ChainedMap; 21 import org.unicode.cldr.util.ChainedMap.M3; 22 import org.unicode.cldr.util.ChainedMap.M4; 23 import org.unicode.cldr.util.ChainedMap.M5; 24 import org.unicode.cldr.util.CldrUtility; 25 import org.unicode.cldr.util.DtdData; 26 import org.unicode.cldr.util.DtdData.Attribute; 27 import org.unicode.cldr.util.DtdData.Element; 28 import org.unicode.cldr.util.DtdData.ElementType; 29 import org.unicode.cldr.util.DtdType; 30 import org.unicode.cldr.util.Pair; 31 import org.unicode.cldr.util.PathHeader; 32 import org.unicode.cldr.util.PathHeader.Factory; 33 import org.unicode.cldr.util.PathHeader.PageId; 34 import org.unicode.cldr.util.PathHeader.SectionId; 35 import org.unicode.cldr.util.PathStarrer; 36 import org.unicode.cldr.util.StandardCodes; 37 import org.unicode.cldr.util.SupplementalDataInfo; 38 import org.unicode.cldr.util.XMLFileReader; 39 import org.unicode.cldr.util.XPathParts; 40 41 public class TestPaths extends TestFmwkPlus { 42 static final CLDRConfig testInfo = CLDRConfig.getInstance(); 43 static final SupplementalDataInfo SDI = 44 testInfo.getSupplementalDataInfo(); // Load first, before XPartPaths is called 45 main(String[] args)46 public static void main(String[] args) { 47 new TestPaths().run(args); 48 } 49 VerifyEnglishVsRoot()50 public void VerifyEnglishVsRoot() { 51 HashSet<String> rootPaths = new HashSet<>(); 52 testInfo.getRoot().forEach(rootPaths::add); 53 HashSet<String> englishPaths = new HashSet<>(); 54 testInfo.getEnglish().forEach(englishPaths::add); 55 englishPaths.removeAll(rootPaths); 56 if (englishPaths.size() == 0) { 57 return; 58 } 59 Factory phf = PathHeader.getFactory(testInfo.getEnglish()); 60 Status status = new Status(); 61 Set<PathHeader> suspiciousPaths = new TreeSet<>(); 62 Set<PathHeader> errorPaths = new TreeSet<>(); 63 ImmutableSet<String> SKIP_VARIANT = 64 ImmutableSet.of( 65 "ps-variant", 66 "ug-variant", 67 "ky-variant", 68 "az-short", 69 "Arab-variant", 70 "am-variant", 71 "pm-variant"); 72 for (String path : englishPaths) { 73 // skip aliases, other counts 74 if (!status.pathWhereFound.equals(path) || path.contains("[@count=\"one\"]")) { 75 continue; 76 } 77 PathHeader ph = phf.fromPath(path); 78 if (ph.getSectionId() == SectionId.Special || ph.getCode().endsWith("-name-other")) { 79 continue; 80 } 81 if (path.contains("@alt") 82 && !SKIP_VARIANT.contains(ph.getCode()) 83 && ph.getPageId() != PageId.Alphabetic_Information) { 84 errorPaths.add(ph); 85 } else { 86 suspiciousPaths.add(ph); 87 } 88 } 89 if (errorPaths.size() != 0) { 90 errln("Error: paths in English but not root:" + getPaths(errorPaths)); 91 } 92 logln("Suspicious: paths in English but not root:" + getPaths(suspiciousPaths)); 93 } 94 getPaths(Set<PathHeader> altPaths)95 private String getPaths(Set<PathHeader> altPaths) { 96 StringBuilder b = new StringBuilder(); 97 for (PathHeader path : altPaths) { 98 b.append("\n\t\t") 99 .append(path) 100 .append(":\t") 101 .append(testInfo.getEnglish().getStringValue(path.getOriginalPath())); 102 } 103 return b.toString(); 104 } 105 106 /** 107 * For each locale to test, loop through all the paths, including "extra" paths, checking for 108 * each path: checkFullpathValue; checkPrettyPaths 109 */ TestPathHeadersAndValues()110 public void TestPathHeadersAndValues() { 111 /* 112 * Use the pathsSeen hash to keep track of which paths have 113 * already been seen. Since the test checkPrettyPaths isn't really 114 * locale-dependent, run it only once for each path, for the first 115 * locale in which the path occurs. 116 */ 117 Set<String> pathsSeen = new HashSet<>(); 118 CLDRFile englishFile = testInfo.getCldrFactory().make("en", true); 119 PathHeader.Factory phf = PathHeader.getFactory(englishFile); 120 Status status = new Status(); 121 for (String locale : getLocalesToTest()) { 122 if (!StandardCodes.isLocaleAtLeastBasic(locale)) { 123 continue; 124 } 125 CLDRFile file = testInfo.getCLDRFile(locale, true); 126 logln("Testing path headers and values for locale => " + locale); 127 final Collection<String> extraPaths = file.getExtraPaths(); 128 for (Iterator<String> it = file.iterator(); it.hasNext(); ) { 129 String path = it.next(); 130 if (extraPaths.contains(path)) { 131 continue; 132 } 133 checkFullpathValue(path, file, locale, status, false /* not extra path */); 134 if (!pathsSeen.contains(path)) { 135 pathsSeen.add(path); 136 checkPrettyPaths(path, phf); 137 } 138 } 139 for (String path : extraPaths) { 140 checkFullpathValue(path, file, locale, status, true /* extra path */); 141 if (!pathsSeen.contains(path)) { 142 pathsSeen.add(path); 143 checkPrettyPaths(path, phf); 144 } 145 } 146 } 147 } 148 149 /** 150 * For the given path and CLDRFile, check that fullPath, value, and source are all non-null. 151 * 152 * <p>Allow null value for some exceptional extra paths. 153 * 154 * @param path the path, such as '//ldml/dates/fields/field[@type="tue"]/relative[@type="1"]' 155 * @param file the CLDRFile 156 * @param locale the locale string 157 * @param status the Status to be used/set by getSourceLocaleID 158 * @param isExtraPath true if the path is an "extra" path, else false 159 */ checkFullpathValue( String path, CLDRFile file, String locale, Status status, boolean isExtraPath)160 private void checkFullpathValue( 161 String path, CLDRFile file, String locale, Status status, boolean isExtraPath) { 162 String fullPath = file.getFullXPath(path); 163 String value = file.getStringValue(path); 164 String source = file.getSourceLocaleID(path, status); 165 166 assertEquals("CanonicalOrder", XPathParts.getFrozenInstance(path).toString(), path); 167 168 if (fullPath == null) { 169 errln("Locale: " + locale + ",\t Null FullPath: " + path); 170 } else if (!path.equals(fullPath)) { 171 assertEquals( 172 "CanonicalOrder (FP)", 173 XPathParts.getFrozenInstance(fullPath).toString(), 174 fullPath); 175 } 176 177 if (value == null) { 178 if (allowsExtraPath(path, isExtraPath)) { 179 return; 180 } 181 if (CldrUtility.INHERITANCE_MARKER.equals( 182 file.getUnresolved().getStringValue(fullPath))) { 183 logKnownIssue( 184 "cldrbug:16209", "Remove this clause (and any paths that still fail)"); 185 // When that ticket is resolved, then comment this clause out. 186 // But leave that comment for future guidance where someone wants to move paths from 187 // root to extraPaths. 188 return; 189 } 190 errln( 191 "Locale: " 192 + locale 193 + ",\t Value=null, \tPath: " 194 + path 195 + ",\t IsExtraPath: " 196 + isExtraPath); 197 } 198 199 if (source == null) { 200 errln("Locale: " + locale + ",\t Source=null, \tPath: " + path); 201 } 202 203 if (status.pathWhereFound == null) { 204 errln("Locale: " + locale + ",\t Path=null, \tPath: " + path); 205 } 206 } 207 208 final ImmutableSet<String> ALLOWED_NULL = 209 ImmutableSet.of( 210 "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Enderbury\"]/exemplarCity"); 211 212 /** Is the path allowed to have a null value? */ allowsExtraPath(String path, boolean isExtraPath)213 public boolean allowsExtraPath(String path, boolean isExtraPath) { 214 return (isExtraPath && extraPathAllowsNullValue(path)) || ALLOWED_NULL.contains(path); 215 } 216 217 /** 218 * Is the given extra path exceptional in the sense that null value is allowed? 219 * 220 * @param path the extra path 221 * @return true if null value is allowed for path, else false 222 * <p>As of 2019-08-09, null values are found for many "metazone" paths like: 223 * //ldml/dates/timeZoneNames/metazone[@type="Galapagos"]/long/standard for many locales. 224 * Also for some "zone" paths like: 225 * //ldml/dates/timeZoneNames/zone[@type="Pacific/Honolulu"]/short/generic for locales 226 * including root, ja, and ar. Also for some "dayPeriods" paths like 227 * //ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="stand-alone"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="midnight"] 228 * only for these six locales: bs_Cyrl, bs_Cyrl_BA, pa_Arab, pa_Arab_PK, uz_Arab, 229 * uz_Arab_AF. 230 * <p>This function is nearly identical to the JavaScript function with the same name. Keep 231 * the two functions consistent with each other. It would be more ideal if this knowledge 232 * were encapsulated on the server and the client didn't need to know about it. The server 233 * could send the client special fallback values instead of null. 234 * <p>Extra paths are generated by CLDRFile.getRawExtraPathsPrivate; this function may need 235 * updating (to allow null for other paths) if that function changes. 236 * <p>Reference: https://unicode-org.atlassian.net/browse/CLDR-11238 237 */ extraPathAllowsNullValue(String path)238 private boolean extraPathAllowsNullValue(String path) { 239 if (path.contains("/timeZoneNames/metazone") 240 || path.contains("/timeZoneNames/zone") 241 || path.contains("/dayPeriods/dayPeriodContext") 242 || path.contains("/unitPattern") 243 || path.contains("/gender") 244 || path.contains("/caseMinimalPairs") 245 || path.contains("/genderMinimalPairs") 246 || path.contains("/sampleName") 247 // || 248 // path.equals("//ldml/dates/timeZoneNames/zone[@type=\"Australia/Currie\"]/exemplarCity") 249 // || 250 // path.equals("//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Enderbury\"]/exemplarCity") 251 // + 252 ) { 253 return true; 254 } 255 return false; 256 } 257 258 /** 259 * Check that the given path and PathHeader.Factory undergo correct roundtrip conversion between 260 * original and pretty paths. 261 * 262 * @param path the path string 263 * @param phf the PathHeader.Factory 264 */ checkPrettyPaths(String path, PathHeader.Factory phf)265 private void checkPrettyPaths(String path, PathHeader.Factory phf) { 266 if (path.endsWith("/alias")) { 267 return; 268 } 269 logln("Testing ==> " + path); 270 String prettied = phf.fromPath(path).toString(); 271 String unprettied = phf.fromPath(path).getOriginalPath(); 272 if (!path.equals(unprettied)) { 273 errln("Path Header doesn't roundtrip:\t" + path + "\t" + prettied + "\t" + unprettied); 274 } else { 275 logln(prettied + "\t" + path); 276 } 277 } 278 getLocalesToTest()279 private Collection<String> getLocalesToTest() { 280 return params.inclusion <= 5 281 ? Arrays.asList("root", "en", "ja", "ar", "de", "ru") 282 : params.inclusion < 10 283 ? testInfo.getCldrFactory().getAvailableLanguages() 284 : testInfo.getCldrFactory().getAvailable(); 285 } 286 287 /** 288 * find all the items that are deprecated, but appear in paths and the items that aren't 289 * deprecated, but don't appear in paths 290 */ 291 static final class CheckDeprecated { 292 M5<DtdType, String, String, String, Boolean> data = 293 ChainedMap.of( 294 new HashMap<DtdType, Object>(), 295 new HashMap<String, Object>(), 296 new HashMap<String, Object>(), 297 new HashMap<String, Object>(), 298 Boolean.class); 299 private TestPaths testPaths; 300 CheckDeprecated(TestPaths testPaths)301 public CheckDeprecated(TestPaths testPaths) { 302 this.testPaths = testPaths; 303 } 304 305 static final Set<String> ALLOWED = 306 new HashSet<>(Arrays.asList("postalCodeData", "postCodeRegex")); 307 static final Set<String> OK_IF_MISSING = 308 new HashSet<>(Arrays.asList("alt", "draft", "references")); 309 check(XPathParts parts, String fullName)310 public boolean check(XPathParts parts, String fullName) { 311 DtdData dtdData = parts.getDtdData(); 312 for (int i = 0; i < parts.size(); ++i) { 313 String elementName = parts.getElement(i); 314 if (dtdData.isDeprecated(elementName, "*", "*")) { 315 if (ALLOWED.contains(elementName)) { 316 return false; 317 } 318 testPaths.errln( 319 "Deprecated element in data: " 320 + dtdData.dtdType 321 + ":" 322 + elementName 323 + " \t;" 324 + fullName); 325 return true; 326 } 327 data.put(dtdData.dtdType, elementName, "*", "*", true); 328 for (Entry<String, String> attributeNValue : parts.getAttributes(i).entrySet()) { 329 String attributeName = attributeNValue.getKey(); 330 if (dtdData.isDeprecated(elementName, attributeName, "*")) { 331 if (attributeName.equals("draft")) { 332 testPaths.errln( 333 "Deprecated attribute in data: " 334 + dtdData.dtdType 335 + ":" 336 + elementName 337 + ":" 338 + attributeName 339 + " \t;" 340 + fullName 341 + " - consider adding to DtdData.DRAFT_ON_NON_LEAF_ALLOWED if you are sure this is ok."); 342 } else { 343 testPaths.errln( 344 "Deprecated attribute in data: " 345 + dtdData.dtdType 346 + ":" 347 + elementName 348 + ":" 349 + attributeName 350 + " \t;" 351 + fullName); 352 } 353 return true; 354 } 355 String attributeValue = attributeNValue.getValue(); 356 if (dtdData.isDeprecated(elementName, attributeName, attributeValue)) { 357 testPaths.errln( 358 "Deprecated attribute value in data: " 359 + dtdData.dtdType 360 + ":" 361 + elementName 362 + ":" 363 + attributeName 364 + ":" 365 + attributeValue 366 + " \t;" 367 + fullName); 368 return true; 369 } 370 data.put(dtdData.dtdType, elementName, attributeName, "*", true); 371 data.put(dtdData.dtdType, elementName, attributeName, attributeValue, true); 372 } 373 } 374 return false; 375 } 376 show(int inclusion)377 public void show(int inclusion) { 378 for (DtdType dtdType : DtdType.values()) { 379 if (dtdType.getStatus() != DtdType.DtdStatus.active) { 380 continue; 381 } 382 if (dtdType == DtdType.ldmlICU) { 383 continue; 384 } 385 M4<String, String, String, Boolean> infoEAV = data.get(dtdType); 386 if (infoEAV == null) { 387 testPaths.warnln("Data doesn't contain: " + dtdType); 388 continue; 389 } 390 DtdData dtdData = DtdData.getInstance(dtdType); 391 for (Element element : dtdData.getElements()) { 392 if (element.isDeprecated() 393 || element == dtdData.ANY 394 || element == dtdData.PCDATA) { 395 continue; 396 } 397 M3<String, String, Boolean> infoAV = infoEAV.get(element.name); 398 if (infoAV == null) { 399 testPaths.logln("Data doesn't contain: " + dtdType + ":" + element.name); 400 continue; 401 } 402 403 for (Attribute attribute : element.getAttributes().keySet()) { 404 if (attribute.isDeprecated() || OK_IF_MISSING.contains(attribute.name)) { 405 continue; 406 } 407 Map<String, Boolean> infoV = infoAV.get(attribute.name); 408 if (infoV == null) { 409 testPaths.logln( 410 "Data doesn't contain: " 411 + dtdType 412 + ":" 413 + element.name 414 + ":" 415 + attribute.name); 416 continue; 417 } 418 for (String value : attribute.values.keySet()) { 419 if (attribute.isDeprecatedValue(value)) { 420 continue; 421 } 422 if (!infoV.containsKey(value)) { 423 testPaths.logln( 424 "Data doesn't contain: " 425 + dtdType 426 + ":" 427 + element.name 428 + ":" 429 + attribute.name 430 + ":" 431 + value); 432 } 433 } 434 } 435 } 436 } 437 } 438 } 439 TestNonLdml()440 public void TestNonLdml() { 441 int maxPerDirectory = getInclusion() <= 5 ? 20 : Integer.MAX_VALUE; 442 CheckDeprecated checkDeprecated = new CheckDeprecated(this); 443 PathStarrer starrer = new PathStarrer(); 444 StringBuilder removed = new StringBuilder(); 445 Set<String> nonFinalValues = new LinkedHashSet<>(); 446 Set<String> skipLast = new HashSet(Arrays.asList("version", "generation")); 447 String[] normalizedPath = {""}; 448 449 int counter = 0; 450 for (String directory : Arrays.asList("keyboards/", "common/", "seed/", "exemplars/")) { 451 String dirPath = CLDRPaths.BASE_DIRECTORY + directory; 452 for (String fileName : new File(dirPath).list()) { 453 File dir2 = new File(dirPath + fileName); 454 if (!dir2.isDirectory() 455 || (dir2.getName().equals("import") 456 && directory.equals("keyboards/")) // has a different root element 457 || fileName.equals("properties") // TODO as flat files 458 // || fileName.equals(".DS_Store") 459 // || ChartDelta.LDML_DIRECTORIES.contains(dir) 460 // || fileName.equals("dtd") // TODO as flat files 461 // || fileName.equals(".project") // TODO as flat files 462 // //|| dir.equals("uca") // TODO as flat files 463 ) { 464 continue; 465 } 466 467 Set<Pair<String, String>> seen = new HashSet<>(); 468 Set<String> seenStarred = new HashSet<>(); 469 int count = 0; 470 Set<Element> haveErrorsAlready = new HashSet<>(); 471 for (String file : dir2.list()) { 472 if (!file.endsWith(".xml")) { 473 continue; 474 } 475 if (++count > maxPerDirectory) { 476 break; 477 } 478 String fullName = dir2 + "/" + file; 479 for (Pair<String, String> pathValue : 480 XMLFileReader.loadPathValues( 481 fullName, new ArrayList<Pair<String, String>>(), true)) { 482 String path = pathValue.getFirst(); 483 final String value = pathValue.getSecond(); 484 XPathParts parts = XPathParts.getFrozenInstance(path); 485 DtdData dtdData = parts.getDtdData(); 486 DtdType type = dtdData.dtdType; 487 488 String finalElementString = parts.getElement(-1); 489 Element finalElement = dtdData.getElementFromName().get(finalElementString); 490 if (!haveErrorsAlready.contains(finalElement)) { 491 ElementType elementType = finalElement.getType(); 492 Element.ValueConstraint requirement = finalElement.getValueConstraint(); 493 if (requirement == Element.ValueConstraint.empty && !value.isEmpty() 494 || requirement == Element.ValueConstraint.nonempty 495 && value.isEmpty()) { 496 finalElement.getValueConstraint(); // for debugging 497 errln( 498 "PCDATA ≠ emptyValue inconsistency:" 499 + "\tfile=" 500 + fileName 501 + "/" 502 + file 503 + "\telementType=" 504 + elementType 505 + "\tvalue=«" 506 + value 507 + "»" 508 + "\tpath=" 509 + path); 510 haveErrorsAlready.add(finalElement); // suppress all but first error 511 } 512 } 513 514 if (checkDeprecated.check(parts, fullName)) { 515 break; 516 } 517 518 String last = parts.getElement(-1); 519 if (skipLast.contains(last)) { 520 continue; 521 } 522 523 checkParts(fileName + "/" + file, parts); 524 525 String dpath = CLDRFile.getDistinguishingXPath(path, normalizedPath); 526 if (!dpath.equals(path)) { 527 checkParts(fileName + "/" + file, dpath); 528 } 529 if (!normalizedPath.equals(path) && !normalizedPath[0].equals(dpath)) { 530 checkParts(fileName + "/" + file, normalizedPath[0]); 531 } 532 XPathParts mutableParts = parts.cloneAsThawed(); 533 counter = 534 removeNonDistinguishing( 535 mutableParts, dtdData, counter, removed, nonFinalValues); 536 String cleaned = mutableParts.toString(); 537 Pair<String, String> pair = 538 Pair.of(type == DtdType.ldml ? file : type.toString(), cleaned); 539 if (seen.contains(pair)) { 540 // parts.set(path); 541 // removeNonDistinguishing(parts, dtdData, 542 // counter, removed, nonFinalValues); 543 if (type != DtdType.keyboardTest3 544 || !logKnownIssue( 545 "CLDR-17398", 546 "keyboardTest data appears as duplicate xpaths")) { 547 errln( 548 "Duplicate " 549 + type.toString() 550 + ": " 551 + file 552 + ", " 553 + path 554 + ", " 555 + cleaned 556 + ", " 557 + value); 558 } 559 } else { 560 seen.add(pair); 561 if (!nonFinalValues.isEmpty()) { 562 String starredPath = starrer.set(path); 563 if (!seenStarred.contains(starredPath)) { 564 seenStarred.add(starredPath); 565 logln("Non-node values: " + nonFinalValues + "\t" + path); 566 } 567 } 568 if (isVerbose()) { 569 String starredPath = starrer.set(path); 570 if (!seenStarred.contains(starredPath)) { 571 seenStarred.add(starredPath); 572 logln("@" + "\t" + cleaned + "\t" + removed); 573 } 574 } 575 } 576 } 577 } 578 } 579 } 580 checkDeprecated.show(getInclusion()); 581 } 582 checkParts(String file, String path)583 private void checkParts(String file, String path) { 584 checkParts(file, XPathParts.getFrozenInstance(path)); 585 } 586 checkParts(String file, XPathParts parts)587 public void checkParts(String file, XPathParts parts) { 588 DtdData dtdData = parts.getDtdData(); 589 Element current = dtdData.ROOT; 590 for (int i = 0; i < parts.size(); ++i) { 591 String elementName = parts.getElement(i); 592 if (i == 0) { 593 assertEquals("root", current.name, elementName); 594 } else { 595 current = current.getChildNamed(elementName); 596 if (!assertNotNull("element", current)) { 597 return; // failed 598 } 599 assertFalse(file + "/" + elementName + " deprecated", current.isDeprecated()); 600 } 601 for (String attributeName : parts.getAttributeKeys(i)) { 602 Attribute attribute = current.getAttributeNamed(attributeName); 603 if (!assertNotNull("attribute", attribute)) { 604 return; // failed 605 } 606 assertFalse( 607 file + "/" + elementName + "@" + attributeName + " deprecated", 608 attribute.isDeprecated()); 609 610 String value = parts.getAttributeValue(i, attributeName); 611 switch (attribute.getValueStatus(value)) { 612 case valid: 613 break; 614 default: 615 errln( 616 file 617 + "/" 618 + elementName 619 + "@" 620 + attributeName 621 + ", expected match to: " 622 + attribute.getMatchString() 623 + " actual: «" 624 + value 625 + "»"); 626 attribute.getValueStatus(value); 627 break; 628 } 629 } 630 } 631 } 632 633 static final Set<String> SKIP_NON_NODE = 634 new HashSet<>(Arrays.asList("references", "visibility", "access")); 635 636 /** 637 * @param parts the thawed XPathParts (can't be frozen, for putAttributeValue) 638 * @param data 639 * @param counter 640 * @param removed 641 * @param nonFinalValues 642 * @return 643 */ removeNonDistinguishing( XPathParts parts, DtdData data, int counter, StringBuilder removed, Set<String> nonFinalValues)644 private int removeNonDistinguishing( 645 XPathParts parts, 646 DtdData data, 647 int counter, 648 StringBuilder removed, 649 Set<String> nonFinalValues) { 650 removed.setLength(0); 651 nonFinalValues.clear(); 652 HashSet<String> toRemove = new HashSet<>(); 653 nonFinalValues.clear(); 654 int size = parts.size(); 655 int last = size - 1; 656 for (int i = 0; i < size; ++i) { 657 removed.append("/"); 658 String element = parts.getElement(i); 659 if (data.isOrdered(element)) { 660 parts.putAttributeValue(i, "_q", String.valueOf(counter)); 661 counter++; 662 } 663 for (String attribute : parts.getAttributeKeys(i)) { 664 if (!data.isDistinguishing(element, attribute)) { 665 toRemove.add(attribute); 666 if (i != last && !SKIP_NON_NODE.contains(attribute)) { 667 if (attribute.equals("draft") 668 && (parts.getElement(1).equals("transforms") 669 || parts.getElement(1).equals("collations"))) { 670 // do nothing 671 } else { 672 nonFinalValues.add(attribute); 673 } 674 } 675 } 676 } 677 if (!toRemove.isEmpty()) { 678 for (String attribute : toRemove) { 679 removed.append( 680 "[@" 681 + attribute 682 + "=\"" 683 + parts.getAttributeValue(i, attribute) 684 + "\"]"); 685 parts.removeAttribute(i, attribute); 686 } 687 toRemove.clear(); 688 } 689 } 690 return counter; 691 } 692 } 693