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