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