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