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