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