• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AliasLocation(String pathWhereFound, String localeWhereFound)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 
getLocaleID()69     public String getLocaleID() {
70         return localeID;
71     }
72 
setLocaleID(String localeID)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      */
putAll(Map<String, String> tempMap, int conflict_resolution)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      */
putAll(XMLSource otherSource, int conflict_resolution)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      */
removeAll(Collection<String> xpaths)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      */
isDraft(String path)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 
isFrozen()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      */
putValueAtPath(String xpath, String value)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 
getPathsAllowingDuplicates()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          */
valueChanged(String xpath, XMLSource source)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 
make(String aliasPath)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          */
Alias(int pos, String oldPath, String newPath, String aliasParts)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          */
addRelative(String oldPath, String relativePath)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 
stripLastElement(String oldPath)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 
toString()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          */
changeNewToOld(String fullPath, String newPath, String oldPath)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 
getOldPath()395         public String getOldPath() {
396             return oldPath;
397         }
398 
getNewLocaleID()399         public String getNewLocaleID() {
400             return newLocaleID;
401         }
402 
getNewPath()403         public String getNewPath() {
404             return newPath;
405         }
406 
composeNewAndOldPath(String path)407         public String composeNewAndOldPath(String path) {
408             return newPath + path.substring(oldPath.length());
409         }
410 
composeOldAndNewPath(String path)411         public String composeOldAndNewPath(String path) {
412             return oldPath + path.substring(newPath.length());
413         }
414 
pathsEqual()415         public boolean pathsEqual() {
416             return pathsEqual;
417         }
418 
isAliasPath(String path)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      */
getAliases()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      */
getReverseAliases()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      */
clearCache()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      */
getSourceLocaleID(String path, CLDRFile.Status status)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      */
removeValueAtPath(String xpath)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      */
getValueAtPath(String xpath)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      */
getFullPath(String xpath)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      */
putFullPathAtDPath(String distinguishingXPath, String fullxpath)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      */
putValueAtDPath(String distinguishingXPath, String value)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      */
removeValueAtDPath(String distinguishingXPath)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      */
getValueAtDPath(String path)553     abstract public String getValueAtDPath(String path);
554 
hasValueAtDPath(String path)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      */
getChangeDateAtDPath(String path)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      */
getFullPathAtDPath(String path)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      */
getXpathComments()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      */
setXpathComments(Comments comments)587     abstract public void setXpathComments(Comments comments);
588 
589     /**
590      * @return an iterator over the distinguished paths
591      */
iterator()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      */
iterator(String prefix)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 
iterator(Matcher pathFilter)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      */
isResolving()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      */
getUnresolving()620     public XMLSource getUnresolving() {
621         return this;
622     }
623 
624     /**
625      * SUBCLASSING: must be overridden
626      */
cloneAsThawed()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      */
toString()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      */
toString(String regex)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      */
isNonInheriting()670     public boolean isNonInheriting() {
671         return nonInheriting;
672     }
673 
674     /**
675      * @return sets whether supplemental. Normally only called internall.
676      */
setNonInheriting(boolean nonInheriting)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 
isResolving()692         public boolean isResolving() {
693             return true;
694         }
695 
getUnresolving()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 
getValueAtDPath(String xpath)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 
getSource(AliasLocation fullStatus)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 
getFullPathAtDPath(String xpath)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
getChangeDateAtDPath(String xpath)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 
getFullPath(String xpath, AliasLocation fullStatus, String fullPathWhereFound)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
getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound)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 
getCachedFullStatus(String xpath)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 
getWinningPath(String xpath)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 
getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status)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 
getPathLocation(String xpath, boolean skipFirst)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          */
fillKeys()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          */
findNonAliasedPaths()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          */
getDirectAliases(String[] paths)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 
getReverseAliases()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          */
iterator()1117         public Iterator<String> iterator() {
1118             return getCachedKeySet().iterator();
1119         }
1120 
getCachedKeySet()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 
putFullPathAtDPath(String distinguishingXPath, String fullxpath)1131         public void putFullPathAtDPath(String distinguishingXPath, String fullxpath) {
1132             throw new UnsupportedOperationException("Resolved CLDRFiles are read-only");
1133         }
1134 
putValueAtDPath(String distinguishingXPath, String value)1135         public void putValueAtDPath(String distinguishingXPath, String value) {
1136             throw new UnsupportedOperationException("Resolved CLDRFiles are read-only");
1137         }
1138 
getXpathComments()1139         public Comments getXpathComments() {
1140             return currentSource.getXpathComments();
1141         }
1142 
setXpathComments(Comments path)1143         public void setXpathComments(Comments path) {
1144             throw new UnsupportedOperationException("Resolved CLDRFiles are read-only");
1145         }
1146 
removeValueAtDPath(String xpath)1147         public void removeValueAtDPath(String xpath) {
1148             throw new UnsupportedOperationException("Resolved CLDRFiles are  read-only");
1149         }
1150 
freeze()1151         public XMLSource freeze() {
1152             return this; // No-op. ResolvingSource is already read-only.
1153         }
1154 
1155         @Override
valueChanged(String xpath, XMLSource nonResolvingSource)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          */
ResolvingSource(List<XMLSource> sourceList)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 
getLocaleID()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                     }
addFallbackCode(typeNo, code, value)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) {
addFallbackCode(CLDRFile.LANGUAGE_NAME, extraCode, extraCode)1388                 addFallbackCode(CLDRFile.LANGUAGE_NAME, extraCode, extraCode);
1389             }
1390 
1391 
addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_GB", "en_GB", "short")1392             addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_GB", "en_GB", "short");
addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_US", "en_US", "short")1393             addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_US", "en_US", "short");
addFallbackCode(CLDRFile.LANGUAGE_NAME, "az", "az", "short")1394             addFallbackCode(CLDRFile.LANGUAGE_NAME, "az", "az", "short");
1395 
addFallbackCode(CLDRFile.SCRIPT_NAME, "Hans", "Hans", "stand-alone")1396             addFallbackCode(CLDRFile.SCRIPT_NAME, "Hans", "Hans", "stand-alone");
addFallbackCode(CLDRFile.SCRIPT_NAME, "Hant", "Hant", "stand-alone")1397             addFallbackCode(CLDRFile.SCRIPT_NAME, "Hant", "Hant", "stand-alone");
1398 
addFallbackCode(CLDRFile.TERRITORY_NAME, "GB", "GB", "short")1399             addFallbackCode(CLDRFile.TERRITORY_NAME, "GB", "GB", "short");
addFallbackCode(CLDRFile.TERRITORY_NAME, "HK", "HK", "short")1400             addFallbackCode(CLDRFile.TERRITORY_NAME, "HK", "HK", "short");
addFallbackCode(CLDRFile.TERRITORY_NAME, "MO", "MO", "short")1401             addFallbackCode(CLDRFile.TERRITORY_NAME, "MO", "MO", "short");
addFallbackCode(CLDRFile.TERRITORY_NAME, "PS", "PS", "short")1402             addFallbackCode(CLDRFile.TERRITORY_NAME, "PS", "PS", "short");
addFallbackCode(CLDRFile.TERRITORY_NAME, "US", "US", "short")1403             addFallbackCode(CLDRFile.TERRITORY_NAME, "US", "US", "short");
1404 
addFallbackCode(CLDRFile.TERRITORY_NAME, "CD", "CD", "variant")1405             addFallbackCode(CLDRFile.TERRITORY_NAME, "CD", "CD", "variant"); // add other geopolitical items
addFallbackCode(CLDRFile.TERRITORY_NAME, "CG", "CG", "variant")1406             addFallbackCode(CLDRFile.TERRITORY_NAME, "CG", "CG", "variant");
addFallbackCode(CLDRFile.TERRITORY_NAME, "CI", "CI", "variant")1407             addFallbackCode(CLDRFile.TERRITORY_NAME, "CI", "CI", "variant");
addFallbackCode(CLDRFile.TERRITORY_NAME, "CZ", "CZ", "variant")1408             addFallbackCode(CLDRFile.TERRITORY_NAME, "CZ", "CZ", "variant");
addFallbackCode(CLDRFile.TERRITORY_NAME, "FK", "FK", "variant")1409             addFallbackCode(CLDRFile.TERRITORY_NAME, "FK", "FK", "variant");
addFallbackCode(CLDRFile.TERRITORY_NAME, "MK", "MK", "variant")1410             addFallbackCode(CLDRFile.TERRITORY_NAME, "MK", "MK", "variant");
addFallbackCode(CLDRFile.TERRITORY_NAME, "TL", "TL", "variant")1411             addFallbackCode(CLDRFile.TERRITORY_NAME, "TL", "TL", "variant");
1412 
addFallbackCode(CLDRFile.TERRITORY_NAME, "XA", "XA")1413             addFallbackCode(CLDRFile.TERRITORY_NAME, "XA", "XA");
addFallbackCode(CLDRFile.TERRITORY_NAME, "XB", "XB")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 
constructedItems.freeze()1453             constructedItems.freeze();
1454             allowDuplicates = Collections.unmodifiableMap(allowDuplicates);
1455             // System.out.println("constructedItems: " + constructedItems);
1456         }
1457 
addFallbackCode(int typeNo, String code, String value)1458         private static void addFallbackCode(int typeNo, String code, String value) {
1459             addFallbackCode(typeNo, code, value, null);
1460         }
1461 
addFallbackCode(int typeNo, String code, String value, String alt)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 
addFallbackCode(String fullpath, String value, String alt)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 
addFallbackCodeToConstructedItems(String fullpath, String value, String alt)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
isHere(String path)1486         public boolean isHere(String path) {
1487             return currentSource.isHere(path); // only test one level
1488         }
1489 
1490         @Override
getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result)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 
sourcesHavePath(String xpath, List<XMLSource> sources)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
getDtdVersionInfo()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      */
isWinningPath(String path)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      */
getWinningPath(String path)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      */
addListener(Listener listener)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      */
notifyListeners(String xpath)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      */
isHere(String path)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      */
getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result)1631     public abstract void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result);
1632 
getDtdVersionInfo()1633     public VersionInfo getDtdVersionInfo() {
1634         return null;
1635     }
1636 
getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound)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
getDtdType()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