• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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