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