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