• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  **********************************************************************
3  * Copyright (c) 2002-2019, International Business Machines
4  * Corporation and others.  All Rights Reserved.
5  **********************************************************************
6  * Author: Mark Davis
7  **********************************************************************
8  */
9 package org.unicode.cldr.util;
10 
11 import com.google.common.base.Joiner;
12 import com.google.common.base.Splitter;
13 import com.google.common.collect.ImmutableMap;
14 import com.google.common.collect.ImmutableMap.Builder;
15 import com.google.common.collect.ImmutableSet;
16 import com.google.common.util.concurrent.UncheckedExecutionException;
17 import com.ibm.icu.impl.Relation;
18 import com.ibm.icu.impl.Row;
19 import com.ibm.icu.impl.Row.R2;
20 import com.ibm.icu.impl.Utility;
21 import com.ibm.icu.text.MessageFormat;
22 import com.ibm.icu.text.PluralRules;
23 import com.ibm.icu.text.SimpleDateFormat;
24 import com.ibm.icu.text.Transform;
25 import com.ibm.icu.text.UnicodeSet;
26 import com.ibm.icu.util.Calendar;
27 import com.ibm.icu.util.Freezable;
28 import com.ibm.icu.util.ICUUncheckedIOException;
29 import com.ibm.icu.util.Output;
30 import com.ibm.icu.util.TimeZone;
31 import com.ibm.icu.util.ULocale;
32 import com.ibm.icu.util.VersionInfo;
33 import java.io.File;
34 import java.io.FileInputStream;
35 import java.io.FilenameFilter;
36 import java.io.InputStream;
37 import java.io.PrintWriter;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collection;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.Date;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.Iterator;
47 import java.util.LinkedHashMap;
48 import java.util.LinkedHashSet;
49 import java.util.LinkedList;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.Set;
54 import java.util.TreeMap;
55 import java.util.TreeSet;
56 import java.util.concurrent.ConcurrentHashMap;
57 import java.util.regex.Matcher;
58 import java.util.regex.Pattern;
59 import java.util.stream.Collectors;
60 import org.unicode.cldr.test.CheckMetazones;
61 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
62 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
63 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope;
64 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget;
65 import org.unicode.cldr.util.LocaleInheritanceInfo.Reason;
66 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
67 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
68 import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
69 import org.unicode.cldr.util.With.SimpleIterator;
70 import org.unicode.cldr.util.XMLFileReader.AllHandler;
71 import org.unicode.cldr.util.XMLSource.ResolvingSource;
72 import org.unicode.cldr.util.XPathParts.Comments;
73 import org.xml.sax.Attributes;
74 import org.xml.sax.Locator;
75 import org.xml.sax.SAXException;
76 import org.xml.sax.SAXParseException;
77 import org.xml.sax.XMLReader;
78 import org.xml.sax.helpers.XMLReaderFactory;
79 
80 /**
81  * This is a class that represents the contents of a CLDR file, as <key,value> pairs, where the key
82  * is a "cleaned" xpath (with non-distinguishing attributes removed), and the value is an object
83  * that contains the full xpath plus a value, which is a string, or a node (the latter for atomic
84  * elements).
85  *
86  * <p><b>WARNING: The API on this class is likely to change.</b> Having the full xpath on the value
87  * is clumsy; I need to change it to having the key be an object that contains the full xpath, but
88  * then sorts as if it were clean.
89  *
90  * <p>Each instance also contains a set of associated comments for each xpath.
91  *
92  * @author medavis
93  */
94 
95 /*
96  * Notes:
97  * http://xml.apache.org/xerces2-j/faq-grammars.html#faq-3
98  * http://developers.sun.com/dev/coolstuff/xml/readme.html
99  * http://lists.xml.org/archives/xml-dev/200007/msg00284.html
100  * http://java.sun.com/j2se/1.4.2/docs/api/org/xml/sax/DTDHandler.html
101  */
102 
103 public class CLDRFile implements Freezable<CLDRFile>, Iterable<String>, LocaleStringProvider {
104 
105     private static final String GETNAME_LOCALE_SEPARATOR =
106             "//ldml/localeDisplayNames/localeDisplayPattern/localeSeparator";
107     private static final String GETNAME_LOCALE_PATTERN =
108             "//ldml/localeDisplayNames/localeDisplayPattern/localePattern";
109     private static final String GETNAME_LOCALE_KEY_TYPE_PATTERN =
110             "//ldml/localeDisplayNames/localeDisplayPattern/localeKeyTypePattern";
111 
112     private static final ImmutableSet<String> casesNominativeOnly =
113             ImmutableSet.of(GrammaticalFeature.grammaticalCase.getDefault(null));
114     /**
115      * Variable to control whether File reads are buffered; this will about halve the time spent in
116      * loadFromFile() and Factory.make() from about 20 % to about 10 %. It will also noticeably
117      * improve the different unit tests take in the TestAll fixture. TRUE - use buffering (default)
118      * FALSE - do not use buffering
119      */
120     private static final boolean USE_LOADING_BUFFER = true;
121 
122     private static final boolean DEBUG = false;
123 
124     public static final Pattern ALT_PROPOSED_PATTERN =
125             PatternCache.get(".*\\[@alt=\"[^\"]*proposed[^\"]*\"].*");
126     public static final Pattern DRAFT_PATTERN = PatternCache.get("\\[@draft=\"([^\"]*)\"\\]");
127     public static final Pattern XML_SPACE_PATTERN =
128             PatternCache.get("\\[@xml:space=\"([^\"]*)\"\\]");
129 
130     private static boolean LOG_PROGRESS = false;
131 
132     public static boolean HACK_ORDER = false;
133     private static boolean DEBUG_LOGGING = false;
134 
135     public static final String SUPPLEMENTAL_NAME = "supplementalData";
136     public static final String SUPPLEMENTAL_METADATA = "supplementalMetadata";
137     public static final String SUPPLEMENTAL_PREFIX = "supplemental";
138     public static final String GEN_VERSION = "46";
139     public static final List<String> SUPPLEMENTAL_NAMES =
140             Arrays.asList(
141                     "characters",
142                     "coverageLevels",
143                     "dayPeriods",
144                     "genderList",
145                     "grammaticalFeatures",
146                     "languageInfo",
147                     "languageGroup",
148                     "likelySubtags",
149                     "metaZones",
150                     "numberingSystems",
151                     "ordinals",
152                     "pluralRanges",
153                     "plurals",
154                     "postalCodeData",
155                     "rgScope",
156                     "supplementalData",
157                     "supplementalMetadata",
158                     "telephoneCodeData",
159                     "units",
160                     "windowsZones");
161 
162     private Set<String> extraPaths = null;
163 
164     private boolean locked;
165     private DtdType dtdType;
166     private DtdData dtdData;
167 
168     XMLSource dataSource; // TODO(jchye): make private
169 
170     private File supplementalDirectory;
171 
172     /**
173      * Does the value in question either match or inherent the current value?
174      *
175      * <p>To match, the value in question and the current value must be non-null and equal.
176      *
177      * <p>To inherit the current value, the value in question must be INHERITANCE_MARKER and the
178      * current value must equal the bailey value.
179      *
180      * <p>This CLDRFile is only used here for getBaileyValue, not to get curValue
181      *
182      * @param value the value in question
183      * @param curValue the current value, that is, XMLSource.getValueAtDPath(xpathString)
184      * @param xpathString the path identifier
185      * @return true if it matches or inherits, else false
186      */
equalsOrInheritsCurrentValue(String value, String curValue, String xpathString)187     public boolean equalsOrInheritsCurrentValue(String value, String curValue, String xpathString) {
188         if (value == null || curValue == null) {
189             return false;
190         }
191         if (value.equals(curValue)) {
192             return true;
193         }
194         if (value.equals(CldrUtility.INHERITANCE_MARKER)) {
195             String baileyValue = getBaileyValue(xpathString, null, null);
196             if (baileyValue == null) {
197                 /* This may happen for Invalid XPath; InvalidXPathException may be thrown. */
198                 return false;
199             }
200             if (curValue.equals(baileyValue)) {
201                 return true;
202             }
203         }
204         return false;
205     }
206 
getResolvingDataSource()207     public XMLSource getResolvingDataSource() {
208         if (!isResolved()) {
209             throw new IllegalArgumentException(
210                     "CLDRFile must be resolved for getResolvingDataSource");
211         }
212         // dataSource instanceof XMLSource.ResolvingSource
213         return dataSource;
214     }
215 
216     public enum DraftStatus {
217         unconfirmed,
218         provisional,
219         contributed,
220         approved;
221 
forString(String string)222         public static DraftStatus forString(String string) {
223             return string == null
224                     ? DraftStatus.approved
225                     : DraftStatus.valueOf(string.toLowerCase(Locale.ENGLISH));
226         }
227 
228         /**
229          * Get the draft status from a full xpath
230          *
231          * @param xpath
232          * @return
233          */
forXpath(String xpath)234         public static DraftStatus forXpath(String xpath) {
235             final String status =
236                     XPathParts.getFrozenInstance(xpath).getAttributeValue(-1, "draft");
237             return forString(status);
238         }
239 
240         /** Return the XPath suffix for this draft status or "" for approved. */
asXpath()241         public String asXpath() {
242             if (this == approved) {
243                 return "";
244             } else {
245                 return "[@draft=\"" + name() + "\"]";
246             }
247         }
248 
249         /** update this XPath with this draft status */
updateXPath(final String fullXpath)250         public String updateXPath(final String fullXpath) {
251             final XPathParts xpp = XPathParts.getFrozenInstance(fullXpath).cloneAsThawed();
252             final String oldDraft = xpp.getAttributeValue(-1, "draft");
253             if (forString(oldDraft) == this) {
254                 return fullXpath; // no change;
255             }
256             if (this == approved) {
257                 xpp.removeAttribute(-1, "draft");
258             } else {
259                 xpp.setAttribute(-1, "draft", this.name());
260             }
261             return xpp.toString();
262         }
263     }
264 
265     @Override
toString()266     public String toString() {
267         return "{"
268                 + "locked="
269                 + locked
270                 + " locale="
271                 + dataSource.getLocaleID()
272                 + " dataSource="
273                 + dataSource.toString()
274                 + "}";
275     }
276 
toString(String regex)277     public String toString(String regex) {
278         return "{"
279                 + "locked="
280                 + locked
281                 + " locale="
282                 + dataSource.getLocaleID()
283                 + " regex="
284                 + regex
285                 + " dataSource="
286                 + dataSource.toString(regex)
287                 + "}";
288     }
289 
290     // for refactoring
291 
setNonInheriting(boolean isSupplemental)292     public CLDRFile setNonInheriting(boolean isSupplemental) {
293         if (locked) {
294             throw new UnsupportedOperationException("Attempt to modify locked object");
295         }
296         dataSource.setNonInheriting(isSupplemental);
297         return this;
298     }
299 
isNonInheriting()300     public boolean isNonInheriting() {
301         return dataSource.isNonInheriting();
302     }
303 
304     private static final boolean DEBUG_CLDR_FILE = false;
305     private String creationTime = null; // only used if DEBUG_CLDR_FILE
306 
307     /**
308      * Construct a new CLDRFile.
309      *
310      * @param dataSource must not be null
311      */
CLDRFile(XMLSource dataSource)312     public CLDRFile(XMLSource dataSource) {
313         this.dataSource = dataSource;
314 
315         if (DEBUG_CLDR_FILE) {
316             creationTime =
317                     new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
318                             .format(Calendar.getInstance().getTime());
319             System.out.println("�� Created new CLDRFile(dataSource) at " + creationTime);
320         }
321     }
322 
323     /**
324      * get Unresolved CLDRFile
325      *
326      * @param localeId
327      * @param dirs
328      * @param minimalDraftStatus
329      */
CLDRFile(String localeId, List<File> dirs, DraftStatus minimalDraftStatus)330     public CLDRFile(String localeId, List<File> dirs, DraftStatus minimalDraftStatus) {
331         // order matters
332         this.dataSource = XMLSource.getFrozenInstance(localeId, dirs, minimalDraftStatus);
333         this.dtdType = dataSource.getXMLNormalizingDtdType();
334         this.dtdData = DtdData.getInstance(this.dtdType);
335     }
336 
CLDRFile(XMLSource dataSource, XMLSource... resolvingParents)337     public CLDRFile(XMLSource dataSource, XMLSource... resolvingParents) {
338         List<XMLSource> sourceList = new ArrayList<>();
339         sourceList.add(dataSource);
340         sourceList.addAll(Arrays.asList(resolvingParents));
341         this.dataSource = new ResolvingSource(sourceList);
342 
343         if (DEBUG_CLDR_FILE) {
344             creationTime =
345                     new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
346                             .format(Calendar.getInstance().getTime());
347             System.out.println(
348                     "�� Created new CLDRFile(dataSource, XMLSource... resolvingParents) at "
349                             + creationTime);
350         }
351     }
352 
loadFromFile( File f, String localeName, DraftStatus minimalDraftStatus, XMLSource source)353     public static CLDRFile loadFromFile(
354             File f, String localeName, DraftStatus minimalDraftStatus, XMLSource source) {
355         String fullFileName = f.getAbsolutePath();
356         try {
357             fullFileName = PathUtilities.getNormalizedPathString(f);
358             if (DEBUG_LOGGING) {
359                 System.out.println("Parsing: " + fullFileName);
360                 Log.logln(LOG_PROGRESS, "Parsing: " + fullFileName);
361             }
362             final CLDRFile cldrFile;
363             if (USE_LOADING_BUFFER) {
364                 // Use Buffering -  improves performance at little cost to memory footprint
365                 // try (InputStream fis = new BufferedInputStream(new FileInputStream(f),32000);) {
366                 try (InputStream fis = InputStreamFactory.createInputStream(f)) {
367                     cldrFile = load(fullFileName, localeName, fis, minimalDraftStatus, source);
368                     return cldrFile;
369                 }
370             } else {
371                 // previous version - do not use buffering
372                 try (InputStream fis = new FileInputStream(f); ) {
373                     cldrFile = load(fullFileName, localeName, fis, minimalDraftStatus, source);
374                     return cldrFile;
375                 }
376             }
377 
378         } catch (Exception e) {
379             // use a StringBuilder to construct the message.
380             StringBuilder sb = new StringBuilder("Cannot read the file '");
381             sb.append(fullFileName);
382             sb.append("': ");
383             sb.append(e.getMessage());
384             throw new ICUUncheckedIOException(sb.toString(), e);
385         }
386     }
387 
loadFromFiles( List<File> dirs, String localeName, DraftStatus minimalDraftStatus, XMLSource source)388     public static CLDRFile loadFromFiles(
389             List<File> dirs, String localeName, DraftStatus minimalDraftStatus, XMLSource source) {
390         try {
391             if (DEBUG_LOGGING) {
392                 System.out.println("Parsing: " + dirs);
393                 Log.logln(LOG_PROGRESS, "Parsing: " + dirs);
394             }
395             if (USE_LOADING_BUFFER) {
396                 // Use Buffering -  improves performance at little cost to memory footprint
397                 // try (InputStream fis = new BufferedInputStream(new FileInputStream(f),32000);) {
398                 CLDRFile cldrFile = new CLDRFile(source);
399                 for (File dir : dirs) {
400                     File f = new File(dir, localeName + ".xml");
401                     try (InputStream fis = InputStreamFactory.createInputStream(f)) {
402                         cldrFile.loadFromInputStream(
403                                 PathUtilities.getNormalizedPathString(f),
404                                 localeName,
405                                 fis,
406                                 minimalDraftStatus,
407                                 false);
408                     }
409                 }
410                 return cldrFile;
411             } else {
412                 throw new IllegalArgumentException("Must use USE_LOADING_BUFFER");
413             }
414 
415         } catch (Exception e) {
416             // e.printStackTrace();
417             // use a StringBuilder to construct the message.
418             StringBuilder sb = new StringBuilder("Cannot read the file '");
419             sb.append(dirs);
420             throw new ICUUncheckedIOException(sb.toString(), e);
421         }
422     }
423 
424     /**
425      * Produce a CLDRFile from a localeName, given a directory. (Normally a Factory is used to
426      * create CLDRFiles.)
427      *
428      * @param f
429      * @param localeName
430      * @param minimalDraftStatus
431      */
loadFromFile(File f, String localeName, DraftStatus minimalDraftStatus)432     public static CLDRFile loadFromFile(File f, String localeName, DraftStatus minimalDraftStatus) {
433         return loadFromFile(f, localeName, minimalDraftStatus, new SimpleXMLSource(localeName));
434     }
435 
loadFromFiles( List<File> dirs, String localeName, DraftStatus minimalDraftStatus)436     public static CLDRFile loadFromFiles(
437             List<File> dirs, String localeName, DraftStatus minimalDraftStatus) {
438         return loadFromFiles(dirs, localeName, minimalDraftStatus, new SimpleXMLSource(localeName));
439     }
440 
load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus)441     static CLDRFile load(
442             String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus) {
443         return load(fileName, localeName, fis, minimalDraftStatus, new SimpleXMLSource(localeName));
444     }
445 
446     /**
447      * Load a CLDRFile from a file input stream.
448      *
449      * @param localeName
450      * @param fis
451      */
load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, XMLSource source)452     private static CLDRFile load(
453             String fileName,
454             String localeName,
455             InputStream fis,
456             DraftStatus minimalDraftStatus,
457             XMLSource source) {
458         CLDRFile cldrFile = new CLDRFile(source);
459         return cldrFile.loadFromInputStream(fileName, localeName, fis, minimalDraftStatus, false);
460     }
461 
462     /**
463      * Load a CLDRFile from a file input stream.
464      *
465      * @param localeName
466      * @param fis
467      */
load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, XMLSource source, boolean leniency)468     private static CLDRFile load(
469             String fileName,
470             String localeName,
471             InputStream fis,
472             DraftStatus minimalDraftStatus,
473             XMLSource source,
474             boolean leniency) {
475         CLDRFile cldrFile = new CLDRFile(source);
476         return cldrFile.loadFromInputStream(
477                 fileName, localeName, fis, minimalDraftStatus, leniency);
478     }
479 
load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, boolean leniency)480     static CLDRFile load(
481             String fileName,
482             String localeName,
483             InputStream fis,
484             DraftStatus minimalDraftStatus,
485             boolean leniency) {
486         return load(
487                 fileName,
488                 localeName,
489                 fis,
490                 minimalDraftStatus,
491                 new SimpleXMLSource(localeName),
492                 leniency);
493     }
494 
495     /**
496      * Low-level function, only normally used for testing.
497      *
498      * @param fileName
499      * @param localeName
500      * @param fis
501      * @param minimalDraftStatus
502      * @param leniency if true, skip dtd validation
503      * @return
504      */
loadFromInputStream( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, boolean leniency)505     public CLDRFile loadFromInputStream(
506             String fileName,
507             String localeName,
508             InputStream fis,
509             DraftStatus minimalDraftStatus,
510             boolean leniency) {
511         CLDRFile cldrFile = this;
512         MyDeclHandler DEFAULT_DECLHANDLER = new MyDeclHandler(cldrFile, minimalDraftStatus);
513         XMLFileReader.read(fileName, fis, -1, !leniency, DEFAULT_DECLHANDLER);
514         if (DEFAULT_DECLHANDLER.isSupplemental < 0) {
515             throw new IllegalArgumentException(
516                     "root of file must be either ldml or supplementalData");
517         }
518         cldrFile.setNonInheriting(DEFAULT_DECLHANDLER.isSupplemental > 0);
519         if (DEFAULT_DECLHANDLER.overrideCount > 0) {
520             throw new IllegalArgumentException(
521                     "Internal problems: either data file has duplicate path, or"
522                             + " CLDRFile.isDistinguishing() or CLDRFile.isOrdered() need updating: "
523                             + DEFAULT_DECLHANDLER.overrideCount
524                             + "; The exact problems are printed on the console above.");
525         }
526         if (localeName == null) {
527             cldrFile.dataSource.setLocaleID(cldrFile.getLocaleIDFromIdentity());
528         }
529         return cldrFile;
530     }
531 
532     /**
533      * Clone the object. Produces unlocked version
534      *
535      * @see com.ibm.icu.util.Freezable
536      */
537     @Override
cloneAsThawed()538     public CLDRFile cloneAsThawed() {
539         try {
540             CLDRFile result = (CLDRFile) super.clone();
541             result.locked = false;
542             result.dataSource = result.dataSource.cloneAsThawed();
543             return result;
544         } catch (CloneNotSupportedException e) {
545             throw new InternalError("should never happen");
546         }
547     }
548 
549     /** Prints the contents of the file (the xpaths/values) to the console. */
show()550     public CLDRFile show() {
551         for (Iterator<String> it2 = iterator(); it2.hasNext(); ) {
552             String xpath = it2.next();
553             System.out.println(getFullXPath(xpath) + " =>\t" + getStringValue(xpath));
554         }
555         return this;
556     }
557 
558     private static final Map<String, Object> nullOptions =
559             Collections.unmodifiableMap(new TreeMap<String, Object>());
560 
561     /**
562      * Write the corresponding XML file out, with the normal formatting and indentation. Will update
563      * the identity element, including version, and other items. If the CLDRFile is empty, the DTD
564      * type will be //ldml.
565      */
write(PrintWriter pw)566     public void write(PrintWriter pw) {
567         write(pw, nullOptions);
568     }
569 
570     /**
571      * Write the corresponding XML file out, with the normal formatting and indentation. Will update
572      * the identity element, including version, and other items. If the CLDRFile is empty, the DTD
573      * type will be //ldml.
574      *
575      * @param pw writer to print to
576      * @param options map of options for writing
577      * @return true if we write the file, false if we cancel due to skipping all paths
578      */
write(PrintWriter pw, Map<String, ?> options)579     public boolean write(PrintWriter pw, Map<String, ?> options) {
580         final CldrXmlWriter xmlWriter = new CldrXmlWriter(this, pw, options);
581         xmlWriter.write();
582         return true;
583     }
584 
585     /** Get a string value from an xpath. */
586     @Override
getStringValue(String xpath)587     public String getStringValue(String xpath) {
588         try {
589             String result = dataSource.getValueAtPath(xpath);
590             if (result == null && dataSource.isResolving()) {
591                 final String fallbackPath = getFallbackPath(xpath, false, true);
592                 // often fallbackPath equals xpath -- in such cases, isn't it a waste of time to
593                 // call getValueAtPath again?
594                 if (fallbackPath != null) {
595                     result = dataSource.getValueAtPath(fallbackPath);
596                 }
597             }
598             if (isResolved()
599                     && GlossonymConstructor.valueIsBogus(result)
600                     && GlossonymConstructor.pathIsEligible(xpath)) {
601                 final String constructedValue = new GlossonymConstructor(this).getValue(xpath);
602                 if (constructedValue != null) {
603                     result = constructedValue;
604                 }
605             }
606             return result;
607         } catch (Exception e) {
608             throw new UncheckedExecutionException("Bad path: " + xpath, e);
609         }
610     }
611 
612     /**
613      * Get GeorgeBailey value: that is, what the value would be if it were not directly contained in
614      * the file at that path. If the value is null or INHERITANCE_MARKER (with resolving), then
615      * baileyValue = resolved value. A non-resolving CLDRFile will always return null.
616      */
getBaileyValue( String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound)617     public String getBaileyValue(
618             String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) {
619         String result = dataSource.getBaileyValue(xpath, pathWhereFound, localeWhereFound);
620         if ((result == null || result.equals(CldrUtility.INHERITANCE_MARKER))
621                 && dataSource.isResolving()) {
622             final String fallbackPath =
623                     getFallbackPath(
624                             xpath, false,
625                             false); // return null if there is no different sideways path
626             if (xpath.equals(fallbackPath)) {
627                 getFallbackPath(xpath, false, true);
628                 throw new IllegalArgumentException(); // should never happen
629             }
630             if (fallbackPath != null) {
631                 result = dataSource.getValueAtPath(fallbackPath);
632                 if (result != null) {
633                     Status status = new Status();
634                     if (localeWhereFound != null) {
635                         localeWhereFound.value = dataSource.getSourceLocaleID(fallbackPath, status);
636                     }
637                     if (pathWhereFound != null) {
638                         pathWhereFound.value = status.pathWhereFound;
639                     }
640                 }
641             }
642         }
643         if (isResolved()
644                 && GlossonymConstructor.valueIsBogus(result)
645                 && GlossonymConstructor.pathIsEligible(xpath)) {
646             final GlossonymConstructor gc = new GlossonymConstructor(this);
647             final String constructedValue =
648                     gc.getValueAndTrack(xpath, pathWhereFound, localeWhereFound);
649             if (constructedValue != null) {
650                 result = constructedValue;
651             }
652         }
653         return result;
654     }
655 
656     /**
657      * Return a list of all paths which contributed to the value, as well as all bailey values. This
658      * is used to explain inheritance and bailey values. The list must be interpreted in order. When
659      * {@link LocaleInheritanceInfo.Reason#isTerminal()} return true, that indicates a successful
660      * lookup and partitions values from subsequent bailey values.
661      *
662      * @see #getBaileyValue(String, Output, Output)
663      * @see #getSourceLocaleIdExtended(String, Status, boolean)
664      */
getPathsWhereFound(String xpath)665     public List<LocaleInheritanceInfo> getPathsWhereFound(String xpath) {
666         if (!isResolved()) {
667             throw new IllegalArgumentException(
668                     "getPathsWhereFound() is only valid on a resolved CLDRFile");
669         }
670         LinkedList<LocaleInheritanceInfo> list = new LinkedList<>();
671         // first, call getSourceLocaleIdExtended to populate the list
672         Status status = new Status();
673         getSourceLocaleIdExtended(xpath, status, false, list);
674         final String path1 = status.pathWhereFound;
675         // For now, the only special case is Glossonym
676         if (path1.equals(GlossonymConstructor.PSEUDO_PATH)) {
677             // it's a Glossonym, so as the GlossonymConstructor what the paths are.  Sort paths in
678             // reverse order.
679             final Set<String> xpaths =
680                     new GlossonymConstructor(this)
681                             .getPathsWhereFound(
682                                     xpath, new TreeSet<String>(Comparator.reverseOrder()));
683             for (final String subpath : xpaths) {
684                 final String locale2 = getSourceLocaleIdExtended(subpath, status, true);
685                 final String path2 = status.pathWhereFound;
686                 // Paths are in reverse order (c-b-a) so we insert them at the top of our list.
687                 list.addFirst(new LocaleInheritanceInfo(locale2, path2, Reason.constructed));
688             }
689 
690             // now the list contains:
691             // constructed: a
692             // constructed: b
693             // constructed: c
694             // (none) - this is where the glossonym was
695             // (bailey value(s))
696         }
697         return list;
698     }
699 
700     static final class SimpleAltPicker implements Transform<String, String> {
701         public final String alt;
702 
SimpleAltPicker(String alt)703         public SimpleAltPicker(String alt) {
704             this.alt = alt;
705         }
706 
707         @Override
transform(@uppressWarnings"unused") String source)708         public String transform(@SuppressWarnings("unused") String source) {
709             return alt;
710         }
711     }
712 
713     /**
714      * Only call if xpath doesn't exist in the current file.
715      *
716      * <p>For now, just handle counts and cases: see getCountPath Also handle extraPaths
717      *
718      * @param xpath
719      * @param winning TODO
720      * @param checkExtraPaths TODO
721      * @return
722      */
getFallbackPath(String xpath, boolean winning, boolean checkExtraPaths)723     private String getFallbackPath(String xpath, boolean winning, boolean checkExtraPaths) {
724         if (GrammaticalFeature.pathHasFeature(xpath) != null) {
725             return getCountPathWithFallback(xpath, Count.other, winning);
726         }
727         if (checkExtraPaths && getRawExtraPaths().contains(xpath)) {
728             return xpath;
729         }
730         return null;
731     }
732 
733     /**
734      * Get the full path from a distinguished path.
735      *
736      * @param xpath the distinguished path
737      * @return the full path
738      *     <p>Examples:
739      *     <p>xpath = //ldml/localeDisplayNames/scripts/script[@type="Adlm"] result =
740      *     //ldml/localeDisplayNames/scripts/script[@type="Adlm"][@draft="unconfirmed"]
741      *     <p>xpath =
742      *     //ldml/dates/calendars/calendar[@type="hebrew"]/dateFormats/dateFormatLength[@type="full"]/dateFormat[@type="standard"]/pattern[@type="standard"]
743      *     result =
744      *     //ldml/dates/calendars/calendar[@type="hebrew"]/dateFormats/dateFormatLength[@type="full"]/dateFormat[@type="standard"]/pattern[@type="standard"][@numbers="hebr"]
745      */
getFullXPath(String xpath)746     public String getFullXPath(String xpath) {
747         if (xpath == null) {
748             throw new NullPointerException("Null distinguishing xpath");
749         }
750         String result = dataSource.getFullPath(xpath);
751         return result != null
752                 ? result
753                 : xpath; // we can't add any non-distinguishing values if there is nothing there.
754         //        if (result == null && dataSource.isResolving()) {
755         //            String fallback = getFallbackPath(xpath, true);
756         //            if (fallback != null) {
757         //                // TODO, add attributes from fallback into main
758         //                result = xpath;
759         //            }
760         //        }
761         //        return result;
762     }
763 
764     /**
765      * Get the last modified date (if available) from a distinguished path.
766      *
767      * @return date or null if not available.
768      */
getLastModifiedDate(String xpath)769     public Date getLastModifiedDate(String xpath) {
770         return dataSource.getChangeDateAtDPath(xpath);
771     }
772 
773     /**
774      * Find out where the value was found (for resolving locales). Returns {@link
775      * XMLSource#CODE_FALLBACK_ID} as the location if nothing is found
776      *
777      * @param distinguishedXPath path (must be distinguished!)
778      * @param status the distinguished path where the item was found. Pass in null if you don't
779      *     care.
780      */
781     @Override
getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status)782     public String getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status) {
783         return getSourceLocaleIdExtended(
784                 distinguishedXPath, status, true /* skipInheritanceMarker */);
785     }
786 
787     /**
788      * Find out where the value was found (for resolving locales). Returns {@link
789      * XMLSource#CODE_FALLBACK_ID} as the location if nothing is found
790      *
791      * @param distinguishedXPath path (must be distinguished!)
792      * @param status the distinguished path where the item was found. Pass in null if you don't
793      *     care.
794      * @param skipInheritanceMarker if true, skip sources in which value is INHERITANCE_MARKER
795      * @return the locale id as a string
796      */
getSourceLocaleIdExtended( String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker)797     public String getSourceLocaleIdExtended(
798             String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker) {
799         return getSourceLocaleIdExtended(distinguishedXPath, status, skipInheritanceMarker, null);
800     }
801 
getSourceLocaleIdExtended( String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker, List<LocaleInheritanceInfo> list)802     public String getSourceLocaleIdExtended(
803             String distinguishedXPath,
804             CLDRFile.Status status,
805             boolean skipInheritanceMarker,
806             List<LocaleInheritanceInfo> list) {
807         String result =
808                 dataSource.getSourceLocaleIdExtended(
809                         distinguishedXPath, status, skipInheritanceMarker, list);
810         if (result == XMLSource.CODE_FALLBACK_ID && dataSource.isResolving()) {
811             final String fallbackPath = getFallbackPath(distinguishedXPath, false, true);
812             if (fallbackPath != null && !fallbackPath.equals(distinguishedXPath)) {
813                 if (list != null) {
814                     list.add(
815                             new LocaleInheritanceInfo(
816                                     getLocaleID(), distinguishedXPath, Reason.fallback, null));
817                 }
818                 result =
819                         dataSource.getSourceLocaleIdExtended(
820                                 fallbackPath, status, skipInheritanceMarker, list);
821             }
822             if (result == XMLSource.CODE_FALLBACK_ID
823                     && getConstructedValue(distinguishedXPath) != null) {
824                 if (status != null) {
825                     status.pathWhereFound = GlossonymConstructor.PSEUDO_PATH;
826                 }
827                 return getLocaleID();
828             }
829         }
830         return result;
831     }
832 
833     /**
834      * return true if the path in this file (without resolution)
835      *
836      * @param path
837      * @return
838      */
isHere(String path)839     public boolean isHere(String path) {
840         return dataSource.isHere(path);
841     }
842 
843     /**
844      * Add a new element to a CLDRFile.
845      *
846      * @param currentFullXPath
847      * @param value
848      */
add(String currentFullXPath, String value)849     public CLDRFile add(String currentFullXPath, String value) {
850         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
851         // StringValue v = new StringValue(value, currentFullXPath);
852         Log.logln(
853                 LOG_PROGRESS,
854                 "ADDING: \t" + currentFullXPath + " \t" + value + "\t" + currentFullXPath);
855         // xpath = xpath.intern();
856         try {
857             dataSource.putValueAtPath(currentFullXPath, value);
858         } catch (RuntimeException e) {
859             throw (IllegalArgumentException)
860                     new IllegalArgumentException(
861                                     "failed adding " + currentFullXPath + ",\t" + value)
862                             .initCause(e);
863         }
864         return this;
865     }
866 
867     /** Note where this element was parsed. */
addSourceLocation(String currentFullXPath, XMLSource.SourceLocation location)868     public CLDRFile addSourceLocation(String currentFullXPath, XMLSource.SourceLocation location) {
869         dataSource.addSourceLocation(currentFullXPath, location);
870         return this;
871     }
872 
873     /**
874      * Get the line and column for a path
875      *
876      * @param path xpath or fullpath
877      */
getSourceLocation(String path)878     public XMLSource.SourceLocation getSourceLocation(String path) {
879         final String fullPath = getFullXPath(path);
880         return dataSource.getSourceLocation(fullPath);
881     }
882 
addComment(String xpath, String comment, Comments.CommentType type)883     public CLDRFile addComment(String xpath, String comment, Comments.CommentType type) {
884         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
885         // System.out.println("Adding comment: <" + xpath + "> '" + comment + "'");
886         Log.logln(LOG_PROGRESS, "ADDING Comment: \t" + type + "\t" + xpath + " \t" + comment);
887         if (xpath == null || xpath.length() == 0) {
888             dataSource
889                     .getXpathComments()
890                     .setFinalComment(
891                             CldrUtility.joinWithSeparation(
892                                     dataSource.getXpathComments().getFinalComment(),
893                                     XPathParts.NEWLINE,
894                                     comment));
895         } else {
896             xpath = getDistinguishingXPath(xpath, null);
897             dataSource.getXpathComments().addComment(type, xpath, comment);
898         }
899         return this;
900     }
901 
902     // TODO Change into enum, update docs
903     public static final int MERGE_KEEP_MINE = 0,
904             MERGE_REPLACE_MINE = 1,
905             MERGE_ADD_ALTERNATE = 2,
906             MERGE_REPLACE_MY_DRAFT = 3;
907 
908     /**
909      * Merges elements from another CLDR file. Note: when both have the same xpath key, the keepMine
910      * determines whether "my" values are kept or the other files values are kept.
911      *
912      * @param other
913      * @param conflict_resolution
914      */
putAll(CLDRFile other, int conflict_resolution)915     public CLDRFile putAll(CLDRFile other, int conflict_resolution) {
916 
917         if (locked) {
918             throw new UnsupportedOperationException("Attempt to modify locked object");
919         }
920         if (conflict_resolution == MERGE_KEEP_MINE) {
921             dataSource.putAll(other.dataSource, MERGE_KEEP_MINE);
922         } else if (conflict_resolution == MERGE_REPLACE_MINE) {
923             dataSource.putAll(other.dataSource, MERGE_REPLACE_MINE);
924         } else if (conflict_resolution == MERGE_REPLACE_MY_DRAFT) {
925             // first find all my alt=..proposed items
926             Set<String> hasDraftVersion = new HashSet<>();
927             for (Iterator<String> it = dataSource.iterator(); it.hasNext(); ) {
928                 String cpath = it.next();
929                 String fullpath = getFullXPath(cpath);
930                 if (fullpath.indexOf("[@draft") >= 0) {
931                     hasDraftVersion.add(
932                             getNondraftNonaltXPath(cpath)); // strips the alt and the draft
933                 }
934             }
935             // only replace draft items!
936             // this is either an item with draft in the fullpath
937             // or an item with draft and alt in the full path
938             for (Iterator<String> it = other.iterator(); it.hasNext(); ) {
939                 String cpath = it.next();
940                 cpath = getNondraftNonaltXPath(cpath);
941                 String newValue = other.getStringValue(cpath);
942                 String newFullPath = getNondraftNonaltXPath(other.getFullXPath(cpath));
943                 // another hack; need to add references back in
944                 newFullPath = addReferencesIfNeeded(newFullPath, getFullXPath(cpath));
945 
946                 if (!hasDraftVersion.contains(cpath)) {
947                     if (cpath.startsWith("//ldml/identity/"))
948                         continue; // skip, since the error msg is not needed.
949                     String myVersion = getStringValue(cpath);
950                     if (myVersion == null || !newValue.equals(myVersion)) {
951                         Log.logln(
952                                 getLocaleID()
953                                         + "\tDenied attempt to replace non-draft"
954                                         + CldrUtility.LINE_SEPARATOR
955                                         + "\tcurr: ["
956                                         + cpath
957                                         + ",\t"
958                                         + myVersion
959                                         + "]"
960                                         + CldrUtility.LINE_SEPARATOR
961                                         + "\twith: ["
962                                         + newValue
963                                         + "]");
964                         continue;
965                     }
966                 }
967                 Log.logln(getLocaleID() + "\tVETTED: [" + newFullPath + ",\t" + newValue + "]");
968                 dataSource.putValueAtPath(newFullPath, newValue);
969             }
970         } else if (conflict_resolution == MERGE_ADD_ALTERNATE) {
971             for (Iterator<String> it = other.iterator(); it.hasNext(); ) {
972                 String key = it.next();
973                 String otherValue = other.getStringValue(key);
974                 String myValue = dataSource.getValueAtPath(key);
975                 if (myValue == null) {
976                     dataSource.putValueAtPath(other.getFullXPath(key), otherValue);
977                 } else if (!(myValue.equals(otherValue)
978                                 && equalsIgnoringDraft(getFullXPath(key), other.getFullXPath(key)))
979                         && !key.startsWith("//ldml/identity")) {
980                     for (int i = 0; ; ++i) {
981                         String prop = "proposed" + (i == 0 ? "" : String.valueOf(i));
982                         XPathParts parts =
983                                 XPathParts.getFrozenInstance(other.getFullXPath(key))
984                                         .cloneAsThawed(); // not frozen, for addAttribut
985                         String fullPath = parts.addAttribute("alt", prop).toString();
986                         String path = getDistinguishingXPath(fullPath, null);
987                         if (dataSource.getValueAtPath(path) != null) {
988                             continue;
989                         }
990                         dataSource.putValueAtPath(fullPath, otherValue);
991                         break;
992                     }
993                 }
994             }
995         } else {
996             throw new IllegalArgumentException("Illegal operand: " + conflict_resolution);
997         }
998 
999         dataSource
1000                 .getXpathComments()
1001                 .setInitialComment(
1002                         CldrUtility.joinWithSeparation(
1003                                 dataSource.getXpathComments().getInitialComment(),
1004                                 XPathParts.NEWLINE,
1005                                 other.dataSource.getXpathComments().getInitialComment()));
1006         dataSource
1007                 .getXpathComments()
1008                 .setFinalComment(
1009                         CldrUtility.joinWithSeparation(
1010                                 dataSource.getXpathComments().getFinalComment(),
1011                                 XPathParts.NEWLINE,
1012                                 other.dataSource.getXpathComments().getFinalComment()));
1013         dataSource.getXpathComments().joinAll(other.dataSource.getXpathComments());
1014         return this;
1015     }
1016 
1017     /** */
addReferencesIfNeeded(String newFullPath, String fullXPath)1018     private String addReferencesIfNeeded(String newFullPath, String fullXPath) {
1019         if (fullXPath == null || fullXPath.indexOf("[@references=") < 0) {
1020             return newFullPath;
1021         }
1022         XPathParts parts = XPathParts.getFrozenInstance(fullXPath);
1023         String accummulatedReferences = null;
1024         for (int i = 0; i < parts.size(); ++i) {
1025             Map<String, String> attributes = parts.getAttributes(i);
1026             String references = attributes.get("references");
1027             if (references == null) {
1028                 continue;
1029             }
1030             if (accummulatedReferences == null) {
1031                 accummulatedReferences = references;
1032             } else {
1033                 accummulatedReferences += ", " + references;
1034             }
1035         }
1036         if (accummulatedReferences == null) {
1037             return newFullPath;
1038         }
1039         XPathParts newParts = XPathParts.getFrozenInstance(newFullPath);
1040         Map<String, String> attributes = newParts.getAttributes(newParts.size() - 1);
1041         String references = attributes.get("references");
1042         if (references == null) references = accummulatedReferences;
1043         else references += ", " + accummulatedReferences;
1044         attributes.put("references", references);
1045         System.out.println(
1046                 "Changing " + newFullPath + " plus " + fullXPath + " to " + newParts.toString());
1047         return newParts.toString();
1048     }
1049 
1050     /** Removes an element from a CLDRFile. */
remove(String xpath)1051     public CLDRFile remove(String xpath) {
1052         remove(xpath, false);
1053         return this;
1054     }
1055 
1056     /** Removes an element from a CLDRFile. */
remove(String xpath, boolean butComment)1057     public CLDRFile remove(String xpath, boolean butComment) {
1058         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
1059         if (butComment) {
1060             appendFinalComment(
1061                     dataSource.getFullPath(xpath) + "::<" + dataSource.getValueAtPath(xpath) + ">");
1062         }
1063         dataSource.removeValueAtPath(xpath);
1064         return this;
1065     }
1066 
1067     /** Removes all xpaths from a CLDRFile. */
removeAll(Set<String> xpaths, boolean butComment)1068     public CLDRFile removeAll(Set<String> xpaths, boolean butComment) {
1069         if (butComment) appendFinalComment("Illegal attributes removed:");
1070         for (Iterator<String> it = xpaths.iterator(); it.hasNext(); ) {
1071             remove(it.next(), butComment);
1072         }
1073         return this;
1074     }
1075 
1076     /** Code should explicitly include CODE_FALLBACK */
1077     public static final Pattern specialsToKeep =
1078             PatternCache.get(
1079                     "/("
1080                             + "measurementSystemName"
1081                             + "|codePattern"
1082                             + "|calendar\\[\\@type\\=\"[^\"]*\"\\]/(?!dateTimeFormats/appendItems)"
1083                             + // gregorian
1084                             "|numbers/symbols/(decimal/group)"
1085                             + "|timeZoneNames/(hourFormat|gmtFormat|regionFormat)"
1086                             + "|pattern"
1087                             + ")");
1088 
1089     public static final Pattern specialsToPushFromRoot =
1090             PatternCache.get(
1091                     "/("
1092                             + "calendar\\[\\@type\\=\"gregorian\"\\]/"
1093                             + "(?!fields)"
1094                             + "(?!dateTimeFormats/appendItems)"
1095                             + "(?!.*\\[@type=\"format\"].*\\[@type=\"narrow\"])"
1096                             + "(?!.*\\[@type=\"stand-alone\"].*\\[@type=\"(abbreviated|wide)\"])"
1097                             + "|numbers/symbols/(decimal/group)"
1098                             + "|timeZoneNames/(hourFormat|gmtFormat|regionFormat)"
1099                             + ")");
1100 
1101     private static final boolean MINIMIZE_ALT_PROPOSED = false;
1102 
1103     public interface RetentionTest {
1104         public enum Retention {
1105             RETAIN,
1106             REMOVE,
1107             RETAIN_IF_DIFFERENT
1108         }
1109 
getRetention(String path)1110         public Retention getRetention(String path);
1111     }
1112 
1113     /** Removes all items with same value */
removeDuplicates( CLDRFile other, boolean butComment, RetentionTest keepIfMatches, Collection<String> removedItems)1114     public CLDRFile removeDuplicates(
1115             CLDRFile other,
1116             boolean butComment,
1117             RetentionTest keepIfMatches,
1118             Collection<String> removedItems) {
1119         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
1120         // Matcher specialPathMatcher = dontRemoveSpecials ? specialsToKeep.matcher("") : null;
1121         boolean first = true;
1122         if (removedItems == null) {
1123             removedItems = new ArrayList<>();
1124         } else {
1125             removedItems.clear();
1126         }
1127         Set<String> checked = new HashSet<>();
1128         for (Iterator<String> it = iterator();
1129                 it.hasNext(); ) { // see what items we have that the other also has
1130             String curXpath = it.next();
1131             boolean logicDuplicate = true;
1132 
1133             if (!checked.contains(curXpath)) {
1134                 // we compare logic Group and only remove when all are duplicate
1135                 Set<String> logicGroups = LogicalGrouping.getPaths(this, curXpath);
1136                 if (logicGroups != null) {
1137                     Iterator<String> iter = logicGroups.iterator();
1138                     while (iter.hasNext() && logicDuplicate) {
1139                         String xpath = iter.next();
1140                         switch (keepIfMatches.getRetention(xpath)) {
1141                             case RETAIN:
1142                                 logicDuplicate = false;
1143                                 continue;
1144                             case RETAIN_IF_DIFFERENT:
1145                                 String currentValue = dataSource.getValueAtPath(xpath);
1146                                 if (currentValue == null) {
1147                                     logicDuplicate = false;
1148                                     continue;
1149                                 }
1150                                 String otherXpath = xpath;
1151                                 String otherValue = other.dataSource.getValueAtPath(otherXpath);
1152                                 if (!currentValue.equals(otherValue)) {
1153                                     if (MINIMIZE_ALT_PROPOSED) {
1154                                         otherXpath = CLDRFile.getNondraftNonaltXPath(xpath);
1155                                         if (otherXpath.equals(xpath)) {
1156                                             logicDuplicate = false;
1157                                             continue;
1158                                         }
1159                                         otherValue = other.dataSource.getValueAtPath(otherXpath);
1160                                         if (!currentValue.equals(otherValue)) {
1161                                             logicDuplicate = false;
1162                                             continue;
1163                                         }
1164                                     } else {
1165                                         logicDuplicate = false;
1166                                         continue;
1167                                     }
1168                                 }
1169                                 String keepValue =
1170                                         XMLSource.getPathsAllowingDuplicates().get(xpath);
1171                                 if (keepValue != null && keepValue.equals(currentValue)) {
1172                                     logicDuplicate = false;
1173                                     continue;
1174                                 }
1175                                 // we've now established that the values are the same
1176                                 String currentFullXPath = dataSource.getFullPath(xpath);
1177                                 String otherFullXPath = other.dataSource.getFullPath(otherXpath);
1178                                 if (!equalsIgnoringDraft(currentFullXPath, otherFullXPath)) {
1179                                     logicDuplicate = false;
1180                                     continue;
1181                                 }
1182                                 if (DEBUG) {
1183                                     keepIfMatches.getRetention(xpath);
1184                                 }
1185                                 break;
1186                             case REMOVE:
1187                                 if (DEBUG) {
1188                                     keepIfMatches.getRetention(xpath);
1189                                 }
1190                                 break;
1191                         }
1192                     }
1193 
1194                     if (first) {
1195                         first = false;
1196                         if (butComment) appendFinalComment("Duplicates removed:");
1197                     }
1198                 }
1199                 // we can't remove right away, since that disturbs the iterator.
1200                 checked.addAll(logicGroups);
1201                 if (logicDuplicate) {
1202                     removedItems.addAll(logicGroups);
1203                 }
1204                 // remove(xpath, butComment);
1205             }
1206         }
1207         // now remove them safely
1208         for (String xpath : removedItems) {
1209             remove(xpath, butComment);
1210         }
1211         return this;
1212     }
1213 
1214     /**
1215      * @return Returns the finalComment.
1216      */
getFinalComment()1217     public String getFinalComment() {
1218         return dataSource.getXpathComments().getFinalComment();
1219     }
1220 
1221     /**
1222      * @return Returns the finalComment.
1223      */
getInitialComment()1224     public String getInitialComment() {
1225         return dataSource.getXpathComments().getInitialComment();
1226     }
1227 
1228     /**
1229      * @return Returns the xpath_comments. Cloned for safety.
1230      */
getXpath_comments()1231     public XPathParts.Comments getXpath_comments() {
1232         return (XPathParts.Comments) dataSource.getXpathComments().clone();
1233     }
1234 
1235     /**
1236      * @return Returns the locale ID. In the case of a supplemental data file, it is
1237      *     SUPPLEMENTAL_NAME.
1238      */
1239     @Override
getLocaleID()1240     public String getLocaleID() {
1241         return dataSource.getLocaleID();
1242     }
1243 
1244     /**
1245      * @return the Locale ID, as declared in the //ldml/identity element
1246      */
getLocaleIDFromIdentity()1247     public String getLocaleIDFromIdentity() {
1248         ULocale.Builder lb = new ULocale.Builder();
1249         for (Iterator<String> i = iterator("//ldml/identity/"); i.hasNext(); ) {
1250             XPathParts xpp = XPathParts.getFrozenInstance(i.next());
1251             String k = xpp.getElement(-1);
1252             String v = xpp.getAttributeValue(-1, "type");
1253             if (k.equals("language")) {
1254                 lb = lb.setLanguage(v);
1255             } else if (k.equals("script")) {
1256                 lb = lb.setScript(v);
1257             } else if (k.equals("territory")) {
1258                 lb = lb.setRegion(v);
1259             } else if (k.equals("variant")) {
1260                 lb = lb.setVariant(v);
1261             }
1262         }
1263         return lb.build().toString(); // TODO: CLDRLocale ?
1264     }
1265 
1266     /**
1267      * Create xpaths for DateFormat that look like
1268      *
1269      * <pre>
1270      * //ldml/dates/calendars/calendar[@type="*"]/dateFormats/dateFormatLength[@type="*"]/dateFormat[@type="standard"]/pattern[@type="standard"]
1271      * //ldml/dates/calendars/calendar[@type="*"]/dateFormats/dateFormatLength[@type="*"]/dateFormat[@type="standard"]/pattern[@type="standard"][@numbers="*"]
1272      * </pre>
1273      *
1274      * @param calendar Calendar system identifier
1275      * @param length full, long, medium, short. "*" is a wildcard selector for XPath
1276      * @return
1277      */
getDateFormatXpath(String calendar, String length)1278     private String getDateFormatXpath(String calendar, String length) {
1279         String formatPattern =
1280                 "//ldml/dates/calendars/calendar[@type=\"%s\"]/dateFormats/dateFormatLength[@type=\"%s\"]/dateFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
1281         return String.format(formatPattern, calendar, length);
1282     }
1283 
1284     /**
1285      * Create xpaths for TimeFormat that look like
1286      *
1287      * <pre>
1288      * //ldml/dates/calendars/calendar[@type="*"]/timeFormats/timeFormatLength[@type="*"]/timeFormat[@type="standard"]/pattern[@type="standard"]
1289      * //ldml/dates/calendars/calendar[@type="*"]/timeFormats/timeFormatLength[@type="*"]/timeFormat[@type="standard"]/pattern[@type="standard"][@numbers="*"] // not currently used
1290      * </pre>
1291      *
1292      * @param calendar Calendar system idenfitier
1293      * @param length full, long, medium, short. "*" is a wildcard selector for XPath
1294      * @return
1295      */
getTimeFormatXpath(String calendar, String length)1296     private String getTimeFormatXpath(String calendar, String length) {
1297         String formatPattern =
1298                 "//ldml/dates/calendars/calendar[@type=\"%s\"]/timeFormats/timeFormatLength[@type=\"%s\"]/timeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
1299         return String.format(formatPattern, calendar, length);
1300     }
1301 
1302     /**
1303      * Create xpaths for the glue pattern from DateTimeFormat that look like
1304      *
1305      * <pre>
1306      *   //ldml/dates/calendars/calendar[@type="*"]/dateTimeFormats/dateTimeFormatLength[@type="*"]/dateTimeFormat[@type="standard"]/pattern[@type="standard"]
1307      *   //ldml/dates/calendars/calendar[@type="*"]/dateTimeFormats/dateTimeFormatLength[@type="*"]/dateTimeFormat[@type="atTime"]/pattern[@type="standard"]
1308      * </pre>
1309      *
1310      * @param calendar
1311      * @param length
1312      * @param formatType "standard" or "atTime"
1313      * @return
1314      */
getDateTimeFormatXpath(String calendar, String length, String formatType)1315     private String getDateTimeFormatXpath(String calendar, String length, String formatType) {
1316         String formatPattern =
1317                 "//ldml/dates/calendars/calendar[@type=\"%s\"]/dateTimeFormats/dateTimeFormatLength[@type=\"%s\"]/dateTimeFormat[@type=\"%s\"]/pattern[@type=\"standard\"]";
1318         return String.format(formatPattern, calendar, length, formatType);
1319     }
1320 
getDateFormat( String calendar, String length, ICUServiceBuilder icuServiceBuilder)1321     public SimpleDateFormat getDateFormat(
1322             String calendar, String length, ICUServiceBuilder icuServiceBuilder) {
1323         String dateFormatXPath = // Get standard dateFmt for same calendar & length as this
1324                 // dateTimePattern
1325                 this.getDateFormatXpath(calendar, length);
1326         String dateFormatValue = this.getWinningValue(dateFormatXPath);
1327 
1328         if (dateFormatValue == null) {
1329             return null;
1330         }
1331 
1332         XPathParts parts = XPathParts.getFrozenInstance(this.getFullXPath(dateFormatXPath));
1333         String dateNumbersOverride = parts.findAttributeValue("pattern", "numbers");
1334         return icuServiceBuilder.getDateFormat(calendar, dateFormatValue, dateNumbersOverride);
1335     }
1336 
getTimeFormat( String calendar, String length, ICUServiceBuilder icuServiceBuilder)1337     public SimpleDateFormat getTimeFormat(
1338             String calendar, String length, ICUServiceBuilder icuServiceBuilder) {
1339         String timeFormatXPath = this.getTimeFormatXpath(calendar, length);
1340         String timeFormatValue = this.getWinningValue(timeFormatXPath);
1341 
1342         if (timeFormatValue == null) {
1343             return null;
1344         }
1345 
1346         XPathParts parts = XPathParts.getFrozenInstance(this.getFullXPath(timeFormatXPath));
1347         String timeNumbersOverride = parts.findAttributeValue("pattern", "numbers");
1348         return icuServiceBuilder.getDateFormat(calendar, timeFormatValue, timeNumbersOverride);
1349     }
1350 
glueDateTimeFormat( String date, String time, String calendar, String length, String formatType, ICUServiceBuilder icuServiceBuilder)1351     public String glueDateTimeFormat(
1352             String date,
1353             String time,
1354             String calendar,
1355             String length,
1356             String formatType,
1357             ICUServiceBuilder icuServiceBuilder) {
1358         // calls getDateTimeFormatXpath, load the glue pattern, then call
1359         // glueDateTimeFormatWithGluePattern
1360         String xpath = this.getDateTimeFormatXpath(calendar, length, formatType);
1361         String fullpath = this.getFullXPath(xpath);
1362         String gluePattern = this.getWinningValue(xpath);
1363         return this.glueDateTimeFormatWithGluePattern(
1364                 date, time, calendar, gluePattern, icuServiceBuilder);
1365     }
1366 
glueDateTimeFormatWithGluePattern( String date, String time, String calendar, String gluePattern, ICUServiceBuilder icuServiceBuilder)1367     public String glueDateTimeFormatWithGluePattern(
1368             String date,
1369             String time,
1370             String calendar,
1371             String gluePattern,
1372             ICUServiceBuilder icuServiceBuilder) {
1373         // uses SimpleDateFormat to get rid of quotes
1374         SimpleDateFormat temp = icuServiceBuilder.getDateFormat(calendar, gluePattern, null);
1375         TimeZone tempTimeZone = TimeZone.GMT_ZONE;
1376         Calendar tempCalendar = Calendar.getInstance(tempTimeZone, ULocale.ENGLISH);
1377         Date tempDate = tempCalendar.getTime();
1378         String gluePatternWithoutQuotes = temp.format(tempDate);
1379 
1380         // uses MessageFormat to interpret the placeholders in the glue pattern
1381         return MessageFormat.format(gluePatternWithoutQuotes, (Object[]) new String[] {time, date});
1382     }
1383 
1384     /**
1385      * @see com.ibm.icu.util.Freezable#isFrozen()
1386      */
1387     @Override
isFrozen()1388     public synchronized boolean isFrozen() {
1389         return locked;
1390     }
1391 
1392     /**
1393      * @see com.ibm.icu.util.Freezable#freeze()
1394      */
1395     @Override
freeze()1396     public synchronized CLDRFile freeze() {
1397         locked = true;
1398         dataSource.freeze();
1399         return this;
1400     }
1401 
clearComments()1402     public CLDRFile clearComments() {
1403         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
1404         dataSource.setXpathComments(new XPathParts.Comments());
1405         return this;
1406     }
1407 
1408     /** Sets a final comment, replacing everything that was there. */
setFinalComment(String comment)1409     public CLDRFile setFinalComment(String comment) {
1410         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
1411         dataSource.getXpathComments().setFinalComment(comment);
1412         return this;
1413     }
1414 
1415     /** Adds a comment to the final list of comments. */
appendFinalComment(String comment)1416     public CLDRFile appendFinalComment(String comment) {
1417         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
1418         dataSource
1419                 .getXpathComments()
1420                 .setFinalComment(
1421                         CldrUtility.joinWithSeparation(
1422                                 dataSource.getXpathComments().getFinalComment(),
1423                                 XPathParts.NEWLINE,
1424                                 comment));
1425         return this;
1426     }
1427 
1428     /** Sets the initial comment, replacing everything that was there. */
setInitialComment(String comment)1429     public CLDRFile setInitialComment(String comment) {
1430         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
1431         dataSource.getXpathComments().setInitialComment(comment);
1432         return this;
1433     }
1434 
1435     // ========== STATIC UTILITIES ==========
1436 
1437     /**
1438      * Utility to restrict to files matching a given regular expression. The expression does not
1439      * contain ".xml". Note that supplementalData is always skipped, and root is always included.
1440      */
getMatchingXMLFiles(File sourceDirs[], Matcher m)1441     public static Set<String> getMatchingXMLFiles(File sourceDirs[], Matcher m) {
1442         Set<String> s = new TreeSet<>();
1443 
1444         for (File dir : sourceDirs) {
1445             if (!dir.exists()) {
1446                 throw new IllegalArgumentException("Directory doesn't exist:\t" + dir.getPath());
1447             }
1448             if (!dir.isDirectory()) {
1449                 throw new IllegalArgumentException(
1450                         "Input isn't a file directory:\t" + dir.getPath());
1451             }
1452             File[] files = dir.listFiles();
1453             for (int i = 0; i < files.length; ++i) {
1454                 String name = files[i].getName();
1455                 if (!name.endsWith(".xml") || name.startsWith(".")) continue;
1456                 // if (name.startsWith(SUPPLEMENTAL_NAME)) continue;
1457                 String locale = name.substring(0, name.length() - 4); // drop .xml
1458                 if (!m.reset(locale).matches()) continue;
1459                 s.add(locale);
1460             }
1461         }
1462         return s;
1463     }
1464 
1465     @Override
iterator()1466     public Iterator<String> iterator() {
1467         return dataSource.iterator();
1468     }
1469 
iterator(String prefix)1470     public synchronized Iterator<String> iterator(String prefix) {
1471         return dataSource.iterator(prefix);
1472     }
1473 
iterator(Matcher pathFilter)1474     public Iterator<String> iterator(Matcher pathFilter) {
1475         return dataSource.iterator(pathFilter);
1476     }
1477 
iterator(String prefix, Comparator<String> comparator)1478     public Iterator<String> iterator(String prefix, Comparator<String> comparator) {
1479         Iterator<String> it =
1480                 (prefix == null || prefix.length() == 0)
1481                         ? dataSource.iterator()
1482                         : dataSource.iterator(prefix);
1483         if (comparator == null) return it;
1484         Set<String> orderedSet = new TreeSet<>(comparator);
1485         it.forEachRemaining(orderedSet::add);
1486         return orderedSet.iterator();
1487     }
1488 
fullIterable()1489     public Iterable<String> fullIterable() {
1490         return new FullIterable(this);
1491     }
1492 
1493     public static class FullIterable implements Iterable<String>, SimpleIterator<String> {
1494         private final CLDRFile file;
1495         private final Iterator<String> fileIterator;
1496         private Iterator<String> extraPaths;
1497 
FullIterable(CLDRFile file)1498         FullIterable(CLDRFile file) {
1499             this.file = file;
1500             this.fileIterator = file.iterator();
1501         }
1502 
1503         @Override
iterator()1504         public Iterator<String> iterator() {
1505             return With.toIterator(this);
1506         }
1507 
1508         @Override
next()1509         public String next() {
1510             if (fileIterator.hasNext()) {
1511                 return fileIterator.next();
1512             }
1513             if (extraPaths == null) {
1514                 extraPaths = file.getExtraPaths().iterator();
1515             }
1516             if (extraPaths.hasNext()) {
1517                 return extraPaths.next();
1518             }
1519             return null;
1520         }
1521     }
1522 
getDistinguishingXPath(String xpath, String[] normalizedPath)1523     public static String getDistinguishingXPath(String xpath, String[] normalizedPath) {
1524         return DistinguishedXPath.getDistinguishingXPath(xpath, normalizedPath);
1525     }
1526 
equalsIgnoringDraft(String path1, String path2)1527     private static boolean equalsIgnoringDraft(String path1, String path2) {
1528         if (path1 == path2) {
1529             return true;
1530         }
1531         if (path1 == null || path2 == null) {
1532             return false;
1533         }
1534         // TODO: optimize
1535         if (path1.indexOf("[@draft=") < 0 && path2.indexOf("[@draft=") < 0) {
1536             return path1.equals(path2);
1537         }
1538         return getNondraftNonaltXPath(path1).equals(getNondraftNonaltXPath(path2));
1539     }
1540 
1541     /*
1542      * TODO: clarify the need for syncObject.
1543      * Formerly, an XPathParts object named "nondraftParts" was used for this purpose, but
1544      * there was no evident reason for it to be an XPathParts object rather than any other
1545      * kind of object.
1546      */
1547     private static Object syncObject = new Object();
1548 
getNondraftNonaltXPath(String xpath)1549     public static String getNondraftNonaltXPath(String xpath) {
1550         if (xpath.indexOf("draft=\"") < 0 && xpath.indexOf("alt=\"") < 0) {
1551             return xpath;
1552         }
1553         synchronized (syncObject) {
1554             XPathParts parts =
1555                     XPathParts.getFrozenInstance(xpath)
1556                             .cloneAsThawed(); // can't be frozen since we call removeAttributes
1557             String restore;
1558             HashSet<String> toRemove = new HashSet<>();
1559             for (int i = 0; i < parts.size(); ++i) {
1560                 if (parts.getAttributeCount(i) == 0) {
1561                     continue;
1562                 }
1563                 Map<String, String> attributes = parts.getAttributes(i);
1564                 toRemove.clear();
1565                 restore = null;
1566                 for (Iterator<String> it = attributes.keySet().iterator(); it.hasNext(); ) {
1567                     String attribute = it.next();
1568                     if (attribute.equals("draft")) {
1569                         toRemove.add(attribute);
1570                     } else if (attribute.equals("alt")) {
1571                         String value = attributes.get(attribute);
1572                         int proposedPos = value.indexOf("proposed");
1573                         if (proposedPos >= 0) {
1574                             toRemove.add(attribute);
1575                             if (proposedPos > 0) {
1576                                 restore =
1577                                         value.substring(
1578                                                 0, proposedPos - 1); // is of form xxx-proposedyyy
1579                             }
1580                         }
1581                     }
1582                 }
1583                 parts.removeAttributes(i, toRemove);
1584                 if (restore != null) {
1585                     attributes.put("alt", restore);
1586                 }
1587             }
1588             return parts.toString();
1589         }
1590     }
1591 
1592     /**
1593      * Determine if an attribute is a distinguishing attribute.
1594      *
1595      * @param elementName
1596      * @param attribute
1597      * @return
1598      */
isDistinguishing(DtdType type, String elementName, String attribute)1599     public static boolean isDistinguishing(DtdType type, String elementName, String attribute) {
1600         return DtdData.getInstance(type).isDistinguishing(elementName, attribute);
1601     }
1602 
1603     /** Utility to create a validating XML reader. */
createXMLReader(boolean validating)1604     public static XMLReader createXMLReader(boolean validating) {
1605         String[] testList = {
1606             "org.apache.xerces.parsers.SAXParser",
1607             "org.apache.crimson.parser.XMLReaderImpl",
1608             "gnu.xml.aelfred2.XmlReader",
1609             "com.bluecast.xml.Piccolo",
1610             "oracle.xml.parser.v2.SAXParser",
1611             ""
1612         };
1613         XMLReader result = null;
1614         for (int i = 0; i < testList.length; ++i) {
1615             try {
1616                 result =
1617                         (testList[i].length() != 0)
1618                                 ? XMLReaderFactory.createXMLReader(testList[i])
1619                                 : XMLReaderFactory.createXMLReader();
1620                 result.setFeature("http://xml.org/sax/features/validation", validating);
1621                 break;
1622             } catch (SAXException e1) {
1623             }
1624         }
1625         if (result == null)
1626             throw new NoClassDefFoundError(
1627                     "No SAX parser is available, or unable to set validation correctly");
1628         return result;
1629     }
1630 
1631     /**
1632      * Return a directory to supplemental data used by this CLDRFile. If the CLDRFile is not
1633      * normally disk-based, the returned directory may be temporary and not guaranteed to exist past
1634      * the lifetime of the CLDRFile. The directory should be considered read-only.
1635      */
getSupplementalDirectory()1636     public File getSupplementalDirectory() {
1637         if (supplementalDirectory == null) {
1638             // ask CLDRConfig.
1639             supplementalDirectory =
1640                     CLDRConfig.getInstance().getSupplementalDataInfo().getDirectory();
1641         }
1642         return supplementalDirectory;
1643     }
1644 
setSupplementalDirectory(File supplementalDirectory)1645     public CLDRFile setSupplementalDirectory(File supplementalDirectory) {
1646         this.supplementalDirectory = supplementalDirectory;
1647         return this;
1648     }
1649 
1650     /**
1651      * Convenience function to return a list of XML files in the Supplemental directory.
1652      *
1653      * @return all files ending in ".xml"
1654      * @see #getSupplementalDirectory()
1655      */
getSupplementalXMLFiles()1656     public File[] getSupplementalXMLFiles() {
1657         return getSupplementalDirectory()
1658                 .listFiles(
1659                         new FilenameFilter() {
1660                             @Override
1661                             public boolean accept(
1662                                     @SuppressWarnings("unused") File dir, String name) {
1663                                 return name.endsWith(".xml");
1664                             }
1665                         });
1666     }
1667 
1668     /**
1669      * Convenience function to return a specific supplemental file
1670      *
1671      * @param filename the file to return
1672      * @return the file (may not exist)
1673      * @see #getSupplementalDirectory()
1674      */
1675     public File getSupplementalFile(String filename) {
1676         return new File(getSupplementalDirectory(), filename);
1677     }
1678 
1679     public static boolean isSupplementalName(String localeName) {
1680         return SUPPLEMENTAL_NAMES.contains(localeName);
1681     }
1682 
1683     // static String[] keys = {"calendar", "collation", "currency"};
1684     //
1685     // static String[] calendar_keys = {"buddhist", "chinese", "gregorian", "hebrew", "islamic",
1686     // "islamic-civil",
1687     // "japanese"};
1688     // static String[] collation_keys = {"phonebook", "traditional", "direct", "pinyin", "stroke",
1689     // "posix", "big5han",
1690     // "gb2312han"};
1691 
1692     /*    */
1693     /**
1694      * Value that contains a node. WARNING: this is not done yet, and may change. In particular, we
1695      * don't want to return a Node, since that is mutable, and makes caching unsafe!!
1696      */
1697     /*
1698      * static public class NodeValue extends Value {
1699      * private Node nodeValue;
1700      */
1701     /**
1702      * Creation. WARNING, may change.
1703      *
1704      * @param value
1705      * @param currentFullXPath
1706      */
1707     /*
1708      * public NodeValue(Node value, String currentFullXPath) {
1709      * super(currentFullXPath);
1710      * this.nodeValue = value;
1711      * }
1712      */
1713     /** boilerplate */
1714 
1715     /*
1716      * public boolean hasSameValue(Object other) {
1717      * if (super.hasSameValue(other)) return false;
1718      * return nodeValue.equals(((NodeValue)other).nodeValue);
1719      * }
1720      */
1721     /** boilerplate */
1722     /*
1723      * public String getStringValue() {
1724      * return nodeValue.toString();
1725      * }
1726      * (non-Javadoc)
1727      *
1728      * @see org.unicode.cldr.util.CLDRFile.Value#changePath(java.lang.String)
1729      *
1730      * public Value changePath(String string) {
1731      * return new NodeValue(nodeValue, string);
1732      * }
1733      * }
1734      */
1735 
1736     private static class MyDeclHandler implements AllHandler {
1737         private static UnicodeSet whitespace = new UnicodeSet("[:whitespace:]");
1738         private DraftStatus minimalDraftStatus;
1739         private static final boolean SHOW_START_END = false;
1740         private int commentStack;
1741         private boolean justPopped = false;
1742         private String lastChars = "";
1743         // private String currentXPath = "/";
1744         private String currentFullXPath = "/";
1745         private String comment = null;
1746         private Map<String, String> attributeOrder;
1747         private DtdData dtdData;
1748         private CLDRFile target;
1749         private String lastActiveLeafNode;
1750         private String lastLeafNode;
1751         private int isSupplemental = -1;
1752         private int[] orderedCounter =
1753                 new int[30]; // just make deep enough to handle any CLDR file.
1754         private String[] orderedString =
1755                 new String[30]; // just make deep enough to handle any CLDR file.
1756         private int level = 0;
1757         private int overrideCount = 0;
1758         private Locator documentLocator = null;
1759 
1760         MyDeclHandler(CLDRFile target, DraftStatus minimalDraftStatus) {
1761             this.target = target;
1762             this.minimalDraftStatus = minimalDraftStatus;
1763         }
1764 
1765         private String show(Attributes attributes) {
1766             if (attributes == null) return "null";
1767             String result = "";
1768             for (int i = 0; i < attributes.getLength(); ++i) {
1769                 String attribute = attributes.getQName(i);
1770                 String value = attributes.getValue(i);
1771                 result += "[@" + attribute + "=\"" + value + "\"]"; // TODO quote the value??
1772             }
1773             return result;
1774         }
1775 
1776         private void push(String qName, Attributes attributes) {
1777             // SHOW_ALL &&
1778             Log.logln(LOG_PROGRESS, "push\t" + qName + "\t" + show(attributes));
1779             ++level;
1780             if (!qName.equals(orderedString[level])) {
1781                 // orderedCounter[level] = 0;
1782                 orderedString[level] = qName;
1783             }
1784             if (lastChars.length() != 0) {
1785                 if (whitespace.containsAll(lastChars)) lastChars = "";
1786                 else
1787                     throw new IllegalArgumentException(
1788                             "Must not have mixed content: "
1789                                     + qName
1790                                     + ", "
1791                                     + show(attributes)
1792                                     + ", Content: "
1793                                     + lastChars);
1794             }
1795             // currentXPath += "/" + qName;
1796             currentFullXPath += "/" + qName;
1797             // if (!isSupplemental) ldmlComparator.addElement(qName);
1798             if (dtdData.isOrdered(qName)) {
1799                 currentFullXPath += orderingAttribute();
1800             }
1801             if (attributes.getLength() > 0) {
1802                 attributeOrder.clear();
1803                 for (int i = 0; i < attributes.getLength(); ++i) {
1804                     String attribute = attributes.getQName(i);
1805                     String value = attributes.getValue(i);
1806 
1807                     // if (!isSupplemental) ldmlComparator.addAttribute(attribute); // must do
1808                     // BEFORE put
1809                     // ldmlComparator.addValue(value);
1810                     // special fix to remove version
1811                     // <!ATTLIST version number CDATA #REQUIRED >
1812                     // <!ATTLIST version cldrVersion CDATA #FIXED "24" >
1813                     if (attribute.equals("cldrVersion") && (qName.equals("version"))) {
1814                         ((SimpleXMLSource) target.dataSource)
1815                                 .setDtdVersionInfo(VersionInfo.getInstance(value));
1816                     } else {
1817                         putAndFixDeprecatedAttribute(qName, attribute, value);
1818                     }
1819                 }
1820                 for (Iterator<String> it = attributeOrder.keySet().iterator(); it.hasNext(); ) {
1821                     String attribute = it.next();
1822                     String value = attributeOrder.get(attribute);
1823                     String both =
1824                             "[@" + attribute + "=\"" + value + "\"]"; // TODO quote the value??
1825                     currentFullXPath += both;
1826                     // distinguishing = key, registry, alt, and type (except for the type attribute
1827                     // on the elements
1828                     // default and mapping).
1829                     // if (isDistinguishing(qName, attribute)) {
1830                     // currentXPath += both;
1831                     // }
1832                 }
1833             }
1834             if (comment != null) {
1835                 if (currentFullXPath.equals("//ldml")
1836                         || currentFullXPath.equals("//supplementalData")) {
1837                     target.setInitialComment(comment);
1838                 } else {
1839                     target.addComment(
1840                             currentFullXPath, comment, XPathParts.Comments.CommentType.PREBLOCK);
1841                 }
1842                 comment = null;
1843             }
1844             justPopped = false;
1845             lastActiveLeafNode = null;
1846             Log.logln(LOG_PROGRESS, "currentFullXPath\t" + currentFullXPath);
1847         }
1848 
1849         private String orderingAttribute() {
1850             return "[@_q=\"" + (orderedCounter[level]++) + "\"]";
1851         }
1852 
1853         private void putAndFixDeprecatedAttribute(String element, String attribute, String value) {
1854             if (attribute.equals("draft")) {
1855                 if (value.equals("true")) value = "approved";
1856                 else if (value.equals("false")) value = "unconfirmed";
1857             } else if (attribute.equals("type")) {
1858                 if (changedTypes.contains(element)
1859                         && isSupplemental < 1) { // measurementSystem for example did not
1860                     // change from 'type' to 'choice'.
1861                     attribute = "choice";
1862                 }
1863             }
1864             // else if (element.equals("dateFormatItem")) {
1865             // if (attribute.equals("id")) {
1866             // String newValue = dateGenerator.getBaseSkeleton(value);
1867             // if (!fixedSkeletons.contains(newValue)) {
1868             // fixedSkeletons.add(newValue);
1869             // if (!value.equals(newValue)) {
1870             // System.out.println(value + " => " + newValue);
1871             // }
1872             // value = newValue;
1873             // }
1874             // }
1875             // }
1876             attributeOrder.put(attribute, value);
1877         }
1878 
1879         // private Set<String> fixedSkeletons = new HashSet();
1880 
1881         // private DateTimePatternGenerator dateGenerator =
1882         // DateTimePatternGenerator.getEmptyInstance();
1883 
1884         /** Types which changed from 'type' to 'choice', but not in supplemental data. */
1885         private static Set<String> changedTypes =
1886                 new HashSet<>(
1887                         Arrays.asList(
1888                                 new String[] {
1889                                     "abbreviationFallback",
1890                                     "default",
1891                                     "mapping",
1892                                     "measurementSystem",
1893                                     "preferenceOrdering"
1894                                 }));
1895 
1896         Matcher draftMatcher = DRAFT_PATTERN.matcher("");
1897 
1898         /**
1899          * Adds a parsed XPath to the CLDRFile.
1900          *
1901          * @param fullXPath
1902          * @param value
1903          */
1904         private void addPath(String fullXPath, String value) {
1905             String former = target.getStringValue(fullXPath);
1906             if (former != null) {
1907                 String formerPath = target.getFullXPath(fullXPath);
1908                 if (!former.equals(value) || !fullXPath.equals(formerPath)) {
1909                     if (!fullXPath.startsWith("//ldml/identity/version")
1910                             && !fullXPath.startsWith("//ldml/identity/generation")) {
1911                         warnOnOverride(former, formerPath);
1912                     }
1913                 }
1914             }
1915             value = trimWhitespaceSpecial(value);
1916             target.add(fullXPath, value)
1917                     .addSourceLocation(fullXPath, new XMLSource.SourceLocation(documentLocator));
1918         }
1919 
1920         private void pop(String qName) {
1921             Log.logln(LOG_PROGRESS, "pop\t" + qName);
1922             --level;
1923 
1924             if (lastChars.length() != 0 || justPopped == false) {
1925                 boolean acceptItem = minimalDraftStatus == DraftStatus.unconfirmed;
1926                 if (!acceptItem) {
1927                     if (draftMatcher.reset(currentFullXPath).find()) {
1928                         DraftStatus foundStatus = DraftStatus.valueOf(draftMatcher.group(1));
1929                         if (minimalDraftStatus.compareTo(foundStatus) <= 0) {
1930                             // what we found is greater than or equal to our status
1931                             acceptItem = true;
1932                         }
1933                     } else {
1934                         acceptItem =
1935                                 true; // if not found, then the draft status is approved, so it is
1936                         // always ok
1937                     }
1938                 }
1939                 if (acceptItem) {
1940                     // Change any deprecated orientation attributes into values
1941                     // for backwards compatibility.
1942                     boolean skipAdd = false;
1943                     if (currentFullXPath.startsWith("//ldml/layout/orientation")) {
1944                         XPathParts parts = XPathParts.getFrozenInstance(currentFullXPath);
1945                         String value = parts.getAttributeValue(-1, "characters");
1946                         if (value != null) {
1947                             addPath("//ldml/layout/orientation/characterOrder", value);
1948                             skipAdd = true;
1949                         }
1950                         value = parts.getAttributeValue(-1, "lines");
1951                         if (value != null) {
1952                             addPath("//ldml/layout/orientation/lineOrder", value);
1953                             skipAdd = true;
1954                         }
1955                     }
1956                     if (!skipAdd) {
1957                         addPath(currentFullXPath, lastChars);
1958                     }
1959                     lastLeafNode = lastActiveLeafNode = currentFullXPath;
1960                 }
1961                 lastChars = "";
1962             } else {
1963                 Log.logln(
1964                         LOG_PROGRESS && lastActiveLeafNode != null,
1965                         "pop: zeroing last leafNode: " + lastActiveLeafNode);
1966                 lastActiveLeafNode = null;
1967                 if (comment != null) {
1968                     target.addComment(
1969                             lastLeafNode, comment, XPathParts.Comments.CommentType.POSTBLOCK);
1970                     comment = null;
1971                 }
1972             }
1973             // currentXPath = stripAfter(currentXPath, qName);
1974             currentFullXPath = stripAfter(currentFullXPath, qName);
1975             justPopped = true;
1976         }
1977 
1978         static Pattern WHITESPACE_WITH_LF = PatternCache.get("\\s*\\u000a\\s*");
1979         Matcher whitespaceWithLf = WHITESPACE_WITH_LF.matcher("");
1980         static final UnicodeSet CONTROLS = new UnicodeSet("[:cc:]");
1981 
1982         /**
1983          * Trim leading whitespace if there is a linefeed among them, then the same with trailing.
1984          *
1985          * @param source
1986          * @return
1987          */
1988         private String trimWhitespaceSpecial(String source) {
1989             if (DEBUG && CONTROLS.containsSome(source)) {
1990                 System.out.println("*** " + source);
1991             }
1992             if (!source.contains("\n")) {
1993                 return source;
1994             }
1995             source = whitespaceWithLf.reset(source).replaceAll("\n");
1996             return source;
1997         }
1998 
1999         private void warnOnOverride(String former, String formerPath) {
2000             String distinguishing = CLDRFile.getDistinguishingXPath(formerPath, null);
2001             System.out.println(
2002                     "\tERROR in "
2003                             + target.getLocaleID()
2004                             + ";\toverriding old value <"
2005                             + former
2006                             + "> at path "
2007                             + distinguishing
2008                             + "\twith\t<"
2009                             + lastChars
2010                             + ">"
2011                             + CldrUtility.LINE_SEPARATOR
2012                             + "\told fullpath: "
2013                             + formerPath
2014                             + CldrUtility.LINE_SEPARATOR
2015                             + "\tnew fullpath: "
2016                             + currentFullXPath);
2017             overrideCount += 1;
2018         }
2019 
2020         private static String stripAfter(String input, String qName) {
2021             int pos = findLastSlash(input);
2022             if (qName != null) {
2023                 // assert input.substring(pos+1).startsWith(qName);
2024                 if (!input.substring(pos + 1).startsWith(qName)) {
2025                     throw new IllegalArgumentException("Internal Error: should never get here.");
2026                 }
2027             }
2028             return input.substring(0, pos);
2029         }
2030 
2031         private static int findLastSlash(String input) {
2032             int braceStack = 0;
2033             char inQuote = 0;
2034             for (int i = input.length() - 1; i >= 0; --i) {
2035                 char ch = input.charAt(i);
2036                 switch (ch) {
2037                     case '\'':
2038                     case '"':
2039                         if (inQuote == 0) {
2040                             inQuote = ch;
2041                         } else if (inQuote == ch) {
2042                             inQuote = 0; // come out of quote
2043                         }
2044                         break;
2045                     case '/':
2046                         if (inQuote == 0 && braceStack == 0) {
2047                             return i;
2048                         }
2049                         break;
2050                     case '[':
2051                         if (inQuote == 0) {
2052                             --braceStack;
2053                         }
2054                         break;
2055                     case ']':
2056                         if (inQuote == 0) {
2057                             ++braceStack;
2058                         }
2059                         break;
2060                 }
2061             }
2062             return -1;
2063         }
2064 
2065         // SAX items we need to catch
2066 
2067         @Override
2068         public void startElement(String uri, String localName, String qName, Attributes attributes)
2069                 throws SAXException {
2070             Log.logln(
2071                     LOG_PROGRESS || SHOW_START_END,
2072                     "startElement uri\t"
2073                             + uri
2074                             + "\tlocalName "
2075                             + localName
2076                             + "\tqName "
2077                             + qName
2078                             + "\tattributes "
2079                             + show(attributes));
2080             try {
2081                 if (isSupplemental < 0) { // set by first element
2082                     attributeOrder =
2083                             new TreeMap<>(
2084                                     // HACK for ldmlIcu
2085                                     dtdData.dtdType == DtdType.ldml
2086                                             ? CLDRFile.getAttributeOrdering()
2087                                             : dtdData.getAttributeComparator());
2088                     isSupplemental = target.dtdType == DtdType.ldml ? 0 : 1;
2089                 }
2090                 push(qName, attributes);
2091             } catch (RuntimeException e) {
2092                 e.printStackTrace();
2093                 throw e;
2094             }
2095         }
2096 
2097         @Override
2098         public void endElement(String uri, String localName, String qName) throws SAXException {
2099             Log.logln(
2100                     LOG_PROGRESS || SHOW_START_END,
2101                     "endElement uri\t" + uri + "\tlocalName " + localName + "\tqName " + qName);
2102             try {
2103                 pop(qName);
2104             } catch (RuntimeException e) {
2105                 // e.printStackTrace();
2106                 throw e;
2107             }
2108         }
2109 
2110         // static final char XML_LINESEPARATOR = (char) 0xA;
2111         // static final String XML_LINESEPARATOR_STRING = String.valueOf(XML_LINESEPARATOR);
2112 
2113         @Override
2114         public void characters(char[] ch, int start, int length) throws SAXException {
2115             try {
2116                 String value = new String(ch, start, length);
2117                 Log.logln(LOG_PROGRESS, "characters:\t" + value);
2118                 // we will strip leading and trailing line separators in another place.
2119                 // if (value.indexOf(XML_LINESEPARATOR) >= 0) {
2120                 // value = value.replace(XML_LINESEPARATOR, '\u0020');
2121                 // }
2122                 lastChars += value;
2123                 justPopped = false;
2124             } catch (RuntimeException e) {
2125                 e.printStackTrace();
2126                 throw e;
2127             }
2128         }
2129 
2130         @Override
2131         public void startDTD(String name, String publicId, String systemId) throws SAXException {
2132             Log.logln(
2133                     LOG_PROGRESS,
2134                     "startDTD name: "
2135                             + name
2136                             + ", publicId: "
2137                             + publicId
2138                             + ", systemId: "
2139                             + systemId);
2140             commentStack++;
2141             target.dtdType = DtdType.fromElement(name);
2142             target.dtdData = dtdData = DtdData.getInstance(target.dtdType);
2143         }
2144 
2145         @Override
2146         public void endDTD() throws SAXException {
2147             Log.logln(LOG_PROGRESS, "endDTD");
2148             commentStack--;
2149         }
2150 
2151         @Override
2152         public void comment(char[] ch, int start, int length) throws SAXException {
2153             final String string = new String(ch, start, length);
2154             Log.logln(LOG_PROGRESS, commentStack + " comment " + string);
2155             try {
2156                 if (commentStack != 0) return;
2157                 String comment0 = trimWhitespaceSpecial(string).trim();
2158                 if (lastActiveLeafNode != null) {
2159                     target.addComment(
2160                             lastActiveLeafNode, comment0, XPathParts.Comments.CommentType.LINE);
2161                 } else {
2162                     comment =
2163                             (comment == null ? comment0 : comment + XPathParts.NEWLINE + comment0);
2164                 }
2165             } catch (RuntimeException e) {
2166                 e.printStackTrace();
2167                 throw e;
2168             }
2169         }
2170 
2171         @Override
2172         public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
2173             if (LOG_PROGRESS)
2174                 Log.logln(
2175                         LOG_PROGRESS,
2176                         "ignorableWhitespace length: "
2177                                 + length
2178                                 + ": "
2179                                 + Utility.hex(new String(ch, start, length)));
2180             // if (lastActiveLeafNode != null) {
2181             for (int i = start; i < start + length; ++i) {
2182                 if (ch[i] == '\n') {
2183                     Log.logln(
2184                             LOG_PROGRESS && lastActiveLeafNode != null,
2185                             "\\n: zeroing last leafNode: " + lastActiveLeafNode);
2186                     lastActiveLeafNode = null;
2187                     break;
2188                 }
2189             }
2190             // }
2191         }
2192 
2193         @Override
2194         public void startDocument() throws SAXException {
2195             Log.logln(LOG_PROGRESS, "startDocument");
2196             commentStack = 0; // initialize
2197         }
2198 
2199         @Override
2200         public void endDocument() throws SAXException {
2201             Log.logln(LOG_PROGRESS, "endDocument");
2202             try {
2203                 if (comment != null)
2204                     target.addComment(null, comment, XPathParts.Comments.CommentType.LINE);
2205             } catch (RuntimeException e) {
2206                 e.printStackTrace();
2207                 throw e;
2208             }
2209         }
2210 
2211         // ==== The following are just for debuggin =====
2212 
2213         @Override
2214         public void elementDecl(String name, String model) throws SAXException {
2215             Log.logln(LOG_PROGRESS, "Attribute\t" + name + "\t" + model);
2216         }
2217 
2218         @Override
2219         public void attributeDecl(
2220                 String eName, String aName, String type, String mode, String value)
2221                 throws SAXException {
2222             Log.logln(
2223                     LOG_PROGRESS,
2224                     "Attribute\t"
2225                             + eName
2226                             + "\t"
2227                             + aName
2228                             + "\t"
2229                             + type
2230                             + "\t"
2231                             + mode
2232                             + "\t"
2233                             + value);
2234         }
2235 
2236         @Override
2237         public void internalEntityDecl(String name, String value) throws SAXException {
2238             Log.logln(LOG_PROGRESS, "Internal Entity\t" + name + "\t" + value);
2239         }
2240 
2241         @Override
2242         public void externalEntityDecl(String name, String publicId, String systemId)
2243                 throws SAXException {
2244             Log.logln(LOG_PROGRESS, "Internal Entity\t" + name + "\t" + publicId + "\t" + systemId);
2245         }
2246 
2247         @Override
2248         public void processingInstruction(String target, String data) throws SAXException {
2249             Log.logln(LOG_PROGRESS, "processingInstruction: " + target + ", " + data);
2250         }
2251 
2252         @Override
2253         public void skippedEntity(String name) throws SAXException {
2254             Log.logln(LOG_PROGRESS, "skippedEntity: " + name);
2255         }
2256 
2257         @Override
2258         public void setDocumentLocator(Locator locator) {
2259             Log.logln(LOG_PROGRESS, "setDocumentLocator Locator " + locator);
2260             documentLocator = locator;
2261         }
2262 
2263         @Override
2264         public void startPrefixMapping(String prefix, String uri) throws SAXException {
2265             Log.logln(LOG_PROGRESS, "startPrefixMapping prefix: " + prefix + ", uri: " + uri);
2266         }
2267 
2268         @Override
2269         public void endPrefixMapping(String prefix) throws SAXException {
2270             Log.logln(LOG_PROGRESS, "endPrefixMapping prefix: " + prefix);
2271         }
2272 
2273         @Override
2274         public void startEntity(String name) throws SAXException {
2275             Log.logln(LOG_PROGRESS, "startEntity name: " + name);
2276         }
2277 
2278         @Override
2279         public void endEntity(String name) throws SAXException {
2280             Log.logln(LOG_PROGRESS, "endEntity name: " + name);
2281         }
2282 
2283         @Override
2284         public void startCDATA() throws SAXException {
2285             Log.logln(LOG_PROGRESS, "startCDATA");
2286         }
2287 
2288         @Override
2289         public void endCDATA() throws SAXException {
2290             Log.logln(LOG_PROGRESS, "endCDATA");
2291         }
2292 
2293         /*
2294          * (non-Javadoc)
2295          *
2296          * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
2297          */
2298         @Override
2299         public void error(SAXParseException exception) throws SAXException {
2300             Log.logln(LOG_PROGRESS || true, "error: " + showSAX(exception));
2301             throw exception;
2302         }
2303 
2304         /*
2305          * (non-Javadoc)
2306          *
2307          * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
2308          */
2309         @Override
2310         public void fatalError(SAXParseException exception) throws SAXException {
2311             Log.logln(LOG_PROGRESS, "fatalError: " + showSAX(exception));
2312             throw exception;
2313         }
2314 
2315         /*
2316          * (non-Javadoc)
2317          *
2318          * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
2319          */
2320         @Override
2321         public void warning(SAXParseException exception) throws SAXException {
2322             Log.logln(LOG_PROGRESS, "warning: " + showSAX(exception));
2323             throw exception;
2324         }
2325     }
2326 
2327     /** Show a SAX exception in a readable form. */
2328     public static String showSAX(SAXParseException exception) {
2329         return exception.getMessage()
2330                 + ";\t SystemID: "
2331                 + exception.getSystemId()
2332                 + ";\t PublicID: "
2333                 + exception.getPublicId()
2334                 + ";\t LineNumber: "
2335                 + exception.getLineNumber()
2336                 + ";\t ColumnNumber: "
2337                 + exception.getColumnNumber();
2338     }
2339 
2340     /** Says whether the whole file is draft */
2341     public boolean isDraft() {
2342         String item = iterator().next();
2343         return item.startsWith("//ldml[@draft=\"unconfirmed\"]");
2344     }
2345 
2346     // public Collection keySet(Matcher regexMatcher, Collection output) {
2347     // if (output == null) output = new ArrayList(0);
2348     // for (Iterator it = keySet().iterator(); it.hasNext();) {
2349     // String path = (String)it.next();
2350     // if (regexMatcher.reset(path).matches()) {
2351     // output.add(path);
2352     // }
2353     // }
2354     // return output;
2355     // }
2356 
2357     // public Collection keySet(String regexPattern, Collection output) {
2358     // return keySet(PatternCache.get(regexPattern).matcher(""), output);
2359     // }
2360 
2361     /**
2362      * Gets the type of a given xpath, eg script, territory, ... TODO move to separate class
2363      *
2364      * @param xpath
2365      * @return
2366      */
2367     public static int getNameType(String xpath) {
2368         for (int i = 0; i < NameTable.length; ++i) {
2369             if (!xpath.startsWith(NameTable[i][0])) continue;
2370             if (xpath.indexOf(NameTable[i][1], NameTable[i][0].length()) >= 0) return i;
2371         }
2372         return -1;
2373     }
2374 
2375     /** Gets the display name for a type */
2376     public static String getNameTypeName(int index) {
2377         try {
2378             return getNameName(index);
2379         } catch (Exception e) {
2380             return "Illegal Type Name: " + index;
2381         }
2382     }
2383 
2384     public static final int NO_NAME = -1,
2385             LANGUAGE_NAME = 0,
2386             SCRIPT_NAME = 1,
2387             TERRITORY_NAME = 2,
2388             VARIANT_NAME = 3,
2389             CURRENCY_NAME = 4,
2390             CURRENCY_SYMBOL = 5,
2391             TZ_EXEMPLAR = 6,
2392             TZ_START = TZ_EXEMPLAR,
2393             TZ_GENERIC_LONG = 7,
2394             TZ_GENERIC_SHORT = 8,
2395             TZ_STANDARD_LONG = 9,
2396             TZ_STANDARD_SHORT = 10,
2397             TZ_DAYLIGHT_LONG = 11,
2398             TZ_DAYLIGHT_SHORT = 12,
2399             TZ_LIMIT = 13,
2400             KEY_NAME = 13,
2401             KEY_TYPE_NAME = 14,
2402             SUBDIVISION_NAME = 15,
2403             LIMIT_TYPES = 15;
2404 
2405     private static final String[][] NameTable = {
2406         {"//ldml/localeDisplayNames/languages/language[@type=\"", "\"]", "language"},
2407         {"//ldml/localeDisplayNames/scripts/script[@type=\"", "\"]", "script"},
2408         {"//ldml/localeDisplayNames/territories/territory[@type=\"", "\"]", "territory"},
2409         {"//ldml/localeDisplayNames/variants/variant[@type=\"", "\"]", "variant"},
2410         {"//ldml/numbers/currencies/currency[@type=\"", "\"]/displayName", "currency"},
2411         {"//ldml/numbers/currencies/currency[@type=\"", "\"]/symbol", "currency-symbol"},
2412         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/exemplarCity", "exemplar-city"},
2413         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/generic", "tz-generic-long"},
2414         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/generic", "tz-generic-short"},
2415         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/standard", "tz-standard-long"},
2416         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/standard", "tz-standard-short"},
2417         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/daylight", "tz-daylight-long"},
2418         {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/daylight", "tz-daylight-short"},
2419         {"//ldml/localeDisplayNames/keys/key[@type=\"", "\"]", "key"},
2420         {"//ldml/localeDisplayNames/types/type[@key=\"", "\"][@type=\"", "\"]", "key|type"},
2421         {"//ldml/localeDisplayNames/subdivisions/subdivision[@type=\"", "\"]", "subdivision"},
2422 
2423         /**
2424          * <long> <generic>Newfoundland Time</generic> <standard>Newfoundland Standard
2425          * Time</standard> <daylight>Newfoundland Daylight Time</daylight> </long> - <short>
2426          * <generic>NT</generic> <standard>NST</standard> <daylight>NDT</daylight> </short>
2427          */
2428     };
2429 
2430     // private static final String[] TYPE_NAME = {"language", "script", "territory", "variant",
2431     // "currency",
2432     // "currency-symbol",
2433     // "tz-exemplar",
2434     // "tz-generic-long", "tz-generic-short"};
2435 
2436     public Iterator<String> getAvailableIterator(int type) {
2437         return iterator(NameTable[type][0]);
2438     }
2439 
2440     /**
2441      * @return the xpath used to access data of a given type
2442      */
2443     public static String getKey(int type, String code) {
2444         switch (type) {
2445             case VARIANT_NAME:
2446                 code = code.toUpperCase(Locale.ROOT);
2447                 break;
2448             case KEY_NAME:
2449                 code = fixKeyName(code);
2450                 break;
2451             case TZ_DAYLIGHT_LONG:
2452             case TZ_DAYLIGHT_SHORT:
2453             case TZ_EXEMPLAR:
2454             case TZ_GENERIC_LONG:
2455             case TZ_GENERIC_SHORT:
2456             case TZ_STANDARD_LONG:
2457             case TZ_STANDARD_SHORT:
2458                 code = getLongTzid(code);
2459                 break;
2460         }
2461         String[] nameTableRow = NameTable[type];
2462         if (code.contains("|")) {
2463             String[] codes = code.split("\\|");
2464             return nameTableRow[0]
2465                     + fixKeyName(codes[0])
2466                     + nameTableRow[1]
2467                     + codes[1]
2468                     + nameTableRow[2];
2469         } else {
2470             return nameTableRow[0] + code + nameTableRow[1];
2471         }
2472     }
2473 
2474     static final Relation<R2<String, String>, String> bcp47AliasMap =
2475             CLDRConfig.getInstance().getSupplementalDataInfo().getBcp47Aliases();
2476 
2477     public static String getLongTzid(String code) {
2478         if (!code.contains("/")) {
2479             Set<String> codes = bcp47AliasMap.get(Row.of("tz", code));
2480             if (codes != null && !codes.isEmpty()) {
2481                 code = codes.iterator().next();
2482             }
2483         }
2484         return code;
2485     }
2486 
2487     static final ImmutableMap<String, String> FIX_KEY_NAME;
2488 
2489     static {
2490         Builder<String, String> temp = ImmutableMap.builder();
2491         for (String s :
2492                 Arrays.asList(
2493                         "colAlternate",
2494                         "colBackwards",
2495                         "colCaseFirst",
2496                         "colCaseLevel",
2497                         "colNormalization",
2498                         "colNumeric",
2499                         "colReorder",
2500                         "colStrength")) {
2501             temp.put(s.toLowerCase(Locale.ROOT), s);
2502         }
2503         FIX_KEY_NAME = temp.build();
2504     }
2505 
2506     private static String fixKeyName(String code) {
2507         String result = FIX_KEY_NAME.get(code);
2508         return result == null ? code : result;
2509     }
2510 
2511     /**
2512      * @return the code used to access data of a given type from the path. Null if not found.
2513      */
2514     public static String getCode(String path) {
2515         int type = getNameType(path);
2516         if (type < 0) {
2517             throw new IllegalArgumentException("Illegal type in path: " + path);
2518         }
2519         String[] nameTableRow = NameTable[type];
2520         int start = nameTableRow[0].length();
2521         int end = path.indexOf(nameTableRow[1], start);
2522         return path.substring(start, end);
2523     }
2524 
2525     /**
2526      * @param type a string such as "language", "script", "territory", "region", ...
2527      * @return the corresponding integer
2528      */
2529     public static int typeNameToCode(String type) {
2530         if (type.equalsIgnoreCase("region")) {
2531             type = "territory";
2532         }
2533         for (int i = 0; i < LIMIT_TYPES; ++i) {
2534             if (type.equalsIgnoreCase(getNameName(i))) {
2535                 return i;
2536             }
2537         }
2538         return -1;
2539     }
2540 
2541     /** For use in getting short names. */
2542     public static final Transform<String, String> SHORT_ALTS =
2543             new Transform<>() {
2544                 @Override
2545                 public String transform(@SuppressWarnings("unused") String source) {
2546                     return "short";
2547                 }
2548             };
2549 
2550     /** Returns the name of a type. */
2551     public static String getNameName(int choice) {
2552         String[] nameTableRow = NameTable[choice];
2553         return nameTableRow[nameTableRow.length - 1];
2554     }
2555 
2556     /**
2557      * Get standard ordering for elements.
2558      *
2559      * @return ordered collection with items.
2560      * @deprecated
2561      */
2562     @Deprecated
2563     public static List<String> getElementOrder() {
2564         return Collections.emptyList(); // elementOrdering.getOrder(); // already unmodifiable
2565     }
2566 
2567     /**
2568      * Get standard ordering for attributes.
2569      *
2570      * @return ordered collection with items.
2571      */
2572     public static List<String> getAttributeOrder() {
2573         return getAttributeOrdering().getOrder(); // already unmodifiable
2574     }
2575 
2576     public static boolean isOrdered(String element, DtdType type) {
2577         return DtdData.getInstance(type).isOrdered(element);
2578     }
2579 
2580     private static Comparator<String> ldmlComparator =
2581             DtdData.getInstance(DtdType.ldmlICU).getDtdComparator(null);
2582 
2583     private static final Map<String, Map<String, String>> defaultSuppressionMap;
2584 
2585     static {
2586         String[][] data = {
2587             {"ldml", "version", GEN_VERSION},
2588             {"version", "cldrVersion", "*"},
2589             {"orientation", "characters", "left-to-right"},
2590             {"orientation", "lines", "top-to-bottom"},
2591             {"weekendStart", "time", "00:00"},
2592             {"weekendEnd", "time", "24:00"},
2593             {"dateFormat", "type", "standard"},
2594             {"timeFormat", "type", "standard"},
2595             {"dateTimeFormat", "type", "standard"},
2596             {"decimalFormat", "type", "standard"},
2597             {"scientificFormat", "type", "standard"},
2598             {"percentFormat", "type", "standard"},
2599             {"pattern", "type", "standard"},
2600             {"currency", "type", "standard"},
2601             {"transform", "visibility", "external"},
2602             {"*", "_q", "*"},
2603         };
2604         Map<String, Map<String, String>> tempmain = asMap(data, true);
2605         defaultSuppressionMap = Collections.unmodifiableMap(tempmain);
2606     }
2607 
2608     public static Map<String, Map<String, String>> getDefaultSuppressionMap() {
2609         return defaultSuppressionMap;
2610     }
2611 
2612     @SuppressWarnings({"rawtypes", "unchecked"})
2613     private static Map asMap(String[][] data, boolean tree) {
2614         Map tempmain = tree ? (Map) new TreeMap() : new HashMap();
2615         int len = data[0].length; // must be same for all elements
2616         for (int i = 0; i < data.length; ++i) {
2617             Map temp = tempmain;
2618             if (len != data[i].length) {
2619                 throw new IllegalArgumentException("Must be square array: fails row " + i);
2620             }
2621             for (int j = 0; j < len - 2; ++j) {
2622                 Map newTemp = (Map) temp.get(data[i][j]);
2623                 if (newTemp == null) {
2624                     temp.put(data[i][j], newTemp = tree ? (Map) new TreeMap() : new HashMap());
2625                 }
2626                 temp = newTemp;
2627             }
2628             temp.put(data[i][len - 2], data[i][len - 1]);
2629         }
2630         return tempmain;
2631     }
2632 
2633     /** Removes a comment. */
2634     public CLDRFile removeComment(String string) {
2635         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
2636         dataSource.getXpathComments().removeComment(string);
2637         return this;
2638     }
2639 
2640     /**
2641      * @param draftStatus TODO
2642      */
2643     public CLDRFile makeDraft(DraftStatus draftStatus) {
2644         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
2645         for (Iterator<String> it = dataSource.iterator(); it.hasNext(); ) {
2646             String path = it.next();
2647             XPathParts parts =
2648                     XPathParts.getFrozenInstance(dataSource.getFullPath(path))
2649                             .cloneAsThawed(); // not frozen, for addAttribute
2650             parts.addAttribute("draft", draftStatus.toString());
2651             dataSource.putValueAtPath(parts.toString(), dataSource.getValueAtPath(path));
2652         }
2653         return this;
2654     }
2655 
2656     public UnicodeSet getExemplarSet(String type, WinningChoice winningChoice) {
2657         return getExemplarSet(type, winningChoice, UnicodeSet.CASE);
2658     }
2659 
2660     public UnicodeSet getExemplarSet(ExemplarType type, WinningChoice winningChoice) {
2661         return getExemplarSet(type, winningChoice, UnicodeSet.CASE);
2662     }
2663 
2664     static final UnicodeSet HACK_CASE_CLOSURE_SET =
2665             new UnicodeSet(
2666                             "[ſẛffẞ{i̇}\u1F71\u1F73\u1F75\u1F77\u1F79\u1F7B\u1F7D\u1FBB\u1FBE\u1FC9\u1FCB\u1FD3\u1FDB\u1FE3\u1FEB\u1FF9\u1FFB\u2126\u212A\u212B]")
2667                     .freeze();
2668 
2669     public enum ExemplarType {
2670         main,
2671         auxiliary,
2672         index,
2673         punctuation,
2674         numbers;
2675 
2676         public static ExemplarType fromString(String type) {
2677             return type.isEmpty() ? main : valueOf(type);
2678         }
2679     }
2680 
2681     public UnicodeSet getExemplarSet(String type, WinningChoice winningChoice, int option) {
2682         return getExemplarSet(ExemplarType.fromString(type), winningChoice, option);
2683     }
2684 
2685     public UnicodeSet getExemplarSet(ExemplarType type, WinningChoice winningChoice, int option) {
2686         UnicodeSet result = getRawExemplarSet(type, winningChoice);
2687         if (result.isEmpty()) {
2688             return result.cloneAsThawed();
2689         }
2690         UnicodeSet toNuke = new UnicodeSet(HACK_CASE_CLOSURE_SET).removeAll(result);
2691         result.closeOver(UnicodeSet.CASE);
2692         result.removeAll(toNuke);
2693         result.remove(0x20);
2694         return result;
2695     }
2696 
2697     public UnicodeSet getRawExemplarSet(ExemplarType type, WinningChoice winningChoice) {
2698         String path = getExemplarPath(type);
2699         if (winningChoice == WinningChoice.WINNING) {
2700             path = getWinningPath(path);
2701         }
2702         String v = getStringValueWithBailey(path);
2703         if (v == null) {
2704             return UnicodeSet.EMPTY;
2705         }
2706         UnicodeSet result = SimpleUnicodeSetFormatter.parseLenient(v);
2707         return result;
2708     }
2709 
2710     public static String getExemplarPath(ExemplarType type) {
2711         return "//ldml/characters/exemplarCharacters"
2712                 + (type == ExemplarType.main ? "" : "[@type=\"" + type + "\"]");
2713     }
2714 
2715     public enum NumberingSystem {
2716         latin(null),
2717         defaultSystem("//ldml/numbers/defaultNumberingSystem"),
2718         nativeSystem("//ldml/numbers/otherNumberingSystems/native"),
2719         traditional("//ldml/numbers/otherNumberingSystems/traditional"),
2720         finance("//ldml/numbers/otherNumberingSystems/finance");
2721         public final String path;
2722 
2723         private NumberingSystem(String path) {
2724             this.path = path;
2725         }
2726     }
2727 
2728     public UnicodeSet getExemplarsNumeric(NumberingSystem system) {
2729         String numberingSystem = system.path == null ? "latn" : getStringValue(system.path);
2730         if (numberingSystem == null) {
2731             return UnicodeSet.EMPTY;
2732         }
2733         return getExemplarsNumeric(numberingSystem);
2734     }
2735 
2736     public UnicodeSet getExemplarsNumeric(String numberingSystem) {
2737         UnicodeSet result = new UnicodeSet();
2738         SupplementalDataInfo sdi = CLDRConfig.getInstance().getSupplementalDataInfo();
2739         String[] symbolPaths = {
2740             "decimal", "group", "percentSign", "perMille", "plusSign", "minusSign",
2741             // "infinity"
2742         };
2743 
2744         String digits = sdi.getDigits(numberingSystem);
2745         if (digits != null) { // TODO, get other characters, see ticket:8316
2746             result.addAll(digits);
2747         }
2748         for (String path : symbolPaths) {
2749             String fullPath =
2750                     "//ldml/numbers/symbols[@numberSystem=\"" + numberingSystem + "\"]/" + path;
2751             String value = getStringValue(fullPath);
2752             if (value != null) {
2753                 result.add(value);
2754             }
2755         }
2756 
2757         return result;
2758     }
2759 
2760     public String getCurrentMetazone(String zone) {
2761         for (Iterator<String> it2 = iterator(); it2.hasNext(); ) {
2762             String xpath = it2.next();
2763             if (xpath.startsWith(
2764                     "//ldml/dates/timeZoneNames/zone[@type=\"" + zone + "\"]/usesMetazone")) {
2765                 XPathParts parts = XPathParts.getFrozenInstance(xpath);
2766                 if (!parts.containsAttribute("to")) {
2767                     return parts.getAttributeValue(4, "mzone");
2768                 }
2769             }
2770         }
2771         return null;
2772     }
2773 
2774     public boolean isResolved() {
2775         return dataSource.isResolving();
2776     }
2777 
2778     // WARNING: this must go AFTER attributeOrdering is set; otherwise it uses a null comparator!!
2779     /*
2780      * TODO: clarify the warning. There is nothing named "attributeOrdering" in this file.
2781      * This member distinguishedXPath is accessed only by the function getNonDistinguishingAttributes.
2782      */
2783     private static final DistinguishedXPath distinguishedXPath = new DistinguishedXPath();
2784 
2785     public static final String distinguishedXPathStats() {
2786         return DistinguishedXPath.stats();
2787     }
2788 
2789     private static class DistinguishedXPath {
2790 
2791         public static final String stats() {
2792             return "distinguishingMap:"
2793                     + distinguishingMap.size()
2794                     + " "
2795                     + "normalizedPathMap:"
2796                     + normalizedPathMap.size();
2797         }
2798 
2799         private static Map<String, String> distinguishingMap = new ConcurrentHashMap<>();
2800         private static Map<String, String> normalizedPathMap = new ConcurrentHashMap<>();
2801 
2802         static {
2803             distinguishingMap.put("", ""); // seed this to make the code simpler
2804         }
2805 
2806         public static String getDistinguishingXPath(String xpath, String[] normalizedPath) {
2807             // For example, this removes [@xml:space="preserve"] from a path with element
2808             // foreignSpaceReplacement.
2809             //     synchronized (distinguishingMap) {
2810             String result = distinguishingMap.get(xpath);
2811             if (result == null) {
2812                 XPathParts distinguishingParts =
2813                         XPathParts.getFrozenInstance(xpath)
2814                                 .cloneAsThawed(); // not frozen, for removeAttributes
2815 
2816                 DtdType type = distinguishingParts.getDtdData().dtdType;
2817                 Set<String> toRemove = new HashSet<>();
2818 
2819                 // first clean up draft and alt
2820                 String draft = null;
2821                 String alt = null;
2822                 String references = "";
2823                 // note: we only need to clean up items that are NOT on the last element,
2824                 // so we go up to size() - 1.
2825 
2826                 // note: each successive item overrides the previous one. That's intended
2827 
2828                 for (int i = 0; i < distinguishingParts.size() - 1; ++i) {
2829                     if (distinguishingParts.getAttributeCount(i) == 0) {
2830                         continue;
2831                     }
2832                     toRemove.clear();
2833                     Map<String, String> attributes = distinguishingParts.getAttributes(i);
2834                     for (String attribute : attributes.keySet()) {
2835                         if (attribute.equals("draft")) {
2836                             draft = attributes.get(attribute);
2837                             toRemove.add(attribute);
2838                         } else if (attribute.equals("alt")) {
2839                             alt = attributes.get(attribute);
2840                             toRemove.add(attribute);
2841                         } else if (attribute.equals("references")) {
2842                             if (references.length() != 0) references += " ";
2843                             references += attributes.get("references");
2844                             toRemove.add(attribute);
2845                         }
2846                     }
2847                     distinguishingParts.removeAttributes(i, toRemove);
2848                 }
2849                 if (draft != null || alt != null || references.length() != 0) {
2850                     // get the last element that is not ordered.
2851                     int placementIndex = distinguishingParts.size() - 1;
2852                     while (true) {
2853                         String element = distinguishingParts.getElement(placementIndex);
2854                         if (!DtdData.getInstance(type).isOrdered(element)) break;
2855                         --placementIndex;
2856                     }
2857                     if (draft != null) {
2858                         distinguishingParts.putAttributeValue(placementIndex, "draft", draft);
2859                     }
2860                     if (alt != null) {
2861                         distinguishingParts.putAttributeValue(placementIndex, "alt", alt);
2862                     }
2863                     if (references.length() != 0) {
2864                         distinguishingParts.putAttributeValue(
2865                                 placementIndex, "references", references);
2866                     }
2867                     String newXPath = distinguishingParts.toString();
2868                     if (!newXPath.equals(xpath)) {
2869                         normalizedPathMap.put(xpath, newXPath); // store differences
2870                     }
2871                 }
2872 
2873                 // now remove non-distinguishing attributes (if non-inheriting)
2874                 for (int i = 0; i < distinguishingParts.size(); ++i) {
2875                     if (distinguishingParts.getAttributeCount(i) == 0) {
2876                         continue;
2877                     }
2878                     String element = distinguishingParts.getElement(i);
2879                     toRemove.clear();
2880                     for (String attribute : distinguishingParts.getAttributeKeys(i)) {
2881                         if (!isDistinguishing(type, element, attribute)) {
2882                             toRemove.add(attribute);
2883                         }
2884                     }
2885                     distinguishingParts.removeAttributes(i, toRemove);
2886                 }
2887 
2888                 result = distinguishingParts.toString();
2889                 if (result.equals(xpath)) { // don't save the copy if we don't have to.
2890                     result = xpath;
2891                 }
2892                 distinguishingMap.put(xpath, result);
2893             }
2894             if (normalizedPath != null) {
2895                 normalizedPath[0] = normalizedPathMap.get(xpath);
2896                 if (normalizedPath[0] == null) {
2897                     normalizedPath[0] = xpath;
2898                 }
2899             }
2900             return result;
2901         }
2902 
2903         public Map<String, String> getNonDistinguishingAttributes(
2904                 String fullPath, Map<String, String> result, Set<String> skipList) {
2905             if (result == null) {
2906                 result = new LinkedHashMap<>();
2907             } else {
2908                 result.clear();
2909             }
2910             XPathParts distinguishingParts = XPathParts.getFrozenInstance(fullPath);
2911             DtdType type = distinguishingParts.getDtdData().dtdType;
2912             for (int i = 0; i < distinguishingParts.size(); ++i) {
2913                 String element = distinguishingParts.getElement(i);
2914                 Map<String, String> attributes = distinguishingParts.getAttributes(i);
2915                 for (Iterator<String> it = attributes.keySet().iterator(); it.hasNext(); ) {
2916                     String attribute = it.next();
2917                     if (!isDistinguishing(type, element, attribute)
2918                             && !skipList.contains(attribute)) {
2919                         result.put(attribute, attributes.get(attribute));
2920                     }
2921                 }
2922             }
2923             return result;
2924         }
2925     }
2926 
2927     /** Fillin value for {@link CLDRFile#getSourceLocaleID(String, Status)} */
2928     public static class Status {
2929         /**
2930          * XPath where originally found. May be {@link GlossonymConstructor#PSEUDO_PATH} if the
2931          * value was constructed.
2932          *
2933          * @see GlossonymnConstructor
2934          */
2935         public String pathWhereFound;
2936 
2937         @Override
2938         public String toString() {
2939             return pathWhereFound;
2940         }
2941     }
2942 
2943     public static boolean isLOG_PROGRESS() {
2944         return LOG_PROGRESS;
2945     }
2946 
2947     public static void setLOG_PROGRESS(boolean log_progress) {
2948         LOG_PROGRESS = log_progress;
2949     }
2950 
2951     public boolean isEmpty() {
2952         return !dataSource.iterator().hasNext();
2953     }
2954 
2955     public Map<String, String> getNonDistinguishingAttributes(
2956             String fullPath, Map<String, String> result, Set<String> skipList) {
2957         return distinguishedXPath.getNonDistinguishingAttributes(fullPath, result, skipList);
2958     }
2959 
2960     public String getDtdVersion() {
2961         return dataSource.getDtdVersionInfo().toString();
2962     }
2963 
2964     public VersionInfo getDtdVersionInfo() {
2965         VersionInfo result = dataSource.getDtdVersionInfo();
2966         if (result != null || isEmpty()) {
2967             return result;
2968         }
2969         // for old files, pick the version from the @version attribute
2970         String path = dataSource.iterator().next();
2971         String full = getFullXPath(path);
2972         XPathParts parts = XPathParts.getFrozenInstance(full);
2973         String versionString = parts.findFirstAttributeValue("version");
2974         return versionString == null ? null : VersionInfo.getInstance(versionString);
2975     }
2976 
2977     private boolean contains(Map<String, String> a, Map<String, String> b) {
2978         for (Iterator<String> it = b.keySet().iterator(); it.hasNext(); ) {
2979             String key = it.next();
2980             String otherValue = a.get(key);
2981             if (otherValue == null) {
2982                 return false;
2983             }
2984             String value = b.get(key);
2985             if (!otherValue.equals(value)) {
2986                 return false;
2987             }
2988         }
2989         return true;
2990     }
2991 
2992     public String getFullXPath(String path, boolean ignoreOtherLeafAttributes) {
2993         String result = getFullXPath(path);
2994         if (result != null) return result;
2995         XPathParts parts = XPathParts.getFrozenInstance(path);
2996         Map<String, String> lastAttributes = parts.getAttributes(parts.size() - 1);
2997         String base =
2998                 parts.toString(parts.size() - 1)
2999                         + "/"
3000                         + parts.getElement(parts.size() - 1); // trim final element
3001         for (Iterator<String> it = iterator(base); it.hasNext(); ) {
3002             String otherPath = it.next();
3003             XPathParts other = XPathParts.getFrozenInstance(otherPath);
3004             if (other.size() != parts.size()) {
3005                 continue;
3006             }
3007             Map<String, String> lastOtherAttributes = other.getAttributes(other.size() - 1);
3008             if (!contains(lastOtherAttributes, lastAttributes)) {
3009                 continue;
3010             }
3011             if (result == null) {
3012                 result = getFullXPath(otherPath);
3013             } else {
3014                 throw new IllegalArgumentException("Multiple values for path: " + path);
3015             }
3016         }
3017         return result;
3018     }
3019 
3020     /**
3021      * Return true if this item is the "winner" in the survey tool
3022      *
3023      * @param path
3024      * @return
3025      */
3026     public boolean isWinningPath(String path) {
3027         return dataSource.isWinningPath(path);
3028     }
3029 
3030     /**
3031      * Returns the "winning" path, for use in the survey tool tests, out of all those paths that
3032      * only differ by having "alt proposed". The exact meaning may be tweaked over time, but the
3033      * user's choice (vote) has precedence, then any undisputed choice, then the "best" choice of
3034      * the remainders. A value is always returned if there is a valid path, and the returned value
3035      * is always a valid path <i>in the resolved file</i>; that is, it may be valid in the parent,
3036      * or valid because of aliasing.
3037      *
3038      * @param path
3039      * @return path, perhaps with an alt proposed added.
3040      */
3041     public String getWinningPath(String path) {
3042         return dataSource.getWinningPath(path);
3043     }
3044 
3045     /**
3046      * Shortcut for getting the string value for the winning path
3047      *
3048      * @param path
3049      * @return
3050      */
3051     public String getWinningValue(String path) {
3052         final String winningPath = getWinningPath(path);
3053         return winningPath == null ? null : getStringValue(winningPath);
3054     }
3055 
3056     /**
3057      * Shortcut for getting the string value for the winning path. If the winning value is an {@link
3058      * CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned.
3059      *
3060      * @param path
3061      * @return the winning value
3062      */
3063     public String getWinningValueWithBailey(String path) {
3064         final String winningPath = getWinningPath(path);
3065         return winningPath == null ? null : getStringValueWithBailey(winningPath);
3066     }
3067 
3068     /**
3069      * Shortcut for getting the string value for a path. If the string value is an {@link
3070      * CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned.
3071      *
3072      * @param path
3073      * @return the string value
3074      */
3075     public String getStringValueWithBailey(String path) {
3076         return getStringValueWithBailey(path, null, null);
3077     }
3078 
3079     /**
3080      * Shortcut for getting the string value for a path. If the string value is an {@link
3081      * CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned.
3082      *
3083      * @param path the given xpath
3084      * @param pathWhereFound if not null, to be filled in with the path where the value is actually
3085      *     found. May be {@link GlossonymConstructor#PSEUDO_PATH} if constructed.
3086      * @param localeWhereFound if not null, to be filled in with the locale where the value is
3087      *     actually found. May be {@link XMLSource#CODE_FALLBACK_ID} if not in root.
3088      * @return the string value
3089      */
3090     public String getStringValueWithBailey(
3091             String path, Output<String> pathWhereFound, Output<String> localeWhereFound) {
3092         String value = getStringValue(path);
3093         if (CldrUtility.INHERITANCE_MARKER.equals(value)) {
3094             value = getBaileyValue(path, pathWhereFound, localeWhereFound);
3095         } else if (localeWhereFound != null || pathWhereFound != null) {
3096             final Status status = new Status();
3097             final String localeWhereFound2 = getSourceLocaleID(path, status);
3098             if (localeWhereFound != null) {
3099                 localeWhereFound.value = localeWhereFound2;
3100             }
3101             if (pathWhereFound != null) {
3102                 pathWhereFound.value = status.pathWhereFound;
3103             }
3104         }
3105         return value;
3106     }
3107 
3108     /**
3109      * Return the distinguished paths that have the specified value. The pathPrefix and pathMatcher
3110      * can be used to restrict the returned paths to those matching. The pathMatcher can be null
3111      * (equals .*).
3112      *
3113      * @param valueToMatch
3114      * @param pathPrefix
3115      * @return
3116      */
3117     public Set<String> getPathsWithValue(
3118             String valueToMatch, String pathPrefix, Matcher pathMatcher, Set<String> result) {
3119         if (result == null) {
3120             result = new HashSet<>();
3121         }
3122         dataSource.getPathsWithValue(valueToMatch, pathPrefix, result);
3123         if (pathMatcher == null) {
3124             return result;
3125         }
3126         for (Iterator<String> it = result.iterator(); it.hasNext(); ) {
3127             String path = it.next();
3128             if (!pathMatcher.reset(path).matches()) {
3129                 it.remove();
3130             }
3131         }
3132         return result;
3133     }
3134 
3135     /**
3136      * Return the distinguished paths that match the pathPrefix and pathMatcher The pathMatcher can
3137      * be null (equals .*).
3138      */
3139     public Set<String> getPaths(String pathPrefix, Matcher pathMatcher, Set<String> result) {
3140         if (result == null) {
3141             result = new HashSet<>();
3142         }
3143         for (Iterator<String> it = dataSource.iterator(pathPrefix); it.hasNext(); ) {
3144             String path = it.next();
3145             if (pathMatcher != null && !pathMatcher.reset(path).matches()) {
3146                 continue;
3147             }
3148             result.add(path);
3149         }
3150         return result;
3151     }
3152 
3153     public enum WinningChoice {
3154         NORMAL,
3155         WINNING
3156     }
3157 
3158     /**
3159      * Used in TestUser to get the "winning" path. Simple implementation just for testing.
3160      *
3161      * @author markdavis
3162      */
3163     static class WinningComparator implements Comparator<String> {
3164         String user;
3165 
3166         public WinningComparator(String user) {
3167             this.user = user;
3168         }
3169 
3170         /**
3171          * if it contains the user, sort first. Otherwise use normal string sorting. A better
3172          * implementation would look at the number of votes next, and whither there was an approved
3173          * or provisional path.
3174          */
3175         @Override
3176         public int compare(String o1, String o2) {
3177             if (o1.contains(user)) {
3178                 if (!o2.contains(user)) {
3179                     return -1; // if it contains user
3180                 }
3181             } else if (o2.contains(user)) {
3182                 return 1; // if it contains user
3183             }
3184             return o1.compareTo(o2);
3185         }
3186     }
3187 
3188     /**
3189      * This is a test class used to simulate what the survey tool would do.
3190      *
3191      * @author markdavis
3192      */
3193     public static class TestUser extends CLDRFile {
3194 
3195         Map<String, String> userOverrides = new HashMap<>();
3196 
3197         public TestUser(CLDRFile baseFile, String user, boolean resolved) {
3198             super(resolved ? baseFile.dataSource : baseFile.dataSource.getUnresolving());
3199             if (!baseFile.isResolved()) {
3200                 throw new IllegalArgumentException("baseFile must be resolved");
3201             }
3202             Relation<String, String> pathMap =
3203                     Relation.of(
3204                             new HashMap<String, Set<String>>(),
3205                             TreeSet.class,
3206                             new WinningComparator(user));
3207             for (String path : baseFile) {
3208                 String newPath = getNondraftNonaltXPath(path);
3209                 pathMap.put(newPath, path);
3210             }
3211             // now reduce the storage by just getting the winning ones
3212             // so map everything but the first path to the first path
3213             for (String path : pathMap.keySet()) {
3214                 String winner = null;
3215                 for (String rowPath : pathMap.getAll(path)) {
3216                     if (winner == null) {
3217                         winner = rowPath;
3218                         continue;
3219                     }
3220                     userOverrides.put(rowPath, winner);
3221                 }
3222             }
3223         }
3224 
3225         @Override
3226         public String getWinningPath(String path) {
3227             String trial = userOverrides.get(path);
3228             if (trial != null) {
3229                 return trial;
3230             }
3231             return path;
3232         }
3233     }
3234 
3235     /**
3236      * Returns the extra paths, skipping those that are already represented in the locale.
3237      *
3238      * @return
3239      */
3240     public Collection<String> getExtraPaths() {
3241         Set<String> toAddTo = new HashSet<>();
3242         toAddTo.addAll(getRawExtraPaths());
3243         for (String path : this) {
3244             toAddTo.remove(path);
3245         }
3246         return toAddTo;
3247     }
3248 
3249     /**
3250      * Returns the extra paths, skipping those that are already represented in the locale.
3251      *
3252      * @return
3253      */
3254     public Collection<String> getExtraPaths(String prefix, Collection<String> toAddTo) {
3255         for (String item : getRawExtraPaths()) {
3256             if (item.startsWith(prefix)
3257                     && dataSource.getValueAtPath(item) == null) { // don't use getStringValue, since
3258                 // it recurses.
3259                 toAddTo.add(item);
3260             }
3261         }
3262         return toAddTo;
3263     }
3264 
3265     // extraPaths contains the raw extra paths.
3266     // It requires filtering in those cases where we don't want duplicate paths.
3267     /**
3268      * Returns the raw extra paths, irrespective of what paths are already represented in the
3269      * locale.
3270      *
3271      * @return
3272      */
3273     public Set<String> getRawExtraPaths() {
3274         if (extraPaths == null) {
3275             extraPaths =
3276                     ImmutableSet.<String>builder()
3277                             .addAll(getRawExtraPathsPrivate())
3278                             .addAll(CONST_EXTRA_PATHS)
3279                             .build();
3280             if (DEBUG) {
3281                 System.out.println(getLocaleID() + "\textras: " + extraPaths.size());
3282             }
3283         }
3284         return extraPaths;
3285     }
3286 
3287     /**
3288      * Add (possibly over four thousand) extra paths to the given collection. These are paths that
3289      * typically don't have a reasonable fallback value that could be added to root. Some of them
3290      * are common to all locales, and some of them are specific to the given locale, based on
3291      * features like the plural rules for the locale.
3292      *
3293      * <p>The ones that are constant for all locales should go into CONST_EXTRA_PATHS.
3294      *
3295      * @return toAddTo (the collection)
3296      *     <p>Called only by getRawExtraPaths.
3297      *     <p>"Raw" refers to the fact that some of the paths may duplicate paths that are already
3298      *     in this CLDRFile (in the xml and/or votes), in which case they will later get filtered by
3299      *     getExtraPaths (removed from toAddTo) rather than re-added.
3300      *     <p>NOTE: values may be null for some "extra" paths in locales for which no explicit
3301      *     values have been submitted. Both unit tests and Survey Tool client code generate errors
3302      *     or warnings for null value, but allow null value for certain exceptional extra paths. See
3303      *     the functions named extraPathAllowsNullValue in TestPaths.java and in the JavaScript
3304      *     client code. Make sure that updates here are reflected there and vice versa.
3305      *     <p>Reference: https://unicode-org.atlassian.net/browse/CLDR-11238
3306      */
3307     private List<String> getRawExtraPathsPrivate() {
3308         Set<String> toAddTo = new HashSet<>();
3309         SupplementalDataInfo supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo();
3310         // units
3311         PluralInfo plurals = supplementalData.getPlurals(PluralType.cardinal, getLocaleID());
3312         if (plurals == null && DEBUG) {
3313             System.err.println(
3314                     "No "
3315                             + PluralType.cardinal
3316                             + "  plurals for "
3317                             + getLocaleID()
3318                             + " in "
3319                             + supplementalData.getDirectory().getAbsolutePath());
3320         }
3321         Set<Count> pluralCounts = Collections.emptySet();
3322         if (plurals != null) {
3323             pluralCounts = plurals.getAdjustedCounts();
3324             Set<Count> pluralCountsRaw = plurals.getCounts();
3325             if (pluralCountsRaw.size() != 1) {
3326                 // we get all the root paths with count
3327                 addPluralCounts(toAddTo, pluralCounts, pluralCountsRaw, this);
3328             }
3329         }
3330         // dayPeriods
3331         String locale = getLocaleID();
3332         DayPeriodInfo dayPeriods =
3333                 supplementalData.getDayPeriods(DayPeriodInfo.Type.format, locale);
3334         if (dayPeriods != null) {
3335             LinkedHashSet<DayPeriod> items = new LinkedHashSet<>(dayPeriods.getPeriods());
3336             items.add(DayPeriod.am);
3337             items.add(DayPeriod.pm);
3338             for (String context : new String[] {"format", "stand-alone"}) {
3339                 for (String width : new String[] {"narrow", "abbreviated", "wide"}) {
3340                     for (DayPeriod dayPeriod : items) {
3341                         // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="am"]
3342                         toAddTo.add(
3343                                 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/"
3344                                         + "dayPeriodContext[@type=\""
3345                                         + context
3346                                         + "\"]/dayPeriodWidth[@type=\""
3347                                         + width
3348                                         + "\"]/dayPeriod[@type=\""
3349                                         + dayPeriod
3350                                         + "\"]");
3351                     }
3352                 }
3353             }
3354         }
3355 
3356         // metazones
3357         Set<String> zones = supplementalData.getAllMetazones();
3358 
3359         for (String zone : zones) {
3360             final boolean metazoneUsesDST = CheckMetazones.metazoneUsesDST(zone);
3361             for (String width : new String[] {"long", "short"}) {
3362                 for (String type : new String[] {"generic", "standard", "daylight"}) {
3363                     if (metazoneUsesDST || type.equals("standard")) {
3364                         // Only add /standard for non-DST metazones
3365                         final String path =
3366                                 "//ldml/dates/timeZoneNames/metazone[@type=\""
3367                                         + zone
3368                                         + "\"]/"
3369                                         + width
3370                                         + "/"
3371                                         + type;
3372                         toAddTo.add(path);
3373                     }
3374                 }
3375             }
3376         }
3377 
3378         //        // Individual zone overrides
3379         //        final String[] overrides = {
3380         //            "Pacific/Honolulu\"]/short/generic",
3381         //            "Pacific/Honolulu\"]/short/standard",
3382         //            "Pacific/Honolulu\"]/short/daylight",
3383         //            "Europe/Dublin\"]/long/daylight",
3384         //            "Europe/London\"]/long/daylight",
3385         //            "Etc/UTC\"]/long/standard",
3386         //            "Etc/UTC\"]/short/standard"
3387         //        };
3388         //        for (String override : overrides) {
3389         //            toAddTo.add("//ldml/dates/timeZoneNames/zone[@type=\"" + override);
3390         //        }
3391 
3392         // Currencies
3393         Set<String> codes = supplementalData.getBcp47Keys().getAll("cu");
3394         for (String code : codes) {
3395             String currencyCode = code.toUpperCase();
3396             toAddTo.add(
3397                     "//ldml/numbers/currencies/currency[@type=\"" + currencyCode + "\"]/symbol");
3398             toAddTo.add(
3399                     "//ldml/numbers/currencies/currency[@type=\""
3400                             + currencyCode
3401                             + "\"]/displayName");
3402             if (!pluralCounts.isEmpty()) {
3403                 for (Count count : pluralCounts) {
3404                     toAddTo.add(
3405                             "//ldml/numbers/currencies/currency[@type=\""
3406                                     + currencyCode
3407                                     + "\"]/displayName[@count=\""
3408                                     + count.toString()
3409                                     + "\"]");
3410                 }
3411             }
3412         }
3413 
3414         // grammatical info
3415 
3416         GrammarInfo grammarInfo = supplementalData.getGrammarInfo(getLocaleID(), true);
3417         if (grammarInfo != null) {
3418             if (grammarInfo.hasInfo(GrammaticalTarget.nominal)) {
3419                 Collection<String> genders =
3420                         grammarInfo.get(
3421                                 GrammaticalTarget.nominal,
3422                                 GrammaticalFeature.grammaticalGender,
3423                                 GrammaticalScope.units);
3424                 Collection<String> rawCases =
3425                         grammarInfo.get(
3426                                 GrammaticalTarget.nominal,
3427                                 GrammaticalFeature.grammaticalCase,
3428                                 GrammaticalScope.units);
3429                 Collection<String> nomCases = rawCases.isEmpty() ? casesNominativeOnly : rawCases;
3430                 Collection<Count> adjustedPlurals = pluralCounts;
3431                 // There was code here allowing fewer plurals to be used, but is retracted for now
3432                 // (needs more thorough integration in logical groups, etc.)
3433                 // This note is left for 'blame' to find the old code in case we revive that.
3434 
3435                 // TODO use UnitPathType to get paths
3436                 if (!genders.isEmpty()) {
3437                     for (String unit : GrammarInfo.getUnitsToAddGrammar()) {
3438                         toAddTo.add(
3439                                 "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\""
3440                                         + unit
3441                                         + "\"]/gender");
3442                     }
3443                     for (Count plural : adjustedPlurals) {
3444                         for (String gender : genders) {
3445                             for (String case1 : nomCases) {
3446                                 final String grammaticalAttributes =
3447                                         GrammarInfo.getGrammaticalInfoAttributes(
3448                                                 grammarInfo,
3449                                                 UnitPathType.power,
3450                                                 plural.toString(),
3451                                                 gender,
3452                                                 case1);
3453                                 toAddTo.add(
3454                                         "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1"
3455                                                 + grammaticalAttributes);
3456                                 toAddTo.add(
3457                                         "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power3\"]/compoundUnitPattern1"
3458                                                 + grammaticalAttributes);
3459                             }
3460                         }
3461                     }
3462                     //             <genderMinimalPairs gender="masculine">Der {0} ist
3463                     // …</genderMinimalPairs>
3464                     for (String gender : genders) {
3465                         toAddTo.add(
3466                                 "//ldml/numbers/minimalPairs/genderMinimalPairs[@gender=\""
3467                                         + gender
3468                                         + "\"]");
3469                     }
3470                 }
3471                 if (!rawCases.isEmpty()) {
3472                     for (String case1 : rawCases) {
3473                         //          <caseMinimalPairs case="nominative">{0} kostet
3474                         // €3,50.</caseMinimalPairs>
3475                         toAddTo.add(
3476                                 "//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\""
3477                                         + case1
3478                                         + "\"]");
3479 
3480                         for (Count plural : adjustedPlurals) {
3481                             for (String unit : GrammarInfo.getUnitsToAddGrammar()) {
3482                                 toAddTo.add(
3483                                         "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\""
3484                                                 + unit
3485                                                 + "\"]/unitPattern"
3486                                                 + GrammarInfo.getGrammaticalInfoAttributes(
3487                                                         grammarInfo,
3488                                                         UnitPathType.unit,
3489                                                         plural.toString(),
3490                                                         null,
3491                                                         case1));
3492                             }
3493                         }
3494                     }
3495                 }
3496             }
3497         }
3498         return toAddTo.stream().map(String::intern).collect(Collectors.toList());
3499     }
3500 
3501     private void addPluralCounts(
3502             Collection<String> toAddTo,
3503             final Set<Count> pluralCounts,
3504             final Set<Count> pluralCountsRaw,
3505             Iterable<String> file) {
3506         for (String path : file) {
3507             String countAttr = "[@count=\"other\"]";
3508             int countPos = path.indexOf(countAttr);
3509             if (countPos < 0) {
3510                 continue;
3511             }
3512             Set<Count> pluralCountsNeeded =
3513                     path.startsWith("//ldml/numbers/minimalPairs") ? pluralCountsRaw : pluralCounts;
3514             if (pluralCountsNeeded.size() > 1) {
3515                 String start = path.substring(0, countPos) + "[@count=\"";
3516                 String end = "\"]" + path.substring(countPos + countAttr.length());
3517                 for (Count count : pluralCounts) {
3518                     if (count == Count.other) {
3519                         continue;
3520                     }
3521                     toAddTo.add(start + count + end);
3522                 }
3523             }
3524         }
3525     }
3526 
3527     /**
3528      * Get the path with the given count, case, or gender, with fallback. The fallback acts like an
3529      * alias in root.
3530      *
3531      * <p>Count:
3532      *
3533      * <p>It acts like there is an alias in root from count=n to count=one, then for currency
3534      * display names from count=one to no count <br>
3535      * For unitPatterns, falls back to Count.one. <br>
3536      * For others, falls back to Count.one, then no count.
3537      *
3538      * <p>Case
3539      *
3540      * <p>The fallback is to no case, which = nominative.
3541      *
3542      * <p>Case
3543      *
3544      * <p>The fallback is to no case, which = nominative.
3545      *
3546      * @param xpath
3547      * @param count Count may be null. Returns null if nothing is found.
3548      * @param winning TODO
3549      * @return
3550      */
3551     public String getCountPathWithFallback(String xpath, Count count, boolean winning) {
3552         String result;
3553         XPathParts parts =
3554                 XPathParts.getFrozenInstance(xpath)
3555                         .cloneAsThawed(); // not frozen, addAttribute in getCountPathWithFallback2
3556 
3557         // In theory we should do all combinations of gender, case, count (and eventually
3558         // definiteness), but for simplicity
3559         // we just successively try "zeroing" each one in a set order.
3560         // tryDefault modifies the parts in question
3561         Output<String> newPath = new Output<>();
3562         if (tryDefault(parts, "gender", null, newPath)) {
3563             return newPath.value;
3564         }
3565 
3566         if (tryDefault(parts, "case", null, newPath)) {
3567             return newPath.value;
3568         }
3569 
3570         boolean isDisplayName = parts.contains("displayName");
3571 
3572         String actualCount = parts.getAttributeValue(-1, "count");
3573         if (actualCount != null) {
3574             if (CldrUtility.DIGITS.containsAll(actualCount)) {
3575                 try {
3576                     int item = Integer.parseInt(actualCount);
3577                     String locale = getLocaleID();
3578                     SupplementalDataInfo sdi = CLDRConfig.getInstance().getSupplementalDataInfo();
3579                     PluralRules rules =
3580                             sdi.getPluralRules(
3581                                     new ULocale(locale), PluralRules.PluralType.CARDINAL);
3582                     String keyword = rules.select(item);
3583                     Count itemCount = Count.valueOf(keyword);
3584                     result = getCountPathWithFallback2(parts, xpath, itemCount, winning);
3585                     if (result != null && isNotRoot(result)) {
3586                         return result;
3587                     }
3588                 } catch (NumberFormatException e) {
3589                 }
3590             }
3591 
3592             // try the given count first
3593             result = getCountPathWithFallback2(parts, xpath, count, winning);
3594             if (result != null && isNotRoot(result)) {
3595                 return result;
3596             }
3597             // now try fallback
3598             if (count != Count.other) {
3599                 result = getCountPathWithFallback2(parts, xpath, Count.other, winning);
3600                 if (result != null && isNotRoot(result)) {
3601                     return result;
3602                 }
3603             }
3604             // now try deletion (for currency)
3605             if (isDisplayName) {
3606                 result = getCountPathWithFallback2(parts, xpath, null, winning);
3607             }
3608             return result;
3609         }
3610         return null;
3611     }
3612 
3613     /**
3614      * Modify the parts by setting the attribute in question to the default value (typically null to
3615      * clear). If there is a value for that path, use it.
3616      */
3617     public boolean tryDefault(
3618             XPathParts parts, String attribute, String defaultValue, Output<String> newPath) {
3619         String oldValue = parts.getAttributeValue(-1, attribute);
3620         if (oldValue != null) {
3621             parts.setAttribute(-1, attribute, null);
3622             newPath.value = parts.toString();
3623             if (dataSource.getValueAtPath(newPath.value) != null) {
3624                 return true;
3625             }
3626         }
3627         return false;
3628     }
3629 
3630     private String getCountPathWithFallback2(
3631             XPathParts parts, String xpathWithNoCount, Count count, boolean winning) {
3632         parts.addAttribute("count", count == null ? null : count.toString());
3633         String newPath = parts.toString();
3634         if (!newPath.equals(xpathWithNoCount)) {
3635             if (winning) {
3636                 String temp = getWinningPath(newPath);
3637                 if (temp != null) {
3638                     newPath = temp;
3639                 }
3640             }
3641             if (dataSource.getValueAtPath(newPath) != null) {
3642                 return newPath;
3643             }
3644             // return getWinningPath(newPath);
3645         }
3646         return null;
3647     }
3648 
3649     /**
3650      * Returns a value to be used for "filling in" a "Change" value in the survey tool. Currently
3651      * returns the following.
3652      *
3653      * <ul>
3654      *   <li>The "winning" value (if not inherited). Example: if "Donnerstag" has the most votes for
3655      *       'thursday', then clicking on the empty field will fill in "Donnerstag"
3656      *   <li>The singular form. Example: if the value for 'hour' is "heure", then clicking on the
3657      *       entry field for 'hours' will insert "heure".
3658      *   <li>The parent's value. Example: if I'm in [de_CH] and there are no proposals for
3659      *       'thursday', then clicking on the empty field will fill in "Donnerstag" from [de].
3660      *   <li>Otherwise don't fill in anything, and return null.
3661      * </ul>
3662      *
3663      * @return
3664      */
3665     public String getFillInValue(String distinguishedPath) {
3666         String winningPath = getWinningPath(distinguishedPath);
3667         if (isNotRoot(winningPath)) {
3668             return getStringValue(winningPath);
3669         }
3670         String fallbackPath = getFallbackPath(winningPath, true, true);
3671         if (fallbackPath != null) {
3672             String value = getWinningValue(fallbackPath);
3673             if (value != null) {
3674                 return value;
3675             }
3676         }
3677         return getStringValue(winningPath);
3678     }
3679 
3680     /**
3681      * returns true if the source of the path exists, and is neither root nor code-fallback
3682      *
3683      * @param distinguishedPath
3684      * @return
3685      */
3686     public boolean isNotRoot(String distinguishedPath) {
3687         String source = getSourceLocaleID(distinguishedPath, null);
3688         return source != null
3689                 && !source.equals("root")
3690                 && !source.equals(XMLSource.CODE_FALLBACK_ID);
3691     }
3692 
3693     public boolean isAliasedAtTopLevel() {
3694         return iterator("//ldml/alias").hasNext();
3695     }
3696 
3697     public static Comparator<String> getComparator(DtdType dtdType) {
3698         if (dtdType == null) {
3699             return ldmlComparator;
3700         }
3701         switch (dtdType) {
3702             case ldml:
3703             case ldmlICU:
3704                 return ldmlComparator;
3705             default:
3706                 return DtdData.getInstance(dtdType).getDtdComparator(null);
3707         }
3708     }
3709 
3710     public Comparator<String> getComparator() {
3711         return getComparator(dtdType);
3712     }
3713 
3714     public DtdType getDtdType() {
3715         return dtdType != null ? dtdType : dataSource.getDtdType();
3716     }
3717 
3718     public DtdData getDtdData() {
3719         return dtdData != null ? dtdData : DtdData.getInstance(getDtdType());
3720     }
3721 
3722     public static Comparator<String> getPathComparator(String path) {
3723         DtdType fileDtdType = DtdType.fromPath(path);
3724         return getComparator(fileDtdType);
3725     }
3726 
3727     public static MapComparator<String> getAttributeOrdering() {
3728         return DtdData.getInstance(DtdType.ldmlICU).getAttributeComparator();
3729     }
3730 
3731     public CLDRFile getUnresolved() {
3732         if (!isResolved()) {
3733             return this;
3734         }
3735         XMLSource source = dataSource.getUnresolving();
3736         return new CLDRFile(source);
3737     }
3738 
3739     public static Comparator<String> getAttributeValueComparator(String element, String attribute) {
3740         return DtdData.getAttributeValueComparator(DtdType.ldml, element, attribute);
3741     }
3742 
3743     public void setDtdType(DtdType dtdType) {
3744         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
3745         this.dtdType = dtdType;
3746     }
3747 
3748     public void disableCaching() {
3749         dataSource.disableCaching();
3750     }
3751 
3752     /**
3753      * Get a constructed value for the given path, if it is a path for which values can be
3754      * constructed
3755      *
3756      * @param xpath the given path, such as
3757      *     //ldml/localeDisplayNames/languages/language[@type="zh_Hans"]
3758      * @return the constructed value, or null if this path doesn't have a constructed value
3759      */
3760     public String getConstructedValue(String xpath) {
3761         if (isResolved() && GlossonymConstructor.pathIsEligible(xpath)) {
3762             return new GlossonymConstructor(this).getValue(xpath);
3763         }
3764         return null;
3765     }
3766 
3767     /**
3768      * Get the string value for the given path in this locale, without resolving to any other path
3769      * or locale.
3770      *
3771      * @param xpath the given path
3772      * @return the string value, unresolved
3773      */
3774     private String getStringValueUnresolved(String xpath) {
3775         CLDRFile sourceFileUnresolved = this.getUnresolved();
3776         return sourceFileUnresolved.getStringValue(xpath);
3777     }
3778 
3779     /**
3780      * Create an overriding LocaleStringProvider for testing and example generation
3781      *
3782      * @param pathAndValueOverrides
3783      * @return
3784      */
3785     public LocaleStringProvider makeOverridingStringProvider(
3786             Map<String, String> pathAndValueOverrides) {
3787         return new OverridingStringProvider(pathAndValueOverrides);
3788     }
3789 
3790     public class OverridingStringProvider implements LocaleStringProvider {
3791         private final Map<String, String> pathAndValueOverrides;
3792 
3793         public OverridingStringProvider(Map<String, String> pathAndValueOverrides) {
3794             this.pathAndValueOverrides = pathAndValueOverrides;
3795         }
3796 
3797         @Override
3798         public String getStringValue(String xpath) {
3799             String value = pathAndValueOverrides.get(xpath);
3800             return value != null ? value : CLDRFile.this.getStringValue(xpath);
3801         }
3802 
3803         @Override
3804         public String getLocaleID() {
3805             return CLDRFile.this.getLocaleID();
3806         }
3807 
3808         @Override
3809         public String getSourceLocaleID(String xpath, Status status) {
3810             if (pathAndValueOverrides.containsKey(xpath)) {
3811                 if (status != null) {
3812                     status.pathWhereFound = xpath;
3813                 }
3814                 return getLocaleID() + "-override";
3815             }
3816             return CLDRFile.this.getSourceLocaleID(xpath, status);
3817         }
3818     }
3819 
3820     public String getKeyName(String key) {
3821         String result = getStringValue("//ldml/localeDisplayNames/keys/key[@type=\"" + key + "\"]");
3822         if (result == null) {
3823             Relation<R2<String, String>, String> toAliases =
3824                     SupplementalDataInfo.getInstance().getBcp47Aliases();
3825             Set<String> aliases = toAliases.get(Row.of(key, ""));
3826             if (aliases != null) {
3827                 for (String alias : aliases) {
3828                     result =
3829                             getStringValue(
3830                                     "//ldml/localeDisplayNames/keys/key[@type=\"" + alias + "\"]");
3831                     if (result != null) {
3832                         break;
3833                     }
3834                 }
3835             }
3836         }
3837         return result;
3838     }
3839 
3840     public String getKeyValueName(String key, String value) {
3841         String result =
3842                 getStringValue(
3843                         "//ldml/localeDisplayNames/types/type[@key=\""
3844                                 + key
3845                                 + "\"][@type=\""
3846                                 + value
3847                                 + "\"]");
3848         if (result == null) {
3849             Relation<R2<String, String>, String> toAliases =
3850                     SupplementalDataInfo.getInstance().getBcp47Aliases();
3851             Set<String> keyAliases = toAliases.get(Row.of(key, ""));
3852             Set<String> valueAliases = toAliases.get(Row.of(key, value));
3853             if (keyAliases != null || valueAliases != null) {
3854                 if (keyAliases == null) {
3855                     keyAliases = Collections.singleton(key);
3856                 }
3857                 if (valueAliases == null) {
3858                     valueAliases = Collections.singleton(value);
3859                 }
3860                 for (String keyAlias : keyAliases) {
3861                     for (String valueAlias : valueAliases) {
3862                         result =
3863                                 getStringValue(
3864                                         "//ldml/localeDisplayNames/types/type[@key=\""
3865                                                 + keyAlias
3866                                                 + "\"][@type=\""
3867                                                 + valueAlias
3868                                                 + "\"]");
3869                         if (result != null) {
3870                             break;
3871                         }
3872                     }
3873                 }
3874             }
3875         }
3876         return result;
3877     }
3878 
3879     /*
3880      *******************************************************************************************
3881      * TODO: move the code below here -- that is, the many (currently ten as of 2022-06-01)
3882      * versions of getName and their subroutines and data -- to a new class in a separate file,
3883      * and enable tracking similar to existing "pathWhereFound/localeWhereFound" but more general.
3884      *
3885      * Reference: https://unicode-org.atlassian.net/browse/CLDR-15830
3886      *******************************************************************************************
3887      */
3888 
3889     static final Joiner JOIN_HYPHEN = Joiner.on('-');
3890     static final Joiner JOIN_UNDERBAR = Joiner.on('_');
3891 
3892     /** Utility for getting a name, given a type and code. */
3893     public String getName(String type, String code) {
3894         return getName(typeNameToCode(type), code);
3895     }
3896 
3897     public String getName(int type, String code) {
3898         return getName(type, code, null, null);
3899     }
3900 
3901     public String getName(int type, String code, Set<String> paths) {
3902         return getName(type, code, null, paths);
3903     }
3904 
3905     public String getName(int type, String code, Transform<String, String> altPicker) {
3906         return getName(type, code, altPicker, null);
3907     }
3908 
3909     /**
3910      * Returns the name of the given bcp47 identifier. Note that extensions must be specified using
3911      * the old "\@key=type" syntax.
3912      *
3913      * @param localeOrTZID
3914      * @return
3915      */
3916     public synchronized String getName(String localeOrTZID) {
3917         return getName(localeOrTZID, false);
3918     }
3919 
3920     public String getName(
3921             LanguageTagParser lparser,
3922             boolean onlyConstructCompound,
3923             Transform<String, String> altPicker) {
3924         return getName(lparser, onlyConstructCompound, altPicker, null);
3925     }
3926 
3927     /**
3928      * @param paths if non-null, will contain contributory paths on return
3929      */
3930     public String getName(
3931             LanguageTagParser lparser,
3932             boolean onlyConstructCompound,
3933             Transform<String, String> altPicker,
3934             Set<String> paths) {
3935         return getName(
3936                 lparser,
3937                 onlyConstructCompound,
3938                 altPicker,
3939                 getWinningValueWithBailey(GETNAME_LOCALE_KEY_TYPE_PATTERN),
3940                 getWinningValueWithBailey(GETNAME_LOCALE_PATTERN),
3941                 getWinningValueWithBailey(GETNAME_LOCALE_SEPARATOR),
3942                 paths);
3943     }
3944 
3945     public synchronized String getName(
3946             String localeOrTZID,
3947             boolean onlyConstructCompound,
3948             String localeKeyTypePattern,
3949             String localePattern,
3950             String localeSeparator) {
3951         return getName(
3952                 localeOrTZID,
3953                 onlyConstructCompound,
3954                 localeKeyTypePattern,
3955                 localePattern,
3956                 localeSeparator,
3957                 null,
3958                 null);
3959     }
3960 
3961     /**
3962      * Returns the name of the given bcp47 identifier. Note that extensions must be specified using
3963      * the old "\@key=type" syntax.
3964      *
3965      * @param localeOrTZID the locale or timezone ID
3966      * @param onlyConstructCompound
3967      * @return
3968      */
3969     public synchronized String getName(String localeOrTZID, boolean onlyConstructCompound) {
3970         return getName(localeOrTZID, onlyConstructCompound, null);
3971     }
3972 
3973     /**
3974      * Returns the name of the given bcp47 identifier. Note that extensions must be specified using
3975      * the old "\@key=type" syntax.
3976      *
3977      * @param localeOrTZID the locale or timezone ID
3978      * @param onlyConstructCompound if true, returns "English (United Kingdom)" instead of "British
3979      *     English"
3980      * @param altPicker Used to select particular alts. For example, SHORT_ALTS can be used to get
3981      *     "English (U.K.)" instead of "English (United Kingdom)"
3982      * @return
3983      */
3984     public synchronized String getName(
3985             String localeOrTZID,
3986             boolean onlyConstructCompound,
3987             Transform<String, String> altPicker) {
3988         return getName(localeOrTZID, onlyConstructCompound, altPicker, null);
3989     }
3990 
3991     /**
3992      * Returns the name of the given bcp47 identifier. Note that extensions must be specified using
3993      * the old "\@key=type" syntax.
3994      *
3995      * @param localeOrTZID the locale or timezone ID
3996      * @param onlyConstructCompound if true, returns "English (United Kingdom)" instead of "British
3997      *     English"
3998      * @param altPicker Used to select particular alts. For example, SHORT_ALTS can be used to get
3999      *     "English (U.K.)" instead of "English (United Kingdom)"
4000      * @return
4001      */
4002     public synchronized String getName(
4003             String localeOrTZID,
4004             boolean onlyConstructCompound,
4005             Transform<String, String> altPicker,
4006             Set<String> paths) {
4007         return getName(
4008                 localeOrTZID,
4009                 onlyConstructCompound,
4010                 getWinningValueWithBailey(GETNAME_LOCALE_KEY_TYPE_PATTERN),
4011                 getWinningValueWithBailey(GETNAME_LOCALE_PATTERN),
4012                 getWinningValueWithBailey(GETNAME_LOCALE_SEPARATOR),
4013                 altPicker,
4014                 paths);
4015     }
4016 
4017     /**
4018      * Returns the name of the given bcp47 identifier. Note that extensions must be specified using
4019      * the old "\@key=type" syntax. Only used by ExampleGenerator.
4020      *
4021      * @param localeOrTZID the locale or timezone ID
4022      * @param onlyConstructCompound
4023      * @param localeKeyTypePattern the pattern used to format key-type pairs
4024      * @param localePattern the pattern used to format primary/secondary subtags
4025      * @param localeSeparator the list separator for secondary subtags
4026      * @param paths if non-null, fillin with contributory paths
4027      * @return
4028      */
4029     public synchronized String getName(
4030             String localeOrTZID,
4031             boolean onlyConstructCompound,
4032             String localeKeyTypePattern,
4033             String localePattern,
4034             String localeSeparator,
4035             Transform<String, String> altPicker,
4036             Set<String> paths) {
4037         // Hack for seed
4038         if (localePattern == null) {
4039             localePattern = "{0} ({1})";
4040         }
4041         boolean isCompound = localeOrTZID.contains("_");
4042         String name =
4043                 isCompound && onlyConstructCompound
4044                         ? null
4045                         : getName(LANGUAGE_NAME, localeOrTZID, altPicker, paths);
4046 
4047         // TODO - handle arbitrary combinations
4048         if (name != null && !name.contains("_") && !name.contains("-")) {
4049             name = replaceBracketsForName(name);
4050             return name;
4051         }
4052         LanguageTagParser lparser = new LanguageTagParser().set(localeOrTZID);
4053         return getName(
4054                 lparser,
4055                 onlyConstructCompound,
4056                 altPicker,
4057                 localeKeyTypePattern,
4058                 localePattern,
4059                 localeSeparator,
4060                 paths);
4061     }
4062 
4063     public String getName(
4064             LanguageTagParser lparser,
4065             boolean onlyConstructCompound,
4066             Transform<String, String> altPicker,
4067             String localeKeyTypePattern,
4068             String localePattern,
4069             String localeSeparator,
4070             Set<String> paths) {
4071         String name;
4072         String original = null;
4073 
4074         // we need to check for prefixes, for lang+script or lang+country
4075         boolean haveScript = false;
4076         boolean haveRegion = false;
4077         // try lang+script
4078         if (onlyConstructCompound) {
4079             name = getName(LANGUAGE_NAME, original = lparser.getLanguage(), altPicker, paths);
4080             if (name == null) name = original;
4081         } else {
4082             String x = lparser.toString(LanguageTagParser.LANGUAGE_SCRIPT_REGION);
4083             name = getName(LANGUAGE_NAME, x, altPicker, paths);
4084             if (name != null) {
4085                 haveScript = haveRegion = true;
4086             } else {
4087                 name =
4088                         getName(
4089                                 LANGUAGE_NAME,
4090                                 lparser.toString(LanguageTagParser.LANGUAGE_SCRIPT),
4091                                 altPicker,
4092                                 paths);
4093                 if (name != null) {
4094                     haveScript = true;
4095                 } else {
4096                     name =
4097                             getName(
4098                                     LANGUAGE_NAME,
4099                                     lparser.toString(LanguageTagParser.LANGUAGE_REGION),
4100                                     altPicker,
4101                                     paths);
4102                     if (name != null) {
4103                         haveRegion = true;
4104                     } else {
4105                         name =
4106                                 getName(
4107                                         LANGUAGE_NAME,
4108                                         original = lparser.getLanguage(),
4109                                         altPicker,
4110                                         paths);
4111                         if (name == null) {
4112                             name = original;
4113                         }
4114                     }
4115                 }
4116             }
4117         }
4118         name = replaceBracketsForName(name);
4119         String extras = "";
4120         if (!haveScript) {
4121             extras =
4122                     addDisplayName(
4123                             lparser.getScript(),
4124                             SCRIPT_NAME,
4125                             localeSeparator,
4126                             extras,
4127                             altPicker,
4128                             paths);
4129         }
4130         if (!haveRegion) {
4131             extras =
4132                     addDisplayName(
4133                             lparser.getRegion(),
4134                             TERRITORY_NAME,
4135                             localeSeparator,
4136                             extras,
4137                             altPicker,
4138                             paths);
4139         }
4140         List<String> variants = lparser.getVariants();
4141         for (String orig : variants) {
4142             extras = addDisplayName(orig, VARIANT_NAME, localeSeparator, extras, altPicker, paths);
4143         }
4144 
4145         // Look for key-type pairs.
4146         main:
4147         for (Map.Entry<String, List<String>> extension :
4148                 lparser.getLocaleExtensionsDetailed().entrySet()) {
4149             String key = extension.getKey();
4150             if (key.equals("h0")) {
4151                 continue;
4152             }
4153             List<String> keyValue = extension.getValue();
4154             String oldFormatType =
4155                     (key.equals("ca") ? JOIN_HYPHEN : JOIN_UNDERBAR)
4156                             .join(keyValue); // default value
4157             // Check if key/type pairs exist in the CLDRFile first.
4158             String value = getKeyValueName(key, oldFormatType);
4159             if (value != null) {
4160                 value = replaceBracketsForName(value);
4161             } else {
4162                 // if we fail, then we construct from the key name and the value
4163                 String kname = getKeyName(key);
4164                 if (kname == null) {
4165                     kname = key; // should not happen, but just in case
4166                 }
4167                 switch (key) {
4168                     case "t":
4169                         List<String> hybrid = lparser.getLocaleExtensionsDetailed().get("h0");
4170                         if (hybrid != null) {
4171                             kname = getKeyValueName("h0", JOIN_UNDERBAR.join(hybrid));
4172                         }
4173                         oldFormatType = getName(oldFormatType);
4174                         break;
4175                     case "h0":
4176                         continue main;
4177                     case "cu":
4178                         oldFormatType =
4179                                 getName(
4180                                         CURRENCY_SYMBOL,
4181                                         oldFormatType.toUpperCase(Locale.ROOT),
4182                                         paths);
4183                         break;
4184                     case "tz":
4185                         if (paths != null) {
4186                             throw new IllegalArgumentException(
4187                                     "Error: getName(…) with paths doesn't handle timezones.");
4188                         }
4189                         oldFormatType =
4190                                 getTZName(
4191                                         oldFormatType, "VVVV"); // TODO: paths not handled here, yet
4192                         break;
4193                     case "kr":
4194                         oldFormatType = getReorderName(localeSeparator, keyValue, paths);
4195                         break;
4196                     case "rg":
4197                     case "sd":
4198                         oldFormatType = getName(SUBDIVISION_NAME, oldFormatType, paths);
4199                         break;
4200                     default:
4201                         oldFormatType = JOIN_HYPHEN.join(keyValue);
4202                 }
4203                 value =
4204                         MessageFormat.format(
4205                                 localeKeyTypePattern, new Object[] {kname, oldFormatType});
4206                 if (paths != null) {
4207                     paths.add(GETNAME_LOCALE_KEY_TYPE_PATTERN);
4208                 }
4209                 value = replaceBracketsForName(value);
4210             }
4211             if (paths != null && !extras.isEmpty()) {
4212                 paths.add(GETNAME_LOCALE_SEPARATOR);
4213             }
4214             extras =
4215                     extras.isEmpty()
4216                             ? value
4217                             : MessageFormat.format(localeSeparator, new Object[] {extras, value});
4218         }
4219         // now handle stray extensions
4220         for (Map.Entry<String, List<String>> extension :
4221                 lparser.getExtensionsDetailed().entrySet()) {
4222             String value =
4223                     MessageFormat.format(
4224                             localeKeyTypePattern,
4225                             new Object[] {
4226                                 extension.getKey(), JOIN_HYPHEN.join(extension.getValue())
4227                             });
4228             if (paths != null) {
4229                 paths.add(GETNAME_LOCALE_KEY_TYPE_PATTERN);
4230             }
4231             extras =
4232                     extras.isEmpty()
4233                             ? value
4234                             : MessageFormat.format(localeSeparator, new Object[] {extras, value});
4235         }
4236         // fix this -- shouldn't be hardcoded!
4237         if (extras.length() == 0) {
4238             return name;
4239         }
4240         if (paths != null) {
4241             paths.add(GETNAME_LOCALE_PATTERN);
4242         }
4243         return MessageFormat.format(localePattern, new Object[] {name, extras});
4244     }
4245 
4246     private static final String replaceBracketsForName(String value) {
4247         value = value.replace('(', '[').replace(')', ']').replace('(', '[').replace(')', ']');
4248         return value;
4249     }
4250 
4251     /**
4252      * Utility for getting the name, given a code.
4253      *
4254      * @param type
4255      * @param code
4256      * @param codeToAlt - if not null, is called on the code. If the result is not null, then that
4257      *     is used for an alt value. If the alt path has a value it is used, otherwise the normal
4258      *     one is used. For example, the transform could return "short" for PS or HK or MO, but not
4259      *     US or GB.
4260      * @param paths if non-null, will have contributory paths on return
4261      * @return
4262      */
4263     public String getName(
4264             int type, String code, Transform<String, String> codeToAlt, Set<String> paths) {
4265         String path = getKey(type, code);
4266         String result = null;
4267         if (codeToAlt != null) {
4268             String alt = codeToAlt.transform(code);
4269             if (alt != null) {
4270                 String altPath = path + "[@alt=\"" + alt + "\"]";
4271                 result = getStringValueWithBaileyNotConstructed(altPath);
4272                 if (paths != null && result != null) {
4273                     paths.add(altPath);
4274                 }
4275             }
4276         }
4277         if (result == null) {
4278             result = getStringValueWithBaileyNotConstructed(path);
4279             if (paths != null && result != null) {
4280                 paths.add(path);
4281             }
4282         }
4283         if (getLocaleID().equals("en")) {
4284             CLDRFile.Status status = new CLDRFile.Status();
4285             String sourceLocale = getSourceLocaleID(path, status);
4286             if (result == null || !sourceLocale.equals("en")) {
4287                 if (type == LANGUAGE_NAME) {
4288                     Set<String> set = Iso639Data.getNames(code);
4289                     if (set != null) {
4290                         return set.iterator().next();
4291                     }
4292                     Map<String, Map<String, String>> map =
4293                             StandardCodes.getLStreg().get("language");
4294                     Map<String, String> info = map.get(code);
4295                     if (info != null) {
4296                         result = info.get("Description");
4297                     }
4298                 } else if (type == TERRITORY_NAME) {
4299                     result = getLstrFallback("region", code, paths);
4300                 } else if (type == SCRIPT_NAME) {
4301                     result = getLstrFallback("script", code, paths);
4302                 }
4303             }
4304         }
4305         return result;
4306     }
4307 
4308     static final Pattern CLEAN_DESCRIPTION = Pattern.compile("([^\\(\\[]*)[\\(\\[].*");
4309     static final Splitter DESCRIPTION_SEP = Splitter.on('▪');
4310 
4311     private String getLstrFallback(String codeType, String code, Set<String> paths) {
4312         Map<String, String> info = StandardCodes.getLStreg().get(codeType).get(code);
4313         if (info != null) {
4314             String temp = info.get("Description");
4315             if (!temp.equalsIgnoreCase("Private use")) {
4316                 List<String> temp2 = DESCRIPTION_SEP.splitToList(temp);
4317                 temp = temp2.get(0);
4318                 final Matcher matcher = CLEAN_DESCRIPTION.matcher(temp);
4319                 if (matcher.lookingAt()) {
4320                     temp = matcher.group(1).trim();
4321                 }
4322                 return temp;
4323             }
4324         }
4325         return null;
4326     }
4327 
4328     /**
4329      * Gets timezone name. Not optimized.
4330      *
4331      * @param tzcode
4332      * @return
4333      */
4334     private String getTZName(String tzcode, String format) {
4335         String longid = getLongTzid(tzcode);
4336         if (tzcode.length() == 4 && !tzcode.equals("gaza")) {
4337             return longid;
4338         }
4339         TimezoneFormatter tzf = new TimezoneFormatter(this);
4340         return tzf.getFormattedZone(longid, format, 0);
4341     }
4342 
4343     private String getReorderName(
4344             String localeSeparator, List<String> keyValues, Set<String> paths) {
4345         String result = null;
4346         for (String value : keyValues) {
4347             String name =
4348                     getName(
4349                             SCRIPT_NAME,
4350                             Character.toUpperCase(value.charAt(0)) + value.substring(1),
4351                             paths);
4352             if (name == null) {
4353                 name = getKeyValueName("kr", value);
4354                 if (name == null) {
4355                     name = value;
4356                 }
4357             }
4358             result =
4359                     result == null
4360                             ? name
4361                             : MessageFormat.format(localeSeparator, new Object[] {result, name});
4362         }
4363         return result;
4364     }
4365 
4366     /**
4367      * Adds the display name for a subtag to a string.
4368      *
4369      * @param subtag the subtag
4370      * @param type the type of the subtag
4371      * @param separatorPattern the pattern to be used for separating display names in the resultant
4372      *     string
4373      * @param extras the string to be added to
4374      * @return the modified display name string
4375      */
4376     private String addDisplayName(
4377             String subtag,
4378             int type,
4379             String separatorPattern,
4380             String extras,
4381             Transform<String, String> altPicker,
4382             Set<String> paths) {
4383         if (subtag.length() == 0) {
4384             return extras;
4385         }
4386         String sname = getName(type, subtag, altPicker, paths);
4387         if (sname == null) {
4388             sname = subtag;
4389         }
4390         sname = replaceBracketsForName(sname);
4391 
4392         if (extras.length() == 0) {
4393             extras += sname;
4394         } else {
4395             extras = MessageFormat.format(separatorPattern, new Object[] {extras, sname});
4396         }
4397         return extras;
4398     }
4399 
4400     /**
4401      * Like getStringValueWithBailey, but reject constructed values, to prevent circularity problems
4402      * with getName
4403      *
4404      * <p>Since GlossonymConstructor uses getName to CREATE constructed values, circularity problems
4405      * would occur if getName in turn used GlossonymConstructor to get constructed Bailey values.
4406      * Note that getStringValueWithBailey only returns a constructed value if the value would
4407      * otherwise be "bogus", and getName has no use for bogus values, so there is no harm in
4408      * returning null rather than code-fallback or other bogus values.
4409      *
4410      * @param path the given xpath
4411      * @return the string value, or null
4412      */
4413     private String getStringValueWithBaileyNotConstructed(String path) {
4414         Output<String> pathWhereFound = new Output<>();
4415         final String value = getStringValueWithBailey(path, pathWhereFound, null);
4416         if (value == null || GlossonymConstructor.PSEUDO_PATH.equals(pathWhereFound.toString())) {
4417             return null;
4418         }
4419         return value;
4420     }
4421 
4422     /**
4423      * A set of paths to be added to getRawExtraPaths(). These are constant across locales, and
4424      * don't have good fallback values in root. NOTE: if this is changed, you'll need to modify
4425      * TestPaths.extraPathAllowsNullValue
4426      */
4427     static final Set<String> CONST_EXTRA_PATHS =
4428             CharUtilities.internImmutableSet(
4429                     Set.of(
4430                             // Individual zone overrides — were in getRawExtraPaths
4431                             "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/generic",
4432                             "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/standard",
4433                             "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/daylight",
4434                             "//ldml/dates/timeZoneNames/zone[@type=\"Europe/Dublin\"]/long/daylight",
4435                             "//ldml/dates/timeZoneNames/zone[@type=\"Europe/London\"]/long/daylight",
4436                             "//ldml/dates/timeZoneNames/zone[@type=\"Etc/UTC\"]/long/standard",
4437                             "//ldml/dates/timeZoneNames/zone[@type=\"Etc/UTC\"]/short/standard",
4438                             // Person name paths
4439                             "//ldml/personNames/sampleName[@item=\"nativeG\"]/nameField[@type=\"given\"]",
4440                             "//ldml/personNames/sampleName[@item=\"nativeGS\"]/nameField[@type=\"given\"]",
4441                             "//ldml/personNames/sampleName[@item=\"nativeGS\"]/nameField[@type=\"surname\"]",
4442                             "//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"given\"]",
4443                             "//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"given2\"]",
4444                             "//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"surname\"]",
4445                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"title\"]",
4446                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given\"]",
4447                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given-informal\"]",
4448                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given2\"]",
4449                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname-prefix\"]",
4450                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname-core\"]",
4451                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname2\"]",
4452                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"generation\"]",
4453                             "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"credentials\"]",
4454                             "//ldml/personNames/sampleName[@item=\"foreignG\"]/nameField[@type=\"given\"]",
4455                             "//ldml/personNames/sampleName[@item=\"foreignGS\"]/nameField[@type=\"given\"]",
4456                             "//ldml/personNames/sampleName[@item=\"foreignGS\"]/nameField[@type=\"surname\"]",
4457                             "//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"given\"]",
4458                             "//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"given2\"]",
4459                             "//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"surname\"]",
4460                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"title\"]",
4461                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given\"]",
4462                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given-informal\"]",
4463                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given2\"]",
4464                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname-prefix\"]",
4465                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname-core\"]",
4466                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname2\"]",
4467                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"generation\"]",
4468                             "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"credentials\"]"));
4469 }
4470