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