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