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