Home | History | Annotate | Download | only in util
      1 /*
      2  ******************************************************************************
      3  * Copyright (C) 2005-2011, International Business Machines Corporation and   *
      4  * others. All Rights Reserved.                                               *
      5  ******************************************************************************
      6  */
      7 
      8 package org.unicode.cldr.util;
      9 
     10 import java.lang.ref.WeakReference;
     11 import java.util.ArrayList;
     12 import java.util.Arrays;
     13 import java.util.Collection;
     14 import java.util.Collections;
     15 import java.util.Date;
     16 import java.util.HashMap;
     17 import java.util.HashSet;
     18 import java.util.Iterator;
     19 import java.util.LinkedHashMap;
     20 import java.util.List;
     21 import java.util.Map;
     22 import java.util.Set;
     23 import java.util.TreeMap;
     24 import java.util.WeakHashMap;
     25 import java.util.regex.Matcher;
     26 import java.util.regex.Pattern;
     27 
     28 import org.unicode.cldr.util.XPathParts.Comments;
     29 
     30 import com.ibm.icu.impl.Utility;
     31 import com.ibm.icu.util.Freezable;
     32 import com.ibm.icu.util.Output;
     33 import com.ibm.icu.util.VersionInfo;
     34 
     35 /**
     36  * Overall process is described in
     37  * http://cldr.unicode.org/development/development-process/design-proposals/resolution-of-cldr-files. Please update that
     38  * document if major
     39  * changes are made.
     40  */
     41 public abstract class XMLSource implements Freezable<XMLSource>, Iterable<String> {
     42     public static final String CODE_FALLBACK_ID = "code-fallback";
     43     public static final String ROOT_ID = "root";
     44     public static final boolean USE_PARTS_IN_ALIAS = false;
     45     private static final String TRACE_INDENT = " "; // "\t"
     46     private transient XPathParts parts = new XPathParts(null, null);
     47     private static Map<String, String> allowDuplicates = new HashMap<String, String>();
     48 
     49     private String localeID;
     50     private boolean nonInheriting;
     51     private TreeMap<String, String> aliases;
     52     private LinkedHashMap<String, List<String>> reverseAliases;
     53     protected boolean locked;
     54     transient String[] fixedPath = new String[1];
     55 
     56     public static class AliasLocation {
     57         public final String pathWhereFound;
     58         public final String localeWhereFound;
     59 
     60         public AliasLocation(String pathWhereFound, String localeWhereFound) {
     61             this.pathWhereFound = pathWhereFound;
     62             this.localeWhereFound = localeWhereFound;
     63         }
     64     }
     65 
     66     // Listeners are stored using weak references so that they can be garbage collected.
     67     private List<WeakReference<Listener>> listeners = new ArrayList<WeakReference<Listener>>();
     68 
     69     public String getLocaleID() {
     70         return localeID;
     71     }
     72 
     73     public void setLocaleID(String localeID) {
     74         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
     75         this.localeID = localeID;
     76     }
     77 
     78     /**
     79      * Adds all the path,value pairs in tempMap.
     80      * The paths must be Full Paths.
     81      *
     82      * @param tempMap
     83      * @param conflict_resolution
     84      */
     85     public void putAll(Map<String, String> tempMap, int conflict_resolution) {
     86         for (Iterator<String> it = tempMap.keySet().iterator(); it.hasNext();) {
     87             String path = it.next();
     88             if (conflict_resolution == CLDRFile.MERGE_KEEP_MINE && getValueAtPath(path) != null) continue;
     89             putValueAtPath(path, tempMap.get(path));
     90         }
     91     }
     92 
     93     /**
     94      * Adds all the path, value pairs in otherSource.
     95      *
     96      * @param otherSource
     97      * @param conflict_resolution
     98      */
     99     public void putAll(XMLSource otherSource, int conflict_resolution) {
    100         for (Iterator<String> it = otherSource.iterator(); it.hasNext();) {
    101             String path = it.next();
    102             final String oldValue = getValueAtDPath(path);
    103             if (conflict_resolution == CLDRFile.MERGE_KEEP_MINE && oldValue != null) {
    104                 continue;
    105             }
    106             final String newValue = otherSource.getValueAtDPath(path);
    107             if (newValue.equals(oldValue)) {
    108                 continue;
    109             }
    110             putValueAtPath(otherSource.getFullPathAtDPath(path), newValue);
    111         }
    112     }
    113 
    114     /**
    115      * Removes all the paths in the collection.
    116      * WARNING: must be distinguishedPaths
    117      *
    118      * @param xpaths
    119      */
    120     public void removeAll(Collection<String> xpaths) {
    121         for (Iterator<String> it = xpaths.iterator(); it.hasNext();) {
    122             removeValueAtDPath(it.next());
    123         }
    124     }
    125 
    126     /**
    127      * Tests whether the full path for this dpath is draft or now.
    128      *
    129      * @param path
    130      * @return
    131      */
    132     public boolean isDraft(String path) {
    133         String fullpath = getFullPath(path);
    134         if (path == null) return false;
    135         if (fullpath.indexOf("[@draft=") < 0) return false;
    136         return parts.set(fullpath).containsAttribute("draft");
    137     }
    138 
    139     public boolean isFrozen() {
    140         return locked;
    141     }
    142 
    143     /**
    144      * Adds the path,value pair. The path must be full path.
    145      *
    146      * @param xpath
    147      * @param value
    148      */
    149     public String putValueAtPath(String xpath, String value) {
    150         if (locked) {
    151             throw new UnsupportedOperationException("Attempt to modify locked object");
    152         }
    153         String distinguishingXPath = CLDRFile.getDistinguishingXPath(xpath, fixedPath, nonInheriting);
    154         putValueAtDPath(distinguishingXPath, value);
    155         if (!fixedPath[0].equals(distinguishingXPath)) {
    156             clearCache();
    157             putFullPathAtDPath(distinguishingXPath, fixedPath[0]);
    158         }
    159         return distinguishingXPath;
    160     }
    161 
    162     /**
    163      * Gets those paths that allow duplicates
    164      */
    165 
    166     public static Map<String, String> getPathsAllowingDuplicates() {
    167         return allowDuplicates;
    168     }
    169 
    170     /**
    171      * A listener for XML source data.
    172      */
    173     public static interface Listener {
    174         /**
    175          * Called whenever the source being listened to has a data change.
    176          *
    177          * @param xpath
    178          *            The xpath that had its value changed.
    179          * @param source
    180          *            back-pointer to the source that changed
    181          */
    182         public void valueChanged(String xpath, XMLSource source);
    183     }
    184 
    185     /**
    186      * Internal class. Immutable!
    187      */
    188     public static final class Alias {
    189         // public String oldLocaleID;
    190         final private String newLocaleID;
    191         final private String oldPath;
    192         final private String newPath;
    193         final private boolean pathsEqual;
    194         static final Pattern aliasPattern = Pattern
    195             .compile("(?:\\[@source=\"([^\"]*)\"])?(?:\\[@path=\"([^\"]*)\"])?(?:\\[@draft=\"([^\"]*)\"])?"); // constant,
    196 
    197         // so no
    198         // need to
    199         // sync
    200 
    201         public static Alias make(String aliasPath) {
    202             int pos = aliasPath.indexOf("/alias");
    203             if (pos < 0) return null; // quickcheck
    204             String aliasParts = aliasPath.substring(pos + 6);
    205             String oldPath = aliasPath.substring(0, pos);
    206             String newPath = null;
    207 
    208             return new Alias(pos, oldPath, newPath, aliasParts);
    209         }
    210 
    211         /**
    212          * @param newLocaleID
    213          * @param oldPath
    214          * @param aliasParts
    215          * @param newPath
    216          * @param pathsEqual
    217          */
    218         private Alias(int pos, String oldPath, String newPath, String aliasParts) {
    219             // if (USE_PARTS_IN_ALIAS) {
    220             // XPathParts tempAliasParts = new XPathParts(null, null);
    221             // if (!tempAliasParts.set(aliasPath).containsElement("alias")) {
    222             // return null;
    223             // }
    224             // Map attributes = tempAliasParts.getAttributes(tempAliasParts.size()-1);
    225             // result.newLocaleID = (String) attributes.get("source");
    226             // relativePath = (String) attributes.get("path");
    227             // if (result.newLocaleID != null && result.newLocaleID.equals("locale")) {
    228             // result.newLocaleID = null;
    229             // }
    230             // if (relativePath == null) {
    231             // result.newPath = result.oldPath;
    232             // }
    233             // else {
    234             // result.newPath = tempAliasParts.trimLast().addRelative(relativePath).toString();
    235             // }
    236             // } else {
    237             // do the same as the above with a regex
    238             Matcher matcher = aliasPattern.matcher(aliasParts);
    239             if (!matcher.matches()) {
    240                 throw new IllegalArgumentException("bad alias pattern for " + aliasParts);
    241             }
    242             String newLocaleID = matcher.group(1);
    243             if (newLocaleID != null && newLocaleID.equals("locale")) {
    244                 newLocaleID = null;
    245             }
    246             String relativePath2 = matcher.group(2);
    247             if (newPath == null) {
    248                 newPath = oldPath;
    249             }
    250             if (relativePath2 != null) {
    251                 newPath = addRelative(newPath, relativePath2);
    252             }
    253 
    254             // if (false) { // test
    255             // if (newLocaleID != null) {
    256             // if (!newLocaleID.equals(result.newLocaleID)) {
    257             // throw new IllegalArgumentException();
    258             // }
    259             // } else if (result.newLocaleID != null) {
    260             // throw new IllegalArgumentException();
    261             // }
    262             // if (!relativePath2.equals(relativePath)) {
    263             // throw new IllegalArgumentException();
    264             // }
    265             // if (!newPath.equals(result.newPath)) {
    266             // throw new IllegalArgumentException();
    267             // }
    268             // }
    269             // }
    270 
    271             boolean pathsEqual = oldPath.equals(newPath);
    272 
    273             if (pathsEqual && newLocaleID == null) {
    274                 throw new IllegalArgumentException("Alias must have different path or different source. AliasPath: "
    275                     + aliasParts
    276                     + ", Alias: " + newPath + ", " + newLocaleID);
    277             }
    278 
    279             this.newLocaleID = newLocaleID;
    280             this.oldPath = oldPath;
    281             this.newPath = newPath;
    282             this.pathsEqual = pathsEqual;
    283         }
    284 
    285         /**
    286          * Create a new path from an old path + relative portion.
    287          * Basically, each ../ at the front of the relative portion removes a trailing
    288          * element+attributes from the old path.
    289          * WARNINGS:
    290          * 1. It could fail if an attribute value contains '/'. This should not be the
    291          * case except in alias elements, but need to verify.
    292          * 2. Also assumes that there are no extra /'s in the relative or old path.
    293          * 3. If we verified that the relative paths always used " in place of ',
    294          * we could also save a step.
    295          *
    296          * Maybe we could clean up #2 and #3 when reading in a CLDRFile the first time?
    297          *
    298          * @param oldPath
    299          * @param relativePath
    300          * @return
    301          */
    302         static String addRelative(String oldPath, String relativePath) {
    303             if (relativePath.startsWith("//")) {
    304                 return relativePath;
    305             }
    306             while (relativePath.startsWith("../")) {
    307                 relativePath = relativePath.substring(3);
    308                 // strip extra "/". Shouldn't occur, but just to be safe.
    309                 while (relativePath.startsWith("/")) {
    310                     relativePath = relativePath.substring(1);
    311                 }
    312                 // strip last element
    313                 oldPath = stripLastElement(oldPath);
    314             }
    315             return oldPath + "/" + relativePath.replace('\'', '"');
    316         }
    317 
    318         // static final String ATTRIBUTE_PATTERN = "\\[@([^=]+)=\"([^\"]*)\"]";
    319         static final Pattern MIDDLE_OF_ATTRIBUTE_VALUE = PatternCache.get("[^\"]*\"\\]");
    320 
    321         public static String stripLastElement(String oldPath) {
    322             int oldPos = oldPath.lastIndexOf('/');
    323             // verify that we are not in the middle of an attribute value
    324             Matcher verifyElement = MIDDLE_OF_ATTRIBUTE_VALUE.matcher(oldPath.substring(oldPos));
    325             while (verifyElement.lookingAt()) {
    326                 oldPos = oldPath.lastIndexOf('/', oldPos - 1);
    327                 // will throw exception if we didn't find anything
    328                 verifyElement.reset(oldPath.substring(oldPos));
    329             }
    330             oldPath = oldPath.substring(0, oldPos);
    331             return oldPath;
    332         }
    333 
    334         public String toString() {
    335             return
    336             // "oldLocaleID: " + oldLocaleID + ", " +
    337             "newLocaleID: " + newLocaleID + ",\t"
    338                 +
    339                 "oldPath: " + oldPath + ",\n\t"
    340                 +
    341                 "newPath: " + newPath;
    342         }
    343 
    344         /**
    345          * This function is called on the full path, when we know the distinguishing path matches the oldPath.
    346          * So we just want to modify the base of the path
    347          *
    348          * @param oldPath
    349          * @param newPath
    350          * @param result
    351          * @return
    352          */
    353         public String changeNewToOld(String fullPath, String newPath, String oldPath) {
    354             // do common case quickly
    355             if (fullPath.startsWith(newPath)) {
    356                 return oldPath + fullPath.substring(newPath.length());
    357             }
    358 
    359             // fullPath will be the same as newPath, except for some attributes at the end.
    360             // add those attributes to oldPath, starting from the end.
    361             XPathParts partsOld = new XPathParts();
    362             XPathParts partsNew = new XPathParts();
    363             XPathParts partsFull = new XPathParts();
    364             partsOld.set(oldPath);
    365             partsNew.set(newPath);
    366             partsFull.set(fullPath);
    367             Map<String, String> attributesFull = partsFull.getAttributes(-1);
    368             Map<String, String> attributesNew = partsNew.getAttributes(-1);
    369             Map<String, String> attributesOld = partsOld.getAttributes(-1);
    370             for (Iterator<String> it = attributesFull.keySet().iterator(); it.hasNext();) {
    371                 String attribute = it.next();
    372                 if (attributesNew.containsKey(attribute)) continue;
    373                 attributesOld.put(attribute, attributesFull.get(attribute));
    374             }
    375             String result = partsOld.toString();
    376 
    377             // for now, just assume check that there are no goofy bits
    378             // if (!fullPath.startsWith(newPath)) {
    379             // if (false) {
    380             // throw new IllegalArgumentException("Failure to fix path. "
    381             // + Utility.LINE_SEPARATOR + "\tfullPath: " + fullPath
    382             // + Utility.LINE_SEPARATOR + "\toldPath: " + oldPath
    383             // + Utility.LINE_SEPARATOR + "\tnewPath: " + newPath
    384             // );
    385             // }
    386             // String tempResult = oldPath + fullPath.substring(newPath.length());
    387             // if (!result.equals(tempResult)) {
    388             // System.err.println("fullPath: " + fullPath + Utility.LINE_SEPARATOR + "\toldPath: "
    389             // + oldPath + Utility.LINE_SEPARATOR + "\tnewPath: " + newPath
    390             // + Utility.LINE_SEPARATOR + "\tnewPath: " + result);
    391             // }
    392             return result;
    393         }
    394 
    395         public String getOldPath() {
    396             return oldPath;
    397         }
    398 
    399         public String getNewLocaleID() {
    400             return newLocaleID;
    401         }
    402 
    403         public String getNewPath() {
    404             return newPath;
    405         }
    406 
    407         public String composeNewAndOldPath(String path) {
    408             return newPath + path.substring(oldPath.length());
    409         }
    410 
    411         public String composeOldAndNewPath(String path) {
    412             return oldPath + path.substring(newPath.length());
    413         }
    414 
    415         public boolean pathsEqual() {
    416             return pathsEqual;
    417         }
    418 
    419         public static boolean isAliasPath(String path) {
    420             return path.contains("/alias");
    421         }
    422     }
    423 
    424     /**
    425      * This method should be overridden.
    426      *
    427      * @return a mapping of paths to their aliases. Note that since root is the
    428      *         only locale to have aliases, all other locales will have no mappings.
    429      */
    430     protected synchronized TreeMap<String, String> getAliases() {
    431         // The cache assumes that aliases will never change over the lifetime of
    432         // an XMLSource.
    433         if (aliases == null) {
    434             aliases = new TreeMap<String, String>();
    435             // Look for aliases and create mappings for them.
    436             // Aliases are only ever found in root.
    437             for (String path : this) {
    438                 if (!Alias.isAliasPath(path)) continue;
    439                 String fullPath = getFullPathAtDPath(path);
    440                 Alias temp = Alias.make(fullPath);
    441                 if (temp == null) continue;
    442                 aliases.put(temp.getOldPath(), temp.getNewPath());
    443             }
    444         }
    445         return aliases;
    446     }
    447 
    448     /**
    449      * @return a reverse mapping of aliases
    450      */
    451     private LinkedHashMap<String, List<String>> getReverseAliases() {
    452         if (reverseAliases != null) return reverseAliases;
    453         // Aliases are only ever found in root.
    454         Map<String, String> aliases = getAliases();
    455         Map<String, List<String>> reverse = new HashMap<String, List<String>>();
    456         for (Map.Entry<String, String> entry : aliases.entrySet()) {
    457             List<String> list = reverse.get(entry.getValue());
    458             if (list == null) {
    459                 list = new ArrayList<String>();
    460                 reverse.put(entry.getValue(), list);
    461             }
    462             list.add(entry.getKey());
    463         }
    464 
    465         // Sort map.
    466         reverseAliases = new LinkedHashMap<String, List<String>>(new TreeMap<String, List<String>>(reverse));
    467         return reverseAliases;
    468     }
    469 
    470     /**
    471      * Clear any internal caches.
    472      */
    473     private void clearCache() {
    474         aliases = null;
    475     }
    476 
    477     /**
    478      * Return the localeID of the XMLSource where the path was found
    479      * SUBCLASSING: must be overridden in a resolving locale
    480      *
    481      * @param path
    482      * @param status
    483      *            TODO
    484      * @return
    485      */
    486     public String getSourceLocaleID(String path, CLDRFile.Status status) {
    487         if (status != null) {
    488             status.pathWhereFound = CLDRFile.getDistinguishingXPath(path, null, false);
    489         }
    490         return getLocaleID();
    491     }
    492 
    493     /**
    494      * Remove the value.
    495      * SUBCLASSING: must be overridden in a resolving locale
    496      *
    497      * @param xpath
    498      */
    499     public void removeValueAtPath(String xpath) {
    500         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
    501         clearCache();
    502         removeValueAtDPath(CLDRFile.getDistinguishingXPath(xpath, null, nonInheriting));
    503     }
    504 
    505     /**
    506      * Get the value.
    507      * SUBCLASSING: must be overridden in a resolving locale
    508      *
    509      * @param xpath
    510      * @return
    511      */
    512     public String getValueAtPath(String xpath) {
    513         return getValueAtDPath(CLDRFile.getDistinguishingXPath(xpath, null, nonInheriting));
    514     }
    515 
    516     /**
    517      * Get the full path for a distinguishing path
    518      * SUBCLASSING: must be overridden in a resolving locale
    519      *
    520      * @param xpath
    521      * @return
    522      */
    523     public String getFullPath(String xpath) {
    524         return getFullPathAtDPath(CLDRFile.getDistinguishingXPath(xpath, null, nonInheriting));
    525     }
    526 
    527     /**
    528      * Put the full path for this distinguishing path
    529      * The caller will have processed the path, and only call this with the distinguishing path
    530      * SUBCLASSING: must be overridden
    531      */
    532     abstract public void putFullPathAtDPath(String distinguishingXPath, String fullxpath);
    533 
    534     /**
    535      * Put the distinguishing path, value.
    536      * The caller will have processed the path, and only call this with the distinguishing path
    537      * SUBCLASSING: must be overridden
    538      */
    539     abstract public void putValueAtDPath(String distinguishingXPath, String value);
    540 
    541     /**
    542      * Remove the path, and the full path, and value corresponding to the path.
    543      * The caller will have processed the path, and only call this with the distinguishing path
    544      * SUBCLASSING: must be overridden
    545      */
    546     abstract public void removeValueAtDPath(String distinguishingXPath);
    547 
    548     /**
    549      * Get the value at the given distinguishing path
    550      * The caller will have processed the path, and only call this with the distinguishing path
    551      * SUBCLASSING: must be overridden
    552      */
    553     abstract public String getValueAtDPath(String path);
    554 
    555     public boolean hasValueAtDPath(String path) {
    556         return (getValueAtDPath(path) != null);
    557     }
    558 
    559     /**
    560      * Get the Last-Change Date (if known) when the value was changed.
    561      * SUBCLASSING: may be overridden. defaults to NULL.
    562      * @return last change date (if known), else null
    563      */
    564     public Date getChangeDateAtDPath(String path) {
    565         return null;
    566     }
    567 
    568     /**
    569      * Get the full path at the given distinguishing path
    570      * The caller will have processed the path, and only call this with the distinguishing path
    571      * SUBCLASSING: must be overridden
    572      */
    573     abstract public String getFullPathAtDPath(String path);
    574 
    575     /**
    576      * Get the comments for the source.
    577      * TODO: integrate the Comments class directly into this class
    578      * SUBCLASSING: must be overridden
    579      */
    580     abstract public Comments getXpathComments();
    581 
    582     /**
    583      * Set the comments for the source.
    584      * TODO: integrate the Comments class directly into this class
    585      * SUBCLASSING: must be overridden
    586      */
    587     abstract public void setXpathComments(Comments comments);
    588 
    589     /**
    590      * @return an iterator over the distinguished paths
    591      */
    592     abstract public Iterator<String> iterator();
    593 
    594     /**
    595      * @return an iterator over the distinguished paths that start with the prefix.
    596      *         SUBCLASSING: Normally overridden for efficiency
    597      */
    598     public Iterator<String> iterator(String prefix) {
    599         if (prefix == null || prefix.length() == 0) return iterator();
    600         return new com.ibm.icu.dev.util.CollectionUtilities.PrefixIterator().set(iterator(), prefix);
    601     }
    602 
    603     public Iterator<String> iterator(Matcher pathFilter) {
    604         if (pathFilter == null) return iterator();
    605         return new com.ibm.icu.dev.util.CollectionUtilities.RegexIterator().set(iterator(), pathFilter);
    606     }
    607 
    608     /**
    609      * @return returns whether resolving or not
    610      *         SUBCLASSING: Only changed for resolving subclasses
    611      */
    612     public boolean isResolving() {
    613         return false;
    614     }
    615 
    616     /**
    617      * Returns the unresolved version of this XMLSource.
    618      * SUBCLASSING: Override in resolving sources.
    619      */
    620     public XMLSource getUnresolving() {
    621         return this;
    622     }
    623 
    624     /**
    625      * SUBCLASSING: must be overridden
    626      */
    627     public XMLSource cloneAsThawed() {
    628         try {
    629             XMLSource result = (XMLSource) super.clone();
    630             result.locked = false;
    631             return result;
    632         } catch (CloneNotSupportedException e) {
    633             throw new InternalError("should never happen");
    634         }
    635     }
    636 
    637     /**
    638      * for debugging only
    639      */
    640     public String toString() {
    641         StringBuffer result = new StringBuffer();
    642         for (Iterator<String> it = iterator(); it.hasNext();) {
    643             String path = it.next();
    644             String value = getValueAtDPath(path);
    645             String fullpath = getFullPathAtDPath(path);
    646             result.append(fullpath).append(" =\t ").append(value).append(CldrUtility.LINE_SEPARATOR);
    647         }
    648         return result.toString();
    649     }
    650 
    651     /**
    652      * for debugging only
    653      */
    654     public String toString(String regex) {
    655         Matcher matcher = PatternCache.get(regex).matcher("");
    656         StringBuffer result = new StringBuffer();
    657         for (Iterator<String> it = iterator(matcher); it.hasNext();) {
    658             String path = it.next();
    659             // if (!matcher.reset(path).matches()) continue;
    660             String value = getValueAtDPath(path);
    661             String fullpath = getFullPathAtDPath(path);
    662             result.append(fullpath).append(" =\t ").append(value).append(CldrUtility.LINE_SEPARATOR);
    663         }
    664         return result.toString();
    665     }
    666 
    667     /**
    668      * @return returns whether supplemental or not
    669      */
    670     public boolean isNonInheriting() {
    671         return nonInheriting;
    672     }
    673 
    674     /**
    675      * @return sets whether supplemental. Normally only called internall.
    676      */
    677     public void setNonInheriting(boolean nonInheriting) {
    678         if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
    679         this.nonInheriting = nonInheriting;
    680     }
    681 
    682     /**
    683      * Internal class for doing resolution
    684      *
    685      * @author davis
    686      *
    687      */
    688     public static class ResolvingSource extends XMLSource implements Listener {
    689         private XMLSource currentSource;
    690         private LinkedHashMap<String, XMLSource> sources;
    691 
    692         public boolean isResolving() {
    693             return true;
    694         }
    695 
    696         public XMLSource getUnresolving() {
    697             return sources.get(getLocaleID());
    698         }
    699 
    700         /*
    701          * If there is an alias, then inheritance gets tricky.
    702          * If there is a path //ldml/xyz/.../uvw/alias[@path=...][@source=...]
    703          * then the parent for //ldml/xyz/.../uvw/abc/.../def/
    704          * is source, and the path to search for is really: //ldml/xyz/.../uvw/path/abc/.../def/
    705          */
    706         public static final boolean TRACE_VALUE = CldrUtility.getProperty("TRACE_VALUE", false);;
    707 
    708         // Map<String,String> getValueAtDPathCache = new HashMap();
    709 
    710         public String getValueAtDPath(String xpath) {
    711             if (DEBUG_PATH != null && DEBUG_PATH.matcher(xpath).find()) {
    712                 System.out.println("Getting value for Path: " + xpath);
    713             }
    714             if (TRACE_VALUE) System.out.println("\t*xpath: " + xpath
    715                 + CldrUtility.LINE_SEPARATOR + "\t*source: " + currentSource.getClass().getName()
    716                 + CldrUtility.LINE_SEPARATOR + "\t*locale: " + currentSource.getLocaleID());
    717             String result = null;
    718             AliasLocation fullStatus = getCachedFullStatus(xpath);
    719             if (fullStatus != null) {
    720                 if (TRACE_VALUE) {
    721                     System.out.println("\t*pathWhereFound: " + fullStatus.pathWhereFound);
    722                     System.out.println("\t*localeWhereFound: " + fullStatus.localeWhereFound);
    723                 }
    724                 result = getSource(fullStatus).getValueAtDPath(fullStatus.pathWhereFound);
    725             }
    726             if (TRACE_VALUE) System.out.println("\t*value: " + result);
    727             return result;
    728         }
    729 
    730         public XMLSource getSource(AliasLocation fullStatus) {
    731             XMLSource source = sources.get(fullStatus.localeWhereFound);
    732             return source == null ? constructedItems : source;
    733         }
    734 
    735         // public String _getValueAtDPath(String xpath) {
    736         // XMLSource currentSource = mySource;
    737         // String result;
    738         // ParentAndPath parentAndPath = new ParentAndPath();
    739         //
    740         // parentAndPath.set(xpath, currentSource, getLocaleID()).next();
    741         // while (true) {
    742         // if (parentAndPath.parentID == null) {
    743         // return constructedItems.getValueAtDPath(xpath);
    744         // }
    745         // currentSource = make(parentAndPath.parentID); // factory.make(parentAndPath.parentID, false).dataSource;
    746         // if (TRACE_VALUE) System.out.println("xpath: " + parentAndPath.path
    747         // + Utility.LINE_SEPARATOR + "\tsource: " + currentSource.getClass().getName()
    748         // + Utility.LINE_SEPARATOR + "\tlocale: " + currentSource.getLocaleID()
    749         // );
    750         // result = currentSource.getValueAtDPath(parentAndPath.path);
    751         // if (result != null) {
    752         // if (TRACE_VALUE) System.out.println("result: " + result);
    753         // return result;
    754         // }
    755         // parentAndPath.next();
    756         // }
    757         // }
    758 
    759         Map<String, String> getFullPathAtDPathCache = new HashMap<String, String>();
    760 
    761         public String getFullPathAtDPath(String xpath) {
    762             String result = currentSource.getFullPathAtDPath(xpath);
    763             if (result != null) {
    764                 return result;
    765             }
    766             // This is tricky. We need to find the alias location's path and full path.
    767             // then we need to the the non-distinguishing elements from them,
    768             // and add them into the requested path.
    769             AliasLocation fullStatus = getCachedFullStatus(xpath);
    770             if (fullStatus != null) {
    771                 String fullPathWhereFound = getSource(fullStatus).getFullPathAtDPath(fullStatus.pathWhereFound);
    772                 if (fullPathWhereFound == null) {
    773                     result = null;
    774                 } else if (fullPathWhereFound.equals(fullStatus.pathWhereFound)) {
    775                     result = xpath; // no difference
    776                 } else {
    777                     result = getFullPath(xpath, fullStatus, fullPathWhereFound);
    778                 }
    779             }
    780             //
    781             // result = getFullPathAtDPathCache.get(xpath);
    782             // if (result == null) {
    783             // if (getCachedKeySet().contains(xpath)) {
    784             // result = _getFullPathAtDPath(xpath);
    785             // getFullPathAtDPathCache.put(xpath, result);
    786             // }
    787             // }
    788             return result;
    789         }
    790 
    791         @Override
    792         public Date getChangeDateAtDPath(String xpath) {
    793             Date result = currentSource.getChangeDateAtDPath(xpath);
    794             if (result != null) {
    795                 return result;
    796             }
    797             AliasLocation fullStatus = getCachedFullStatus(xpath);
    798             if (fullStatus != null) {
    799                 result = getSource(fullStatus).getChangeDateAtDPath(fullStatus.pathWhereFound);
    800             }
    801             return result;
    802         }
    803 
    804         private String getFullPath(String xpath, AliasLocation fullStatus, String fullPathWhereFound) {
    805             String result = getFullPathAtDPathCache.get(xpath);
    806             if (result == null) {
    807                 // find the differences, and add them into xpath
    808                 // we do this by walking through each element, adding the corresponding attribute values.
    809                 // we add attributes FROM THE END, in case the lengths are different!
    810                 XPathParts xpathParts = new XPathParts().set(xpath);
    811                 XPathParts fullPathWhereFoundParts = new XPathParts().set(fullPathWhereFound);
    812                 XPathParts pathWhereFoundParts = new XPathParts().set(fullStatus.pathWhereFound);
    813                 int offset = xpathParts.size() - pathWhereFoundParts.size();
    814 
    815                 for (int i = 0; i < pathWhereFoundParts.size(); ++i) {
    816                     Map<String, String> fullAttributes = fullPathWhereFoundParts.getAttributes(i);
    817                     Map<String, String> attributes = pathWhereFoundParts.getAttributes(i);
    818                     if (!attributes.equals(fullAttributes)) { // add differences
    819                         //Map<String, String> targetAttributes = xpathParts.getAttributes(i + offset);
    820                         for (String key : fullAttributes.keySet()) {
    821                             if (!attributes.containsKey(key)) {
    822                                 String value = fullAttributes.get(key);
    823                                 xpathParts.putAttributeValue(i + offset, key, value);
    824                             }
    825                         }
    826                     }
    827                 }
    828                 result = xpathParts.toString();
    829                 getFullPathAtDPathCache.put(xpath, result);
    830             }
    831             return result;
    832         }
    833 
    834         /**
    835          * Return the value that would obtain if the value didn't exist.
    836          */
    837         @Override
    838         public String getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) {
    839             AliasLocation fullStatus = getPathLocation(xpath, true);
    840             if (localeWhereFound != null) {
    841                 localeWhereFound.value = fullStatus.localeWhereFound;
    842             }
    843             if (pathWhereFound != null) {
    844                 pathWhereFound.value = fullStatus.pathWhereFound;
    845             }
    846             return getSource(fullStatus).getValueAtDPath(fullStatus.pathWhereFound);
    847         }
    848 
    849         private AliasLocation getCachedFullStatus(String xpath) {
    850             synchronized (getSourceLocaleIDCache) {
    851                 AliasLocation fullStatus = getSourceLocaleIDCache.get(xpath);
    852                 if (fullStatus == null) {
    853                     fullStatus = getPathLocation(xpath, false);
    854                     getSourceLocaleIDCache.put(xpath, fullStatus); // cache copy
    855                 }
    856                 return fullStatus;
    857             }
    858         }
    859 
    860         // private String _getFullPathAtDPath(String xpath) {
    861         // String result = null;
    862         // XMLSource currentSource = mySource;
    863         // ParentAndPath parentAndPath = new ParentAndPath();
    864         // parentAndPath.set(xpath, currentSource, getLocaleID()).next();
    865         // while (true) {
    866         // if (parentAndPath.parentID == null) {
    867         // return constructedItems.getFullPathAtDPath(xpath);
    868         // }
    869         // currentSource = make(parentAndPath.parentID); // factory.make(parentAndPath.parentID, false).dataSource;
    870         // result = currentSource.getValueAtDPath(parentAndPath.path);
    871         // if (result != null) {
    872         // result = currentSource.getFullPathAtDPath(parentAndPath.path);
    873         // return tempAlias.changeNewToOld(result, parentAndPath.path, xpath);
    874         // }
    875         // parentAndPath.next();
    876         // }
    877         // }
    878 
    879         public String getWinningPath(String xpath) {
    880             String result = currentSource.getWinningPath(xpath);
    881             if (result != null) return result;
    882             AliasLocation fullStatus = getCachedFullStatus(xpath);
    883             if (fullStatus != null) {
    884                 result = getSource(fullStatus).getWinningPath(fullStatus.pathWhereFound);
    885             } else {
    886                 result = xpath;
    887             }
    888             //
    889             // result = getWinningPathCache.get(xpath);
    890             // if (result == null) {
    891             // if (!getCachedKeySet().contains(xpath)) {
    892             // return xpath;
    893             // }
    894             // result = _getWinningPath(xpath);
    895             // getWinningPathCache.put(xpath, result);
    896             // }
    897             return result;
    898         }
    899 
    900         // Map<String,String> getWinningPathCache = new HashMap();
    901         //
    902         // public String _getWinningPath(String xpath) {
    903         // XMLSource currentSource = mySource;
    904         // ParentAndPath parentAndPath = new ParentAndPath();
    905         // parentAndPath.set(xpath, currentSource, getLocaleID()).next();
    906         // while (true) {
    907         // if (parentAndPath.parentID == null) {
    908         // return xpath; // ran out of parents
    909         // //return constructedItems.getWinningPath(xpath);
    910         // }
    911         // currentSource = make(parentAndPath.parentID); // factory.make(parentAndPath.parentID, false).dataSource;
    912         // String result = currentSource.getWinningPath(parentAndPath.path);
    913         // if (result != null) {
    914         // return result;
    915         // }
    916         // parentAndPath.next();
    917         // }
    918         // }
    919 
    920         private transient Map<String, AliasLocation> getSourceLocaleIDCache = new WeakHashMap<String, AliasLocation>();
    921 
    922         public String getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status) {
    923             AliasLocation fullStatus = getCachedFullStatus(distinguishedXPath);
    924             if (status != null) {
    925                 status.pathWhereFound = fullStatus.pathWhereFound;
    926             }
    927             return fullStatus.localeWhereFound;
    928         }
    929 
    930         static final Pattern COUNT_EQUALS = PatternCache.get("\\[@count=\"[^\"]*\"]");
    931 
    932         private AliasLocation getPathLocation(String xpath, boolean skipFirst) {
    933             for (XMLSource source : sources.values()) {
    934                 // allow the first source to be skipped, for george bailey value
    935                 if (skipFirst) {
    936                     skipFirst = false;
    937                     continue;
    938                 }
    939                 if (source.hasValueAtDPath(xpath)) {
    940                     String value = source.getValueAtDPath(xpath);
    941                     /*
    942                      * TODO: this looks dubious, see https://unicode.org/cldr/trac/ticket/11299
    943                      *
    944                      * Both the "immediate parent" and the "ultimate ancestor" may be of interest, as for
    945                      * "Jump to Original" -- does this code result in skipping the immediate parent?
    946                      */
    947                     if (CldrUtility.INHERITANCE_MARKER.equals(value)) {
    948                         continue;
    949                     }
    950                     return new AliasLocation(xpath, source.getLocaleID());
    951                 }
    952             }
    953             // Path not found, check if an alias exists
    954             TreeMap<String, String> aliases = sources.get("root").getAliases();
    955             String aliasedPath = aliases.get(xpath);
    956 
    957             if (aliasedPath == null) {
    958                 // Check if there is an alias for a subset xpath.
    959                 // If there are one or more matching aliases, lowerKey() will
    960                 // return the alias with the longest matching prefix since the
    961                 // hashmap is sorted according to xpath.
    962                 String possibleSubpath = aliases.lowerKey(xpath);
    963                 if (possibleSubpath != null && xpath.startsWith(possibleSubpath)) {
    964                     aliasedPath = aliases.get(possibleSubpath) +
    965                         xpath.substring(possibleSubpath.length());
    966                 }
    967             }
    968 
    969             // counts are special; they act like there is a root alias to 'other'
    970             // and in the special case of currencies, other => null
    971             // //ldml/numbers/currencies/currency[@type="BRZ"]/displayName[@count="other"] => //ldml/numbers/currencies/currency[@type="BRZ"]/displayName
    972             if (aliasedPath == null && xpath.contains("[@count=")) {
    973                 aliasedPath = COUNT_EQUALS.matcher(xpath).replaceAll("[@count=\"other\"]");
    974                 if (aliasedPath.equals(xpath)) {
    975                     if (xpath.contains("/displayName")) {
    976                         aliasedPath = COUNT_EQUALS.matcher(xpath).replaceAll("");
    977                         if (aliasedPath.equals(xpath)) {
    978                             throw new RuntimeException("Internal error");
    979                         }
    980                     } else {
    981                         aliasedPath = null;
    982                     }
    983                 }
    984             }
    985 
    986             if (aliasedPath != null) {
    987                 // Call getCachedFullStatus recursively to avoid recalculating cached aliases.
    988                 return getCachedFullStatus(aliasedPath);
    989             }
    990 
    991             // Fallback location.
    992             return new AliasLocation(xpath, CODE_FALLBACK_ID);
    993         }
    994 
    995         /**
    996          * We have to go through the source, add all the paths, then recurse to parents
    997          * However, aliases are tricky, so watch it.
    998          */
    999         static final boolean TRACE_FILL = CldrUtility.getProperty("TRACE_FILL", false);
   1000         static final String DEBUG_PATH_STRING = CldrUtility.getProperty("DEBUG_PATH", null);
   1001         static final Pattern DEBUG_PATH = DEBUG_PATH_STRING == null ? null : PatternCache.get(DEBUG_PATH_STRING);
   1002         static final boolean SKIP_FALLBACKID = CldrUtility.getProperty("SKIP_FALLBACKID", false);;
   1003 
   1004         static final int MAX_LEVEL = 40; /* Throw an error if it goes past this. */
   1005 
   1006         /**
   1007          * Initialises the set of xpaths that a fully resolved XMLSource contains.
   1008          * http://cldr.unicode.org/development/development-process/design-proposals/resolution-of-cldr-files.
   1009          * Information about the aliased path and source locale ID of each xpath
   1010          * is not precalculated here since it doesn't appear to improve overall
   1011          * performance.
   1012          */
   1013         private Set<String> fillKeys() {
   1014             Set<String> paths = findNonAliasedPaths();
   1015             // Find aliased paths and loop until no more aliases can be found.
   1016             Set<String> newPaths = paths;
   1017             int level = 0;
   1018             boolean newPathsFound = false;
   1019             do {
   1020                 // Debugging code to protect against an infinite loop.
   1021                 if (TRACE_FILL && DEBUG_PATH == null || level > MAX_LEVEL) {
   1022                     System.out.println(Utility.repeat(TRACE_INDENT, level) + "# paths waiting to be aliased: "
   1023                         + newPaths.size());
   1024                     System.out.println(Utility.repeat(TRACE_INDENT, level) + "# paths found: " + paths.size());
   1025                 }
   1026                 if (level > MAX_LEVEL) throw new IllegalArgumentException("Stack overflow");
   1027 
   1028                 String[] sortedPaths = new String[newPaths.size()];
   1029                 newPaths.toArray(sortedPaths);
   1030                 Arrays.sort(sortedPaths);
   1031 
   1032                 newPaths = getDirectAliases(sortedPaths);
   1033                 newPathsFound = paths.addAll(newPaths);
   1034                 level++;
   1035             } while (newPathsFound);
   1036             return paths;
   1037         }
   1038 
   1039         /**
   1040          * Creates the set of resolved paths for this ResolvingSource while
   1041          * ignoring aliasing.
   1042          *
   1043          * @return
   1044          */
   1045         private Set<String> findNonAliasedPaths() {
   1046             HashSet<String> paths = new HashSet<String>();
   1047 
   1048             // Get all XMLSources used during resolution.
   1049             List<XMLSource> sourceList = new ArrayList<XMLSource>(sources.values());
   1050             if (!SKIP_FALLBACKID) {
   1051                 sourceList.add(constructedItems);
   1052             }
   1053 
   1054             // Make a pass through, filling all the direct paths, excluding aliases, and collecting others
   1055             for (XMLSource curSource : sourceList) {
   1056                 for (String xpath : curSource) {
   1057                     paths.add(xpath);
   1058                 }
   1059             }
   1060             return paths;
   1061         }
   1062 
   1063         /**
   1064          * Takes in a list of xpaths and returns a new set of paths that alias
   1065          * directly to those existing xpaths.
   1066          *
   1067          * @param paths
   1068          *            a sorted list of xpaths
   1069          * @param reverseAliases
   1070          *            a map of reverse aliases sorted by key.
   1071          * @return
   1072          */
   1073         private Set<String> getDirectAliases(String[] paths) {
   1074             HashSet<String> newPaths = new HashSet<String>();
   1075             // Keep track of the current path index: since it's sorted, we
   1076             // never have to backtrack.
   1077             int pathIndex = 0;
   1078             LinkedHashMap<String, List<String>> reverseAliases = getReverseAliases();
   1079             for (String subpath : reverseAliases.keySet()) {
   1080                 // Find the first path that matches the current alias.
   1081                 while (pathIndex < paths.length &&
   1082                     paths[pathIndex].compareTo(subpath) < 0) {
   1083                     pathIndex++;
   1084                 }
   1085 
   1086                 // Alias all paths that match the current alias.
   1087                 String xpath;
   1088                 List<String> list = reverseAliases.get(subpath);
   1089                 int endIndex = pathIndex;
   1090                 int suffixStart = subpath.length();
   1091                 // Suffixes should always start with an element and not an
   1092                 // attribute to prevent invalid aliasing.
   1093                 while (endIndex < paths.length &&
   1094                     (xpath = paths[endIndex]).startsWith(subpath) &&
   1095                     xpath.charAt(suffixStart) == '/') {
   1096                     String suffix = xpath.substring(suffixStart);
   1097                     for (String reverseAlias : list) {
   1098                         String reversePath = reverseAlias + suffix;
   1099                         newPaths.add(reversePath);
   1100                     }
   1101                     endIndex++;
   1102                 }
   1103                 if (endIndex == paths.length) break;
   1104             }
   1105             return newPaths;
   1106         }
   1107 
   1108         private LinkedHashMap<String, List<String>> getReverseAliases() {
   1109             return sources.get("root").getReverseAliases();
   1110         }
   1111 
   1112         private transient Set<String> cachedKeySet = null;
   1113 
   1114         /**
   1115          * @return an iterator over all the xpaths in this XMLSource.
   1116          */
   1117         public Iterator<String> iterator() {
   1118             return getCachedKeySet().iterator();
   1119         }
   1120 
   1121         private Set<String> getCachedKeySet() {
   1122             if (cachedKeySet == null) {
   1123                 cachedKeySet = fillKeys();
   1124                 // System.out.println("CachedKeySet: " + cachedKeySet);
   1125                 // cachedKeySet.addAll(constructedItems.keySet());
   1126                 cachedKeySet = Collections.unmodifiableSet(cachedKeySet);
   1127             }
   1128             return cachedKeySet;
   1129         }
   1130 
   1131         public void putFullPathAtDPath(String distinguishingXPath, String fullxpath) {
   1132             throw new UnsupportedOperationException("Resolved CLDRFiles are read-only");
   1133         }
   1134 
   1135         public void putValueAtDPath(String distinguishingXPath, String value) {
   1136             throw new UnsupportedOperationException("Resolved CLDRFiles are read-only");
   1137         }
   1138 
   1139         public Comments getXpathComments() {
   1140             return currentSource.getXpathComments();
   1141         }
   1142 
   1143         public void setXpathComments(Comments path) {
   1144             throw new UnsupportedOperationException("Resolved CLDRFiles are read-only");
   1145         }
   1146 
   1147         public void removeValueAtDPath(String xpath) {
   1148             throw new UnsupportedOperationException("Resolved CLDRFiles are  read-only");
   1149         }
   1150 
   1151         public XMLSource freeze() {
   1152             return this; // No-op. ResolvingSource is already read-only.
   1153         }
   1154 
   1155         @Override
   1156         public void valueChanged(String xpath, XMLSource nonResolvingSource) {
   1157             synchronized (getSourceLocaleIDCache) {
   1158                 AliasLocation location = getSourceLocaleIDCache.remove(xpath);
   1159                 if (location == null) return;
   1160                 // Paths aliasing to this path (directly or indirectly) may be affected,
   1161                 // so clear them as well.
   1162                 // There's probably a more elegant way to fix the paths than simply
   1163                 // throwing everything out.
   1164                 Set<String> dependentPaths = getDirectAliases(new String[] { xpath });
   1165                 if (dependentPaths.size() > 0) {
   1166                     for (String path : dependentPaths) {
   1167                         getSourceLocaleIDCache.remove(path);
   1168                     }
   1169                 }
   1170             }
   1171         }
   1172 
   1173         /**
   1174          * Creates a new ResolvingSource with the given locale resolution chain.
   1175          *
   1176          * @param sourceList
   1177          *            the list of XMLSources to look in during resolution,
   1178          *            ordered from the current locale up to root.
   1179          */
   1180         public ResolvingSource(List<XMLSource> sourceList) {
   1181             // Sanity check for root.
   1182             if (sourceList == null || !sourceList.get(sourceList.size() - 1).getLocaleID().equals("root")) {
   1183                 throw new IllegalArgumentException("Last element should be root");
   1184             }
   1185             currentSource = sourceList.get(0); // Convenience variable
   1186             sources = new LinkedHashMap<String, XMLSource>();
   1187             for (XMLSource source : sourceList) {
   1188                 sources.put(source.getLocaleID(), source);
   1189             }
   1190 
   1191             // Add listeners to all locales except root, since we don't expect
   1192             // root to change programatically.
   1193             for (int i = 0, limit = sourceList.size() - 1; i < limit; i++) {
   1194                 sourceList.get(i).addListener(this);
   1195             }
   1196         }
   1197 
   1198         public String getLocaleID() {
   1199             return currentSource.getLocaleID();
   1200         }
   1201 
   1202         private static final String[] keyDisplayNames = {
   1203             "calendar",
   1204             "cf",
   1205             "collation",
   1206             "currency",
   1207             "hc",
   1208             "lb",
   1209             "ms",
   1210             "numbers"
   1211         };
   1212         private static final String[][] typeDisplayNames = {
   1213             { "account", "cf" },
   1214             { "ahom", "numbers" },
   1215             { "arab", "numbers" },
   1216             { "arabext", "numbers" },
   1217             { "armn", "numbers" },
   1218             { "armnlow", "numbers" },
   1219             { "bali", "numbers" },
   1220             { "beng", "numbers" },
   1221             { "big5han", "collation" },
   1222             { "brah", "numbers" },
   1223             { "buddhist", "calendar" },
   1224             { "cakm", "numbers" },
   1225             { "cham", "numbers" },
   1226             { "chinese", "calendar" },
   1227             { "compat", "collation" },
   1228             { "coptic", "calendar" },
   1229             { "cyrl", "numbers" },
   1230             { "dangi", "calendar" },
   1231             { "deva", "numbers" },
   1232             { "dictionary", "collation" },
   1233             { "ducet", "collation" },
   1234             { "emoji", "collation" },
   1235             { "eor", "collation" },
   1236             { "ethi", "numbers" },
   1237             { "ethiopic", "calendar" },
   1238             { "ethiopic-amete-alem", "calendar" },
   1239             { "fullwide", "numbers" },
   1240             { "gb2312han", "collation" },
   1241             { "geor", "numbers" },
   1242             { "gong", "numbers" },
   1243             { "gonm", "numbers" },
   1244             { "gregorian", "calendar" },
   1245             { "grek", "numbers" },
   1246             { "greklow", "numbers" },
   1247             { "gujr", "numbers" },
   1248             { "guru", "numbers" },
   1249             { "h11", "hc" },
   1250             { "h12", "hc" },
   1251             { "h23", "hc" },
   1252             { "h24", "hc" },
   1253             { "hanidec", "numbers" },
   1254             { "hans", "numbers" },
   1255             { "hansfin", "numbers" },
   1256             { "hant", "numbers" },
   1257             { "hantfin", "numbers" },
   1258             { "hebr", "numbers" },
   1259             { "hebrew", "calendar" },
   1260             { "hmng", "numbers" },
   1261             { "indian", "calendar" },
   1262             { "islamic", "calendar" },
   1263             { "islamic-civil", "calendar" },
   1264             { "islamic-rgsa", "calendar" },
   1265             { "islamic-tbla", "calendar" },
   1266             { "islamic-umalqura", "calendar" },
   1267             { "iso8601", "calendar" },
   1268             { "japanese", "calendar" },
   1269             { "java", "numbers" },
   1270             { "jpan", "numbers" },
   1271             { "jpanfin", "numbers" },
   1272             { "kali", "numbers" },
   1273             { "khmr", "numbers" },
   1274             { "knda", "numbers" },
   1275             { "lana", "numbers" },
   1276             { "lanatham", "numbers" },
   1277             { "laoo", "numbers" },
   1278             { "latn", "numbers" },
   1279             { "lepc", "numbers" },
   1280             { "limb", "numbers" },
   1281             { "loose", "lb" },
   1282             { "mathbold", "numbers" },
   1283             { "mathdbl", "numbers" },
   1284             { "mathmono", "numbers" },
   1285             { "mathsanb", "numbers" },
   1286             { "mathsans", "numbers" },
   1287             { "metric", "ms" },
   1288             { "mlym", "numbers" },
   1289             { "modi", "numbers" },
   1290             { "mong", "numbers" },
   1291             { "mroo", "numbers" },
   1292             { "mtei", "numbers" },
   1293             { "mymr", "numbers" },
   1294             { "mymrshan", "numbers" },
   1295             { "mymrtlng", "numbers" },
   1296             { "nkoo", "numbers" },
   1297             { "normal", "lb" },
   1298             { "olck", "numbers" },
   1299             { "orya", "numbers" },
   1300             { "osma", "numbers" },
   1301             { "persian", "calendar" },
   1302             { "phonebook", "collation" },
   1303             { "pinyin", "collation" },
   1304             { "reformed", "collation" },
   1305             { "roc", "calendar" },
   1306             { "rohg", "numbers" },
   1307             { "roman", "numbers" },
   1308             { "romanlow", "numbers" },
   1309             { "saur", "numbers" },
   1310             { "search", "collation" },
   1311             { "searchjl", "collation" },
   1312             { "shrd", "numbers" },
   1313             { "sind", "numbers" },
   1314             { "sinh", "numbers" },
   1315             { "sora", "numbers" },
   1316             { "standard", "cf" },
   1317             { "standard", "collation" },
   1318             { "strict", "lb" },
   1319             { "stroke", "collation" },
   1320             { "sund", "numbers" },
   1321             { "takr", "numbers" },
   1322             { "talu", "numbers" },
   1323             { "taml", "numbers" },
   1324             { "tamldec", "numbers" },
   1325             { "telu", "numbers" },
   1326             { "thai", "numbers" },
   1327             { "tibt", "numbers" },
   1328             { "tirh", "numbers" },
   1329             { "traditional", "collation" },
   1330             { "unihan", "collation" },
   1331             { "uksystem", "ms" },
   1332             { "ussystem", "ms" },
   1333             { "vaii", "numbers" },
   1334             { "wara", "numbers" },
   1335             { "zhuyin", "collation" } };
   1336 
   1337         private static final boolean SKIP_SINGLEZONES = false;
   1338         private static XMLSource constructedItems = new SimpleXMLSource(CODE_FALLBACK_ID);
   1339 
   1340         static {
   1341             StandardCodes sc = StandardCodes.make();
   1342             Map<String, Set<String>> countries_zoneSet = sc.getCountryToZoneSet();
   1343             Map<String, String> zone_countries = sc.getZoneToCounty();
   1344 
   1345             // Set types = sc.getAvailableTypes();
   1346             for (int typeNo = 0; typeNo <= CLDRFile.TZ_START; ++typeNo) {
   1347                 String type = CLDRFile.getNameName(typeNo);
   1348                 // int typeNo = typeNameToCode(type);
   1349                 // if (typeNo < 0) continue;
   1350                 String type2 = (typeNo == CLDRFile.CURRENCY_SYMBOL) ? CLDRFile.getNameName(CLDRFile.CURRENCY_NAME)
   1351                     : (typeNo >= CLDRFile.TZ_START) ? "tzid"
   1352                         : type;
   1353                 Set<String> codes = sc.getSurveyToolDisplayCodes(type2);
   1354                 // String prefix = CLDRFile.NameTable[typeNo][0];
   1355                 // String postfix = CLDRFile.NameTable[typeNo][1];
   1356                 // String prefix2 = "//ldml" + prefix.substring(6); // [@version=\"" + GEN_VERSION + "\"]
   1357                 for (Iterator<String> codeIt = codes.iterator(); codeIt.hasNext();) {
   1358                     String code = codeIt.next();
   1359                     String value = code;
   1360                     if (typeNo == CLDRFile.TZ_EXEMPLAR) { // skip single-zone countries
   1361                         if (SKIP_SINGLEZONES) {
   1362                             String country = (String) zone_countries.get(code);
   1363                             Set<String> s = countries_zoneSet.get(country);
   1364                             if (s != null && s.size() == 1) continue;
   1365                         }
   1366                         value = TimezoneFormatter.getFallbackName(value);
   1367                     }
   1368                     addFallbackCode(typeNo, code, value);
   1369                 }
   1370             }
   1371 
   1372             // Add commonlyUsed
   1373             // //ldml/dates/timeZoneNames/metazone[@type="New_Zealand"]/commonlyUsed
   1374             // should get this from supplemental metadata, but for now...
   1375             // String[] metazones =
   1376             // "Acre Afghanistan Africa_Central Africa_Eastern Africa_FarWestern Africa_Southern Africa_Western Aktyubinsk Alaska Alaska_Hawaii Almaty Amazon America_Central America_Eastern America_Mountain America_Pacific Anadyr Aqtau Aqtobe Arabian Argentina Argentina_Western Armenia Ashkhabad Atlantic Australia_Central Australia_CentralWestern Australia_Eastern Australia_Western Azerbaijan Azores Baku Bangladesh Bering Bhutan Bolivia Borneo Brasilia British Brunei Cape_Verde Chamorro Changbai Chatham Chile China Choibalsan Christmas Cocos Colombia Cook Cuba Dacca Davis Dominican DumontDUrville Dushanbe Dutch_Guiana East_Timor Easter Ecuador Europe_Central Europe_Eastern Europe_Western Falkland Fiji French_Guiana French_Southern Frunze Gambier GMT Galapagos Georgia Gilbert_Islands Goose_Bay Greenland_Central Greenland_Eastern Greenland_Western Guam Gulf Guyana Hawaii_Aleutian Hong_Kong Hovd India Indian_Ocean Indochina Indonesia_Central Indonesia_Eastern Indonesia_Western Iran Irkutsk Irish Israel Japan Kamchatka Karachi Kashgar Kazakhstan_Eastern Kazakhstan_Western Kizilorda Korea Kosrae Krasnoyarsk Kuybyshev Kwajalein Kyrgystan Lanka Liberia Line_Islands Long_Shu Lord_Howe Macau Magadan Malaya Malaysia Maldives Marquesas Marshall_Islands Mauritius Mawson Mongolia Moscow Myanmar Nauru Nepal New_Caledonia New_Zealand Newfoundland Niue Norfolk North_Mariana Noronha Novosibirsk Omsk Oral Pakistan Palau Papua_New_Guinea Paraguay Peru Philippines Phoenix_Islands Pierre_Miquelon Pitcairn Ponape Qyzylorda Reunion Rothera Sakhalin Samara Samarkand Samoa Seychelles Shevchenko Singapore Solomon South_Georgia Suriname Sverdlovsk Syowa Tahiti Tajikistan Tashkent Tbilisi Tokelau Tonga Truk Turkey Turkmenistan Tuvalu Uralsk Uruguay Urumqi Uzbekistan Vanuatu Venezuela Vladivostok Volgograd Vostok Wake Wallis Yakutsk Yekaterinburg Yerevan Yukon".split("\\s+");
   1377             // for (String metazone : metazones) {
   1378             // constructedItems.putValueAtPath(
   1379             // "//ldml/dates/timeZoneNames/metazone[@type=\""
   1380             // + metazone
   1381             // + "\"]/commonlyUsed",
   1382             // "false");
   1383             // }
   1384 
   1385             String[] extraCodes = { "ar_001", "de_AT", "de_CH", "en_AU", "en_CA", "en_GB", "en_US", "es_419", "es_ES", "es_MX",
   1386                 "fr_CA", "fr_CH", "frc", "lou", "nds_NL", "nl_BE", "pt_BR", "pt_PT", "ro_MD", "sw_CD", "zh_Hans", "zh_Hant" };
   1387             for (String extraCode : extraCodes) {
   1388                 addFallbackCode(CLDRFile.LANGUAGE_NAME, extraCode, extraCode);
   1389             }
   1390 
   1391 
   1392             addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_GB", "en_GB", "short");
   1393             addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_US", "en_US", "short");
   1394             addFallbackCode(CLDRFile.LANGUAGE_NAME, "az", "az", "short");
   1395 
   1396             addFallbackCode(CLDRFile.SCRIPT_NAME, "Hans", "Hans", "stand-alone");
   1397             addFallbackCode(CLDRFile.SCRIPT_NAME, "Hant", "Hant", "stand-alone");
   1398 
   1399             addFallbackCode(CLDRFile.TERRITORY_NAME, "GB", "GB", "short");
   1400             addFallbackCode(CLDRFile.TERRITORY_NAME, "HK", "HK", "short");
   1401             addFallbackCode(CLDRFile.TERRITORY_NAME, "MO", "MO", "short");
   1402             addFallbackCode(CLDRFile.TERRITORY_NAME, "PS", "PS", "short");
   1403             addFallbackCode(CLDRFile.TERRITORY_NAME, "US", "US", "short");
   1404 
   1405             addFallbackCode(CLDRFile.TERRITORY_NAME, "CD", "CD", "variant"); // add other geopolitical items
   1406             addFallbackCode(CLDRFile.TERRITORY_NAME, "CG", "CG", "variant");
   1407             addFallbackCode(CLDRFile.TERRITORY_NAME, "CI", "CI", "variant");
   1408             addFallbackCode(CLDRFile.TERRITORY_NAME, "CZ", "CZ", "variant");
   1409             addFallbackCode(CLDRFile.TERRITORY_NAME, "FK", "FK", "variant");
   1410             addFallbackCode(CLDRFile.TERRITORY_NAME, "MK", "MK", "variant");
   1411             addFallbackCode(CLDRFile.TERRITORY_NAME, "TL", "TL", "variant");
   1412 
   1413             addFallbackCode(CLDRFile.TERRITORY_NAME, "XA", "XA");
   1414             addFallbackCode(CLDRFile.TERRITORY_NAME, "XB", "XB");
   1415 
   1416             addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr/era[@type=\"0\"]", "BCE", "variant");
   1417             addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr/era[@type=\"1\"]", "CE", "variant");
   1418             addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames/era[@type=\"0\"]", "BCE", "variant");
   1419             addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames/era[@type=\"1\"]", "CE", "variant");
   1420             addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNarrow/era[@type=\"0\"]", "BCE", "variant");
   1421             addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNarrow/era[@type=\"1\"]", "CE", "variant");
   1422 
   1423             //String defaultCurrPattern = "#,##0.00"; // use root value; can't get the locale's currency pattern in this static context; "" and "" cause errors.
   1424             for (int i = 0; i < keyDisplayNames.length; ++i) {
   1425                 constructedItems.putValueAtPath(
   1426                     "//ldml/localeDisplayNames/keys/key" +
   1427                         "[@type=\"" + keyDisplayNames[i] + "\"]",
   1428                     keyDisplayNames[i]);
   1429             }
   1430             for (int i = 0; i < typeDisplayNames.length; ++i) {
   1431                 constructedItems.putValueAtPath(
   1432                     "//ldml/localeDisplayNames/types/type"
   1433                         + "[@key=\"" + typeDisplayNames[i][1] + "\"]"
   1434                         + "[@type=\"" + typeDisplayNames[i][0] + "\"]",
   1435                     typeDisplayNames[i][0]);
   1436             }
   1437             //            String[][] relativeValues = {
   1438             //                // {"Three days ago", "-3"},
   1439             //                { "The day before yesterday", "-2" },
   1440             //                { "Yesterday", "-1" },
   1441             //                { "Today", "0" },
   1442             //                { "Tomorrow", "1" },
   1443             //                { "The day after tomorrow", "2" },
   1444             //                // {"Three days from now", "3"},
   1445             //            };
   1446             //            for (int i = 0; i < relativeValues.length; ++i) {
   1447             //                constructedItems.putValueAtPath(
   1448             //                    "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/fields/field[@type=\"day\"]/relative[@type=\""
   1449             //                        + relativeValues[i][1] + "\"]",
   1450             //                        relativeValues[i][0]);
   1451             //            }
   1452 
   1453             constructedItems.freeze();
   1454             allowDuplicates = Collections.unmodifiableMap(allowDuplicates);
   1455             // System.out.println("constructedItems: " + constructedItems);
   1456         }
   1457 
   1458         private static void addFallbackCode(int typeNo, String code, String value) {
   1459             addFallbackCode(typeNo, code, value, null);
   1460         }
   1461 
   1462         private static void addFallbackCode(int typeNo, String code, String value, String alt) {
   1463             // String path = prefix + code + postfix;
   1464             String fullpath = CLDRFile.getKey(typeNo, code);
   1465             String distinguishingPath = addFallbackCodeToConstructedItems(fullpath, value, alt);
   1466             if (typeNo == CLDRFile.LANGUAGE_NAME || typeNo == CLDRFile.SCRIPT_NAME || typeNo == CLDRFile.TERRITORY_NAME) {
   1467                 allowDuplicates.put(distinguishingPath, code);
   1468             }
   1469         }
   1470 
   1471         private static void addFallbackCode(String fullpath, String value, String alt) { // assumes no allowDuplicates for this
   1472             addFallbackCodeToConstructedItems(fullpath, value, alt); // ignore unneeded return value
   1473         }
   1474 
   1475         private static String addFallbackCodeToConstructedItems(String fullpath, String value, String alt) {
   1476             if (alt != null) {
   1477                 // Insert the @alt= string after the last occurrence of "]"
   1478                 StringBuffer fullpathBuf = new StringBuffer(fullpath);
   1479                 fullpath = fullpathBuf.insert(fullpathBuf.lastIndexOf("]") + 1, "[@alt=\"" + alt + "\"]").toString();
   1480             }
   1481             // System.out.println(fullpath + "\t=> " + code);
   1482             return constructedItems.putValueAtPath(fullpath, value);
   1483         }
   1484 
   1485         @Override
   1486         public boolean isHere(String path) {
   1487             return currentSource.isHere(path); // only test one level
   1488         }
   1489 
   1490         @Override
   1491         public void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result) {
   1492             // NOTE: No caching is currently performed here because the unresolved
   1493             // locales already cache their value-path mappings, and it's not
   1494             // clear yet how much further caching would speed this up.
   1495 
   1496             // Add all non-aliased paths with the specified value.
   1497             List<XMLSource> children = new ArrayList<XMLSource>();
   1498             Set<String> filteredPaths = new HashSet<String>();
   1499             for (XMLSource source : sources.values()) {
   1500                 Set<String> pathsWithValue = new HashSet<String>();
   1501                 source.getPathsWithValue(valueToMatch, pathPrefix, pathsWithValue);
   1502                 // Don't add a path with the value if it is overridden by a child locale.
   1503                 for (String pathWithValue : pathsWithValue) {
   1504                     if (!sourcesHavePath(pathWithValue, children)) {
   1505                         filteredPaths.add(pathWithValue);
   1506                     }
   1507                 }
   1508                 children.add(source);
   1509             }
   1510 
   1511             // Find all paths that alias to the specified value, then filter by
   1512             // path prefix.
   1513             Set<String> aliases = new HashSet<String>();
   1514             Set<String> oldAliases = new HashSet<String>(filteredPaths);
   1515             Set<String> newAliases;
   1516             do {
   1517                 String[] sortedPaths = new String[oldAliases.size()];
   1518                 oldAliases.toArray(sortedPaths);
   1519                 Arrays.sort(sortedPaths);
   1520                 newAliases = getDirectAliases(sortedPaths);
   1521                 oldAliases = newAliases;
   1522                 aliases.addAll(newAliases);
   1523             } while (newAliases.size() > 0);
   1524 
   1525             // get the aliases, but only the ones that have values that match
   1526             String norm = null;
   1527             for (String alias : aliases) {
   1528                 if (alias.startsWith(pathPrefix)) {
   1529                     if (norm == null) {
   1530                         norm = SimpleXMLSource.normalize(valueToMatch);
   1531                     }
   1532                     String value = getValueAtDPath(alias);
   1533                     if (SimpleXMLSource.normalize(value).equals(norm)) {
   1534                         filteredPaths.add(alias);
   1535                     }
   1536                 }
   1537             }
   1538 
   1539             result.addAll(filteredPaths);
   1540         }
   1541 
   1542         private boolean sourcesHavePath(String xpath, List<XMLSource> sources) {
   1543             for (XMLSource source : sources) {
   1544                 if (source.hasValueAtDPath(xpath)) return true;
   1545             }
   1546             return false;
   1547         }
   1548 
   1549         @Override
   1550         public VersionInfo getDtdVersionInfo() {
   1551             return currentSource.getDtdVersionInfo();
   1552         }
   1553     }
   1554 
   1555     /**
   1556      * See CLDRFile isWinningPath for documentation
   1557      *
   1558      * @param path
   1559      * @return
   1560      */
   1561     public boolean isWinningPath(String path) {
   1562         return getWinningPath(path).equals(path);
   1563     }
   1564 
   1565     /**
   1566      * See CLDRFile getWinningPath for documentation.
   1567      * Default implementation is that it removes draft and [@alt="...proposed..." if possible
   1568      *
   1569      * @param path
   1570      * @return
   1571      */
   1572     public String getWinningPath(String path) {
   1573         String newPath = CLDRFile.getNondraftNonaltXPath(path);
   1574         if (!newPath.equals(path)) {
   1575             String value = getValueAtPath(newPath); // ensure that it still works
   1576             if (value != null) {
   1577                 return newPath;
   1578             }
   1579         }
   1580         return path;
   1581     }
   1582 
   1583     /**
   1584      * Adds a listener to this XML source.
   1585      */
   1586     public void addListener(Listener listener) {
   1587         listeners.add(new WeakReference<Listener>(listener));
   1588     }
   1589 
   1590     /**
   1591      * Notifies all listeners that a change has occurred. This method should be
   1592      * called by the XMLSource being updated after any change
   1593      * (usually in putValueAtDPath() and removeValueAtDPath()).
   1594      * This should only be called by XMLSource / CLDRFile
   1595      *
   1596      * @param xpath
   1597      *            the xpath where the change occurred.
   1598      */
   1599     protected void notifyListeners(String xpath) {
   1600         int i = 0;
   1601         while (i < listeners.size()) {
   1602             Listener listener = listeners.get(i).get();
   1603             if (listener == null) { // listener has been garbage-collected.
   1604                 listeners.remove(i);
   1605             } else {
   1606                 listener.valueChanged(xpath, this);
   1607                 i++;
   1608             }
   1609         }
   1610     }
   1611 
   1612     /**
   1613      * return true if the path in this file (without resolution). Default implementation is to just see if the path has
   1614      * a value.
   1615      * The resolved source must just test the top level.
   1616      *
   1617      * @param path
   1618      * @return
   1619      */
   1620     public boolean isHere(String path) {
   1621         return getValueAtPath(path) != null;
   1622     }
   1623 
   1624     /**
   1625      * Find all the distinguished paths having values matching valueToMatch, and add them to result.
   1626      *
   1627      * @param valueToMatch
   1628      * @param pathPrefix
   1629      * @param result
   1630      */
   1631     public abstract void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result);
   1632 
   1633     public VersionInfo getDtdVersionInfo() {
   1634         return null;
   1635     }
   1636 
   1637     public String getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) {
   1638         return null; // only a resolving xmlsource will return a value
   1639     }
   1640 
   1641     // HACK, should be field on XMLSource
   1642     public DtdType getDtdType() {
   1643         final Iterator<String> it = iterator();
   1644         if (it.hasNext()) {
   1645             String path = it.next();
   1646             return DtdType.fromPath(path);
   1647         }
   1648         return null;
   1649     }
   1650 }
   1651