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