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