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