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