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