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