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