1 /* 2 ****************************************************************************** 3 * Copyright (C) 2005-2011, International Business Machines Corporation and * 4 * others. All Rights Reserved. * 5 ****************************************************************************** 6 */ 7 8 package org.unicode.cldr.util; 9 10 import java.lang.ref.WeakReference; 11 import java.util.ArrayList; 12 import java.util.Arrays; 13 import java.util.Collection; 14 import java.util.Collections; 15 import java.util.Date; 16 import java.util.HashMap; 17 import java.util.HashSet; 18 import java.util.Iterator; 19 import java.util.LinkedHashMap; 20 import java.util.List; 21 import java.util.Map; 22 import java.util.Set; 23 import java.util.TreeMap; 24 import java.util.WeakHashMap; 25 import java.util.regex.Matcher; 26 import java.util.regex.Pattern; 27 28 import org.unicode.cldr.util.XPathParts.Comments; 29 30 import com.ibm.icu.impl.Utility; 31 import com.ibm.icu.util.Freezable; 32 import com.ibm.icu.util.Output; 33 import com.ibm.icu.util.VersionInfo; 34 35 /** 36 * Overall process is described in 37 * http://cldr.unicode.org/development/development-process/design-proposals/resolution-of-cldr-files. Please update that 38 * document if major 39 * changes are made. 40 */ 41 public abstract class XMLSource implements Freezable<XMLSource>, Iterable<String> { 42 public static final String CODE_FALLBACK_ID = "code-fallback"; 43 public static final String ROOT_ID = "root"; 44 public static final boolean USE_PARTS_IN_ALIAS = false; 45 private static final String TRACE_INDENT = " "; // "\t" 46 private transient XPathParts parts = new XPathParts(null, null); 47 private static Map<String, String> allowDuplicates = new HashMap<String, String>(); 48 49 private String localeID; 50 private boolean nonInheriting; 51 private TreeMap<String, String> aliases; 52 private LinkedHashMap<String, List<String>> reverseAliases; 53 protected boolean locked; 54 transient String[] fixedPath = new String[1]; 55 56 public static class AliasLocation { 57 public final String pathWhereFound; 58 public final String localeWhereFound; 59 AliasLocation(String pathWhereFound, String localeWhereFound)60 public AliasLocation(String pathWhereFound, String localeWhereFound) { 61 this.pathWhereFound = pathWhereFound; 62 this.localeWhereFound = localeWhereFound; 63 } 64 } 65 66 // Listeners are stored using weak references so that they can be garbage collected. 67 private List<WeakReference<Listener>> listeners = new ArrayList<WeakReference<Listener>>(); 68 getLocaleID()69 public String getLocaleID() { 70 return localeID; 71 } 72 setLocaleID(String localeID)73 public void setLocaleID(String localeID) { 74 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 75 this.localeID = localeID; 76 } 77 78 /** 79 * Adds all the path,value pairs in tempMap. 80 * The paths must be Full Paths. 81 * 82 * @param tempMap 83 * @param conflict_resolution 84 */ putAll(Map<String, String> tempMap, int conflict_resolution)85 public void putAll(Map<String, String> tempMap, int conflict_resolution) { 86 for (Iterator<String> it = tempMap.keySet().iterator(); it.hasNext();) { 87 String path = it.next(); 88 if (conflict_resolution == CLDRFile.MERGE_KEEP_MINE && getValueAtPath(path) != null) continue; 89 putValueAtPath(path, tempMap.get(path)); 90 } 91 } 92 93 /** 94 * Adds all the path, value pairs in otherSource. 95 * 96 * @param otherSource 97 * @param conflict_resolution 98 */ putAll(XMLSource otherSource, int conflict_resolution)99 public void putAll(XMLSource otherSource, int conflict_resolution) { 100 for (Iterator<String> it = otherSource.iterator(); it.hasNext();) { 101 String path = it.next(); 102 final String oldValue = getValueAtDPath(path); 103 if (conflict_resolution == CLDRFile.MERGE_KEEP_MINE && oldValue != null) { 104 continue; 105 } 106 final String newValue = otherSource.getValueAtDPath(path); 107 if (newValue.equals(oldValue)) { 108 continue; 109 } 110 putValueAtPath(otherSource.getFullPathAtDPath(path), newValue); 111 } 112 } 113 114 /** 115 * Removes all the paths in the collection. 116 * WARNING: must be distinguishedPaths 117 * 118 * @param xpaths 119 */ removeAll(Collection<String> xpaths)120 public void removeAll(Collection<String> xpaths) { 121 for (Iterator<String> it = xpaths.iterator(); it.hasNext();) { 122 removeValueAtDPath(it.next()); 123 } 124 } 125 126 /** 127 * Tests whether the full path for this dpath is draft or now. 128 * 129 * @param path 130 * @return 131 */ isDraft(String path)132 public boolean isDraft(String path) { 133 String fullpath = getFullPath(path); 134 if (path == null) return false; 135 if (fullpath.indexOf("[@draft=") < 0) return false; 136 return parts.set(fullpath).containsAttribute("draft"); 137 } 138 isFrozen()139 public boolean isFrozen() { 140 return locked; 141 } 142 143 /** 144 * Adds the path,value pair. The path must be full path. 145 * 146 * @param xpath 147 * @param value 148 */ putValueAtPath(String xpath, String value)149 public String putValueAtPath(String xpath, String value) { 150 if (locked) { 151 throw new UnsupportedOperationException("Attempt to modify locked object"); 152 } 153 String distinguishingXPath = CLDRFile.getDistinguishingXPath(xpath, fixedPath, nonInheriting); 154 putValueAtDPath(distinguishingXPath, value); 155 if (!fixedPath[0].equals(distinguishingXPath)) { 156 clearCache(); 157 putFullPathAtDPath(distinguishingXPath, fixedPath[0]); 158 } 159 return distinguishingXPath; 160 } 161 162 /** 163 * Gets those paths that allow duplicates 164 */ 165 getPathsAllowingDuplicates()166 public static Map<String, String> getPathsAllowingDuplicates() { 167 return allowDuplicates; 168 } 169 170 /** 171 * A listener for XML source data. 172 */ 173 public static interface Listener { 174 /** 175 * Called whenever the source being listened to has a data change. 176 * 177 * @param xpath 178 * The xpath that had its value changed. 179 * @param source 180 * back-pointer to the source that changed 181 */ valueChanged(String xpath, XMLSource source)182 public void valueChanged(String xpath, XMLSource source); 183 } 184 185 /** 186 * Internal class. Immutable! 187 */ 188 public static final class Alias { 189 // public String oldLocaleID; 190 final private String newLocaleID; 191 final private String oldPath; 192 final private String newPath; 193 final private boolean pathsEqual; 194 static final Pattern aliasPattern = Pattern 195 .compile("(?:\\[@source=\"([^\"]*)\"])?(?:\\[@path=\"([^\"]*)\"])?(?:\\[@draft=\"([^\"]*)\"])?"); // constant, 196 197 // so no 198 // need to 199 // sync 200 make(String aliasPath)201 public static Alias make(String aliasPath) { 202 int pos = aliasPath.indexOf("/alias"); 203 if (pos < 0) return null; // quickcheck 204 String aliasParts = aliasPath.substring(pos + 6); 205 String oldPath = aliasPath.substring(0, pos); 206 String newPath = null; 207 208 return new Alias(pos, oldPath, newPath, aliasParts); 209 } 210 211 /** 212 * @param newLocaleID 213 * @param oldPath 214 * @param aliasParts 215 * @param newPath 216 * @param pathsEqual 217 */ Alias(int pos, String oldPath, String newPath, String aliasParts)218 private Alias(int pos, String oldPath, String newPath, String aliasParts) { 219 // if (USE_PARTS_IN_ALIAS) { 220 // XPathParts tempAliasParts = new XPathParts(null, null); 221 // if (!tempAliasParts.set(aliasPath).containsElement("alias")) { 222 // return null; 223 // } 224 // Map attributes = tempAliasParts.getAttributes(tempAliasParts.size()-1); 225 // result.newLocaleID = (String) attributes.get("source"); 226 // relativePath = (String) attributes.get("path"); 227 // if (result.newLocaleID != null && result.newLocaleID.equals("locale")) { 228 // result.newLocaleID = null; 229 // } 230 // if (relativePath == null) { 231 // result.newPath = result.oldPath; 232 // } 233 // else { 234 // result.newPath = tempAliasParts.trimLast().addRelative(relativePath).toString(); 235 // } 236 // } else { 237 // do the same as the above with a regex 238 Matcher matcher = aliasPattern.matcher(aliasParts); 239 if (!matcher.matches()) { 240 throw new IllegalArgumentException("bad alias pattern for " + aliasParts); 241 } 242 String newLocaleID = matcher.group(1); 243 if (newLocaleID != null && newLocaleID.equals("locale")) { 244 newLocaleID = null; 245 } 246 String relativePath2 = matcher.group(2); 247 if (newPath == null) { 248 newPath = oldPath; 249 } 250 if (relativePath2 != null) { 251 newPath = addRelative(newPath, relativePath2); 252 } 253 254 // if (false) { // test 255 // if (newLocaleID != null) { 256 // if (!newLocaleID.equals(result.newLocaleID)) { 257 // throw new IllegalArgumentException(); 258 // } 259 // } else if (result.newLocaleID != null) { 260 // throw new IllegalArgumentException(); 261 // } 262 // if (!relativePath2.equals(relativePath)) { 263 // throw new IllegalArgumentException(); 264 // } 265 // if (!newPath.equals(result.newPath)) { 266 // throw new IllegalArgumentException(); 267 // } 268 // } 269 // } 270 271 boolean pathsEqual = oldPath.equals(newPath); 272 273 if (pathsEqual && newLocaleID == null) { 274 throw new IllegalArgumentException("Alias must have different path or different source. AliasPath: " 275 + aliasParts 276 + ", Alias: " + newPath + ", " + newLocaleID); 277 } 278 279 this.newLocaleID = newLocaleID; 280 this.oldPath = oldPath; 281 this.newPath = newPath; 282 this.pathsEqual = pathsEqual; 283 } 284 285 /** 286 * Create a new path from an old path + relative portion. 287 * Basically, each ../ at the front of the relative portion removes a trailing 288 * element+attributes from the old path. 289 * WARNINGS: 290 * 1. It could fail if an attribute value contains '/'. This should not be the 291 * case except in alias elements, but need to verify. 292 * 2. Also assumes that there are no extra /'s in the relative or old path. 293 * 3. If we verified that the relative paths always used " in place of ', 294 * we could also save a step. 295 * 296 * Maybe we could clean up #2 and #3 when reading in a CLDRFile the first time? 297 * 298 * @param oldPath 299 * @param relativePath 300 * @return 301 */ addRelative(String oldPath, String relativePath)302 static String addRelative(String oldPath, String relativePath) { 303 if (relativePath.startsWith("//")) { 304 return relativePath; 305 } 306 while (relativePath.startsWith("../")) { 307 relativePath = relativePath.substring(3); 308 // strip extra "/". Shouldn't occur, but just to be safe. 309 while (relativePath.startsWith("/")) { 310 relativePath = relativePath.substring(1); 311 } 312 // strip last element 313 oldPath = stripLastElement(oldPath); 314 } 315 return oldPath + "/" + relativePath.replace('\'', '"'); 316 } 317 318 // static final String ATTRIBUTE_PATTERN = "\\[@([^=]+)=\"([^\"]*)\"]"; 319 static final Pattern MIDDLE_OF_ATTRIBUTE_VALUE = PatternCache.get("[^\"]*\"\\]"); 320 stripLastElement(String oldPath)321 public static String stripLastElement(String oldPath) { 322 int oldPos = oldPath.lastIndexOf('/'); 323 // verify that we are not in the middle of an attribute value 324 Matcher verifyElement = MIDDLE_OF_ATTRIBUTE_VALUE.matcher(oldPath.substring(oldPos)); 325 while (verifyElement.lookingAt()) { 326 oldPos = oldPath.lastIndexOf('/', oldPos - 1); 327 // will throw exception if we didn't find anything 328 verifyElement.reset(oldPath.substring(oldPos)); 329 } 330 oldPath = oldPath.substring(0, oldPos); 331 return oldPath; 332 } 333 toString()334 public String toString() { 335 return 336 // "oldLocaleID: " + oldLocaleID + ", " + 337 "newLocaleID: " + newLocaleID + ",\t" 338 + 339 "oldPath: " + oldPath + ",\n\t" 340 + 341 "newPath: " + newPath; 342 } 343 344 /** 345 * This function is called on the full path, when we know the distinguishing path matches the oldPath. 346 * So we just want to modify the base of the path 347 * 348 * @param oldPath 349 * @param newPath 350 * @param result 351 * @return 352 */ changeNewToOld(String fullPath, String newPath, String oldPath)353 public String changeNewToOld(String fullPath, String newPath, String oldPath) { 354 // do common case quickly 355 if (fullPath.startsWith(newPath)) { 356 return oldPath + fullPath.substring(newPath.length()); 357 } 358 359 // fullPath will be the same as newPath, except for some attributes at the end. 360 // add those attributes to oldPath, starting from the end. 361 XPathParts partsOld = new XPathParts(); 362 XPathParts partsNew = new XPathParts(); 363 XPathParts partsFull = new XPathParts(); 364 partsOld.set(oldPath); 365 partsNew.set(newPath); 366 partsFull.set(fullPath); 367 Map<String, String> attributesFull = partsFull.getAttributes(-1); 368 Map<String, String> attributesNew = partsNew.getAttributes(-1); 369 Map<String, String> attributesOld = partsOld.getAttributes(-1); 370 for (Iterator<String> it = attributesFull.keySet().iterator(); it.hasNext();) { 371 String attribute = it.next(); 372 if (attributesNew.containsKey(attribute)) continue; 373 attributesOld.put(attribute, attributesFull.get(attribute)); 374 } 375 String result = partsOld.toString(); 376 377 // for now, just assume check that there are no goofy bits 378 // if (!fullPath.startsWith(newPath)) { 379 // if (false) { 380 // throw new IllegalArgumentException("Failure to fix path. " 381 // + Utility.LINE_SEPARATOR + "\tfullPath: " + fullPath 382 // + Utility.LINE_SEPARATOR + "\toldPath: " + oldPath 383 // + Utility.LINE_SEPARATOR + "\tnewPath: " + newPath 384 // ); 385 // } 386 // String tempResult = oldPath + fullPath.substring(newPath.length()); 387 // if (!result.equals(tempResult)) { 388 // System.err.println("fullPath: " + fullPath + Utility.LINE_SEPARATOR + "\toldPath: " 389 // + oldPath + Utility.LINE_SEPARATOR + "\tnewPath: " + newPath 390 // + Utility.LINE_SEPARATOR + "\tnewPath: " + result); 391 // } 392 return result; 393 } 394 getOldPath()395 public String getOldPath() { 396 return oldPath; 397 } 398 getNewLocaleID()399 public String getNewLocaleID() { 400 return newLocaleID; 401 } 402 getNewPath()403 public String getNewPath() { 404 return newPath; 405 } 406 composeNewAndOldPath(String path)407 public String composeNewAndOldPath(String path) { 408 return newPath + path.substring(oldPath.length()); 409 } 410 composeOldAndNewPath(String path)411 public String composeOldAndNewPath(String path) { 412 return oldPath + path.substring(newPath.length()); 413 } 414 pathsEqual()415 public boolean pathsEqual() { 416 return pathsEqual; 417 } 418 isAliasPath(String path)419 public static boolean isAliasPath(String path) { 420 return path.contains("/alias"); 421 } 422 } 423 424 /** 425 * This method should be overridden. 426 * 427 * @return a mapping of paths to their aliases. Note that since root is the 428 * only locale to have aliases, all other locales will have no mappings. 429 */ getAliases()430 protected synchronized TreeMap<String, String> getAliases() { 431 // The cache assumes that aliases will never change over the lifetime of 432 // an XMLSource. 433 if (aliases == null) { 434 aliases = new TreeMap<String, String>(); 435 // Look for aliases and create mappings for them. 436 // Aliases are only ever found in root. 437 for (String path : this) { 438 if (!Alias.isAliasPath(path)) continue; 439 String fullPath = getFullPathAtDPath(path); 440 Alias temp = Alias.make(fullPath); 441 if (temp == null) continue; 442 aliases.put(temp.getOldPath(), temp.getNewPath()); 443 } 444 } 445 return aliases; 446 } 447 448 /** 449 * @return a reverse mapping of aliases 450 */ getReverseAliases()451 private LinkedHashMap<String, List<String>> getReverseAliases() { 452 if (reverseAliases != null) return reverseAliases; 453 // Aliases are only ever found in root. 454 Map<String, String> aliases = getAliases(); 455 Map<String, List<String>> reverse = new HashMap<String, List<String>>(); 456 for (Map.Entry<String, String> entry : aliases.entrySet()) { 457 List<String> list = reverse.get(entry.getValue()); 458 if (list == null) { 459 list = new ArrayList<String>(); 460 reverse.put(entry.getValue(), list); 461 } 462 list.add(entry.getKey()); 463 } 464 465 // Sort map. 466 reverseAliases = new LinkedHashMap<String, List<String>>(new TreeMap<String, List<String>>(reverse)); 467 return reverseAliases; 468 } 469 470 /** 471 * Clear any internal caches. 472 */ clearCache()473 private void clearCache() { 474 aliases = null; 475 } 476 477 /** 478 * Return the localeID of the XMLSource where the path was found 479 * SUBCLASSING: must be overridden in a resolving locale 480 * 481 * @param path 482 * @param status 483 * TODO 484 * @return 485 */ getSourceLocaleID(String path, CLDRFile.Status status)486 public String getSourceLocaleID(String path, CLDRFile.Status status) { 487 if (status != null) { 488 status.pathWhereFound = CLDRFile.getDistinguishingXPath(path, null, false); 489 } 490 return getLocaleID(); 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, nonInheriting)); 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, nonInheriting)); 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, nonInheriting)); 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 */ iterator()592 abstract public Iterator<String> iterator(); 593 594 /** 595 * @return an iterator over the distinguished paths that start with the prefix. 596 * SUBCLASSING: Normally overridden for efficiency 597 */ iterator(String prefix)598 public Iterator<String> iterator(String prefix) { 599 if (prefix == null || prefix.length() == 0) return iterator(); 600 return new com.ibm.icu.dev.util.CollectionUtilities.PrefixIterator().set(iterator(), prefix); 601 } 602 iterator(Matcher pathFilter)603 public Iterator<String> iterator(Matcher pathFilter) { 604 if (pathFilter == null) return iterator(); 605 return new com.ibm.icu.dev.util.CollectionUtilities.RegexIterator().set(iterator(), pathFilter); 606 } 607 608 /** 609 * @return returns whether resolving or not 610 * SUBCLASSING: Only changed for resolving subclasses 611 */ isResolving()612 public boolean isResolving() { 613 return false; 614 } 615 616 /** 617 * Returns the unresolved version of this XMLSource. 618 * SUBCLASSING: Override in resolving sources. 619 */ getUnresolving()620 public XMLSource getUnresolving() { 621 return this; 622 } 623 624 /** 625 * SUBCLASSING: must be overridden 626 */ cloneAsThawed()627 public XMLSource cloneAsThawed() { 628 try { 629 XMLSource result = (XMLSource) super.clone(); 630 result.locked = false; 631 return result; 632 } catch (CloneNotSupportedException e) { 633 throw new InternalError("should never happen"); 634 } 635 } 636 637 /** 638 * for debugging only 639 */ toString()640 public String toString() { 641 StringBuffer result = new StringBuffer(); 642 for (Iterator<String> it = iterator(); it.hasNext();) { 643 String path = it.next(); 644 String value = getValueAtDPath(path); 645 String fullpath = getFullPathAtDPath(path); 646 result.append(fullpath).append(" =\t ").append(value).append(CldrUtility.LINE_SEPARATOR); 647 } 648 return result.toString(); 649 } 650 651 /** 652 * for debugging only 653 */ toString(String regex)654 public String toString(String regex) { 655 Matcher matcher = PatternCache.get(regex).matcher(""); 656 StringBuffer result = new StringBuffer(); 657 for (Iterator<String> it = iterator(matcher); it.hasNext();) { 658 String path = it.next(); 659 // if (!matcher.reset(path).matches()) continue; 660 String value = getValueAtDPath(path); 661 String fullpath = getFullPathAtDPath(path); 662 result.append(fullpath).append(" =\t ").append(value).append(CldrUtility.LINE_SEPARATOR); 663 } 664 return result.toString(); 665 } 666 667 /** 668 * @return returns whether supplemental or not 669 */ isNonInheriting()670 public boolean isNonInheriting() { 671 return nonInheriting; 672 } 673 674 /** 675 * @return sets whether supplemental. Normally only called internall. 676 */ setNonInheriting(boolean nonInheriting)677 public void setNonInheriting(boolean nonInheriting) { 678 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 679 this.nonInheriting = nonInheriting; 680 } 681 682 /** 683 * Internal class for doing resolution 684 * 685 * @author davis 686 * 687 */ 688 public static class ResolvingSource extends XMLSource implements Listener { 689 private XMLSource currentSource; 690 private LinkedHashMap<String, XMLSource> sources; 691 isResolving()692 public boolean isResolving() { 693 return true; 694 } 695 getUnresolving()696 public XMLSource getUnresolving() { 697 return sources.get(getLocaleID()); 698 } 699 700 /* 701 * If there is an alias, then inheritance gets tricky. 702 * If there is a path //ldml/xyz/.../uvw/alias[@path=...][@source=...] 703 * then the parent for //ldml/xyz/.../uvw/abc/.../def/ 704 * is source, and the path to search for is really: //ldml/xyz/.../uvw/path/abc/.../def/ 705 */ 706 public static final boolean TRACE_VALUE = CldrUtility.getProperty("TRACE_VALUE", false);; 707 708 // Map<String,String> getValueAtDPathCache = new HashMap(); 709 getValueAtDPath(String xpath)710 public String getValueAtDPath(String xpath) { 711 if (DEBUG_PATH != null && DEBUG_PATH.matcher(xpath).find()) { 712 System.out.println("Getting value for Path: " + xpath); 713 } 714 if (TRACE_VALUE) System.out.println("\t*xpath: " + xpath 715 + CldrUtility.LINE_SEPARATOR + "\t*source: " + currentSource.getClass().getName() 716 + CldrUtility.LINE_SEPARATOR + "\t*locale: " + currentSource.getLocaleID()); 717 String result = null; 718 AliasLocation fullStatus = getCachedFullStatus(xpath); 719 if (fullStatus != null) { 720 if (TRACE_VALUE) { 721 System.out.println("\t*pathWhereFound: " + fullStatus.pathWhereFound); 722 System.out.println("\t*localeWhereFound: " + fullStatus.localeWhereFound); 723 } 724 result = getSource(fullStatus).getValueAtDPath(fullStatus.pathWhereFound); 725 } 726 if (TRACE_VALUE) System.out.println("\t*value: " + result); 727 return result; 728 } 729 getSource(AliasLocation fullStatus)730 public XMLSource getSource(AliasLocation fullStatus) { 731 XMLSource source = sources.get(fullStatus.localeWhereFound); 732 return source == null ? constructedItems : source; 733 } 734 735 // public String _getValueAtDPath(String xpath) { 736 // XMLSource currentSource = mySource; 737 // String result; 738 // ParentAndPath parentAndPath = new ParentAndPath(); 739 // 740 // parentAndPath.set(xpath, currentSource, getLocaleID()).next(); 741 // while (true) { 742 // if (parentAndPath.parentID == null) { 743 // return constructedItems.getValueAtDPath(xpath); 744 // } 745 // currentSource = make(parentAndPath.parentID); // factory.make(parentAndPath.parentID, false).dataSource; 746 // if (TRACE_VALUE) System.out.println("xpath: " + parentAndPath.path 747 // + Utility.LINE_SEPARATOR + "\tsource: " + currentSource.getClass().getName() 748 // + Utility.LINE_SEPARATOR + "\tlocale: " + currentSource.getLocaleID() 749 // ); 750 // result = currentSource.getValueAtDPath(parentAndPath.path); 751 // if (result != null) { 752 // if (TRACE_VALUE) System.out.println("result: " + result); 753 // return result; 754 // } 755 // parentAndPath.next(); 756 // } 757 // } 758 759 Map<String, String> getFullPathAtDPathCache = new HashMap<String, String>(); 760 getFullPathAtDPath(String xpath)761 public String getFullPathAtDPath(String xpath) { 762 String result = currentSource.getFullPathAtDPath(xpath); 763 if (result != null) { 764 return result; 765 } 766 // This is tricky. We need to find the alias location's path and full path. 767 // then we need to the the non-distinguishing elements from them, 768 // and add them into the requested path. 769 AliasLocation fullStatus = getCachedFullStatus(xpath); 770 if (fullStatus != null) { 771 String fullPathWhereFound = getSource(fullStatus).getFullPathAtDPath(fullStatus.pathWhereFound); 772 if (fullPathWhereFound == null) { 773 result = null; 774 } else if (fullPathWhereFound.equals(fullStatus.pathWhereFound)) { 775 result = xpath; // no difference 776 } else { 777 result = getFullPath(xpath, fullStatus, fullPathWhereFound); 778 } 779 } 780 // 781 // result = getFullPathAtDPathCache.get(xpath); 782 // if (result == null) { 783 // if (getCachedKeySet().contains(xpath)) { 784 // result = _getFullPathAtDPath(xpath); 785 // getFullPathAtDPathCache.put(xpath, result); 786 // } 787 // } 788 return result; 789 } 790 791 @Override getChangeDateAtDPath(String xpath)792 public Date getChangeDateAtDPath(String xpath) { 793 Date result = currentSource.getChangeDateAtDPath(xpath); 794 if (result != null) { 795 return result; 796 } 797 AliasLocation fullStatus = getCachedFullStatus(xpath); 798 if (fullStatus != null) { 799 result = getSource(fullStatus).getChangeDateAtDPath(fullStatus.pathWhereFound); 800 } 801 return result; 802 } 803 getFullPath(String xpath, AliasLocation fullStatus, String fullPathWhereFound)804 private String getFullPath(String xpath, AliasLocation fullStatus, String fullPathWhereFound) { 805 String result = getFullPathAtDPathCache.get(xpath); 806 if (result == null) { 807 // find the differences, and add them into xpath 808 // we do this by walking through each element, adding the corresponding attribute values. 809 // we add attributes FROM THE END, in case the lengths are different! 810 XPathParts xpathParts = new XPathParts().set(xpath); 811 XPathParts fullPathWhereFoundParts = new XPathParts().set(fullPathWhereFound); 812 XPathParts pathWhereFoundParts = new XPathParts().set(fullStatus.pathWhereFound); 813 int offset = xpathParts.size() - pathWhereFoundParts.size(); 814 815 for (int i = 0; i < pathWhereFoundParts.size(); ++i) { 816 Map<String, String> fullAttributes = fullPathWhereFoundParts.getAttributes(i); 817 Map<String, String> attributes = pathWhereFoundParts.getAttributes(i); 818 if (!attributes.equals(fullAttributes)) { // add differences 819 //Map<String, String> targetAttributes = xpathParts.getAttributes(i + offset); 820 for (String key : fullAttributes.keySet()) { 821 if (!attributes.containsKey(key)) { 822 String value = fullAttributes.get(key); 823 xpathParts.putAttributeValue(i + offset, key, value); 824 } 825 } 826 } 827 } 828 result = xpathParts.toString(); 829 getFullPathAtDPathCache.put(xpath, result); 830 } 831 return result; 832 } 833 834 /** 835 * Return the value that would obtain if the value didn't exist. 836 */ 837 @Override getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound)838 public String getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) { 839 AliasLocation fullStatus = getPathLocation(xpath, true); 840 if (localeWhereFound != null) { 841 localeWhereFound.value = fullStatus.localeWhereFound; 842 } 843 if (pathWhereFound != null) { 844 pathWhereFound.value = fullStatus.pathWhereFound; 845 } 846 return getSource(fullStatus).getValueAtDPath(fullStatus.pathWhereFound); 847 } 848 getCachedFullStatus(String xpath)849 private AliasLocation getCachedFullStatus(String xpath) { 850 synchronized (getSourceLocaleIDCache) { 851 AliasLocation fullStatus = getSourceLocaleIDCache.get(xpath); 852 if (fullStatus == null) { 853 fullStatus = getPathLocation(xpath, false); 854 getSourceLocaleIDCache.put(xpath, fullStatus); // cache copy 855 } 856 return fullStatus; 857 } 858 } 859 860 // private String _getFullPathAtDPath(String xpath) { 861 // String result = null; 862 // XMLSource currentSource = mySource; 863 // ParentAndPath parentAndPath = new ParentAndPath(); 864 // parentAndPath.set(xpath, currentSource, getLocaleID()).next(); 865 // while (true) { 866 // if (parentAndPath.parentID == null) { 867 // return constructedItems.getFullPathAtDPath(xpath); 868 // } 869 // currentSource = make(parentAndPath.parentID); // factory.make(parentAndPath.parentID, false).dataSource; 870 // result = currentSource.getValueAtDPath(parentAndPath.path); 871 // if (result != null) { 872 // result = currentSource.getFullPathAtDPath(parentAndPath.path); 873 // return tempAlias.changeNewToOld(result, parentAndPath.path, xpath); 874 // } 875 // parentAndPath.next(); 876 // } 877 // } 878 getWinningPath(String xpath)879 public String getWinningPath(String xpath) { 880 String result = currentSource.getWinningPath(xpath); 881 if (result != null) return result; 882 AliasLocation fullStatus = getCachedFullStatus(xpath); 883 if (fullStatus != null) { 884 result = getSource(fullStatus).getWinningPath(fullStatus.pathWhereFound); 885 } else { 886 result = xpath; 887 } 888 // 889 // result = getWinningPathCache.get(xpath); 890 // if (result == null) { 891 // if (!getCachedKeySet().contains(xpath)) { 892 // return xpath; 893 // } 894 // result = _getWinningPath(xpath); 895 // getWinningPathCache.put(xpath, result); 896 // } 897 return result; 898 } 899 900 // Map<String,String> getWinningPathCache = new HashMap(); 901 // 902 // public String _getWinningPath(String xpath) { 903 // XMLSource currentSource = mySource; 904 // ParentAndPath parentAndPath = new ParentAndPath(); 905 // parentAndPath.set(xpath, currentSource, getLocaleID()).next(); 906 // while (true) { 907 // if (parentAndPath.parentID == null) { 908 // return xpath; // ran out of parents 909 // //return constructedItems.getWinningPath(xpath); 910 // } 911 // currentSource = make(parentAndPath.parentID); // factory.make(parentAndPath.parentID, false).dataSource; 912 // String result = currentSource.getWinningPath(parentAndPath.path); 913 // if (result != null) { 914 // return result; 915 // } 916 // parentAndPath.next(); 917 // } 918 // } 919 920 private transient Map<String, AliasLocation> getSourceLocaleIDCache = new WeakHashMap<String, AliasLocation>(); 921 getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status)922 public String getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status) { 923 AliasLocation fullStatus = getCachedFullStatus(distinguishedXPath); 924 if (status != null) { 925 status.pathWhereFound = fullStatus.pathWhereFound; 926 } 927 return fullStatus.localeWhereFound; 928 } 929 930 static final Pattern COUNT_EQUALS = PatternCache.get("\\[@count=\"[^\"]*\"]"); 931 getPathLocation(String xpath, boolean skipFirst)932 private AliasLocation getPathLocation(String xpath, boolean skipFirst) { 933 for (XMLSource source : sources.values()) { 934 // allow the first source to be skipped, for george bailey value 935 if (skipFirst) { 936 skipFirst = false; 937 continue; 938 } 939 if (source.hasValueAtDPath(xpath)) { 940 String value = source.getValueAtDPath(xpath); 941 /* 942 * TODO: this looks dubious, see https://unicode.org/cldr/trac/ticket/11299 943 * 944 * Both the "immediate parent" and the "ultimate ancestor" may be of interest, as for 945 * "Jump to Original" -- does this code result in skipping the immediate parent? 946 */ 947 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 948 continue; 949 } 950 return new AliasLocation(xpath, source.getLocaleID()); 951 } 952 } 953 // Path not found, check if an alias exists 954 TreeMap<String, String> aliases = sources.get("root").getAliases(); 955 String aliasedPath = aliases.get(xpath); 956 957 if (aliasedPath == null) { 958 // Check if there is an alias for a subset xpath. 959 // If there are one or more matching aliases, lowerKey() will 960 // return the alias with the longest matching prefix since the 961 // hashmap is sorted according to xpath. 962 String possibleSubpath = aliases.lowerKey(xpath); 963 if (possibleSubpath != null && xpath.startsWith(possibleSubpath)) { 964 aliasedPath = aliases.get(possibleSubpath) + 965 xpath.substring(possibleSubpath.length()); 966 } 967 } 968 969 // counts are special; they act like there is a root alias to 'other' 970 // and in the special case of currencies, other => null 971 // //ldml/numbers/currencies/currency[@type="BRZ"]/displayName[@count="other"] => //ldml/numbers/currencies/currency[@type="BRZ"]/displayName 972 if (aliasedPath == null && xpath.contains("[@count=")) { 973 aliasedPath = COUNT_EQUALS.matcher(xpath).replaceAll("[@count=\"other\"]"); 974 if (aliasedPath.equals(xpath)) { 975 if (xpath.contains("/displayName")) { 976 aliasedPath = COUNT_EQUALS.matcher(xpath).replaceAll(""); 977 if (aliasedPath.equals(xpath)) { 978 throw new RuntimeException("Internal error"); 979 } 980 } else { 981 aliasedPath = null; 982 } 983 } 984 } 985 986 if (aliasedPath != null) { 987 // Call getCachedFullStatus recursively to avoid recalculating cached aliases. 988 return getCachedFullStatus(aliasedPath); 989 } 990 991 // Fallback location. 992 return new AliasLocation(xpath, CODE_FALLBACK_ID); 993 } 994 995 /** 996 * We have to go through the source, add all the paths, then recurse to parents 997 * However, aliases are tricky, so watch it. 998 */ 999 static final boolean TRACE_FILL = CldrUtility.getProperty("TRACE_FILL", false); 1000 static final String DEBUG_PATH_STRING = CldrUtility.getProperty("DEBUG_PATH", null); 1001 static final Pattern DEBUG_PATH = DEBUG_PATH_STRING == null ? null : PatternCache.get(DEBUG_PATH_STRING); 1002 static final boolean SKIP_FALLBACKID = CldrUtility.getProperty("SKIP_FALLBACKID", false);; 1003 1004 static final int MAX_LEVEL = 40; /* Throw an error if it goes past this. */ 1005 1006 /** 1007 * Initialises the set of xpaths that a fully resolved XMLSource contains. 1008 * http://cldr.unicode.org/development/development-process/design-proposals/resolution-of-cldr-files. 1009 * Information about the aliased path and source locale ID of each xpath 1010 * is not precalculated here since it doesn't appear to improve overall 1011 * performance. 1012 */ fillKeys()1013 private Set<String> fillKeys() { 1014 Set<String> paths = findNonAliasedPaths(); 1015 // Find aliased paths and loop until no more aliases can be found. 1016 Set<String> newPaths = paths; 1017 int level = 0; 1018 boolean newPathsFound = false; 1019 do { 1020 // Debugging code to protect against an infinite loop. 1021 if (TRACE_FILL && DEBUG_PATH == null || level > MAX_LEVEL) { 1022 System.out.println(Utility.repeat(TRACE_INDENT, level) + "# paths waiting to be aliased: " 1023 + newPaths.size()); 1024 System.out.println(Utility.repeat(TRACE_INDENT, level) + "# paths found: " + paths.size()); 1025 } 1026 if (level > MAX_LEVEL) throw new IllegalArgumentException("Stack overflow"); 1027 1028 String[] sortedPaths = new String[newPaths.size()]; 1029 newPaths.toArray(sortedPaths); 1030 Arrays.sort(sortedPaths); 1031 1032 newPaths = getDirectAliases(sortedPaths); 1033 newPathsFound = paths.addAll(newPaths); 1034 level++; 1035 } while (newPathsFound); 1036 return paths; 1037 } 1038 1039 /** 1040 * Creates the set of resolved paths for this ResolvingSource while 1041 * ignoring aliasing. 1042 * 1043 * @return 1044 */ findNonAliasedPaths()1045 private Set<String> findNonAliasedPaths() { 1046 HashSet<String> paths = new HashSet<String>(); 1047 1048 // Get all XMLSources used during resolution. 1049 List<XMLSource> sourceList = new ArrayList<XMLSource>(sources.values()); 1050 if (!SKIP_FALLBACKID) { 1051 sourceList.add(constructedItems); 1052 } 1053 1054 // Make a pass through, filling all the direct paths, excluding aliases, and collecting others 1055 for (XMLSource curSource : sourceList) { 1056 for (String xpath : curSource) { 1057 paths.add(xpath); 1058 } 1059 } 1060 return paths; 1061 } 1062 1063 /** 1064 * Takes in a list of xpaths and returns a new set of paths that alias 1065 * directly to those existing xpaths. 1066 * 1067 * @param paths 1068 * a sorted list of xpaths 1069 * @param reverseAliases 1070 * a map of reverse aliases sorted by key. 1071 * @return 1072 */ getDirectAliases(String[] paths)1073 private Set<String> getDirectAliases(String[] paths) { 1074 HashSet<String> newPaths = new HashSet<String>(); 1075 // Keep track of the current path index: since it's sorted, we 1076 // never have to backtrack. 1077 int pathIndex = 0; 1078 LinkedHashMap<String, List<String>> reverseAliases = getReverseAliases(); 1079 for (String subpath : reverseAliases.keySet()) { 1080 // Find the first path that matches the current alias. 1081 while (pathIndex < paths.length && 1082 paths[pathIndex].compareTo(subpath) < 0) { 1083 pathIndex++; 1084 } 1085 1086 // Alias all paths that match the current alias. 1087 String xpath; 1088 List<String> list = reverseAliases.get(subpath); 1089 int endIndex = pathIndex; 1090 int suffixStart = subpath.length(); 1091 // Suffixes should always start with an element and not an 1092 // attribute to prevent invalid aliasing. 1093 while (endIndex < paths.length && 1094 (xpath = paths[endIndex]).startsWith(subpath) && 1095 xpath.charAt(suffixStart) == '/') { 1096 String suffix = xpath.substring(suffixStart); 1097 for (String reverseAlias : list) { 1098 String reversePath = reverseAlias + suffix; 1099 newPaths.add(reversePath); 1100 } 1101 endIndex++; 1102 } 1103 if (endIndex == paths.length) break; 1104 } 1105 return newPaths; 1106 } 1107 getReverseAliases()1108 private LinkedHashMap<String, List<String>> getReverseAliases() { 1109 return sources.get("root").getReverseAliases(); 1110 } 1111 1112 private transient Set<String> cachedKeySet = null; 1113 1114 /** 1115 * @return an iterator over all the xpaths in this XMLSource. 1116 */ iterator()1117 public Iterator<String> iterator() { 1118 return getCachedKeySet().iterator(); 1119 } 1120 getCachedKeySet()1121 private Set<String> getCachedKeySet() { 1122 if (cachedKeySet == null) { 1123 cachedKeySet = fillKeys(); 1124 // System.out.println("CachedKeySet: " + cachedKeySet); 1125 // cachedKeySet.addAll(constructedItems.keySet()); 1126 cachedKeySet = Collections.unmodifiableSet(cachedKeySet); 1127 } 1128 return cachedKeySet; 1129 } 1130 putFullPathAtDPath(String distinguishingXPath, String fullxpath)1131 public void putFullPathAtDPath(String distinguishingXPath, String fullxpath) { 1132 throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); 1133 } 1134 putValueAtDPath(String distinguishingXPath, String value)1135 public void putValueAtDPath(String distinguishingXPath, String value) { 1136 throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); 1137 } 1138 getXpathComments()1139 public Comments getXpathComments() { 1140 return currentSource.getXpathComments(); 1141 } 1142 setXpathComments(Comments path)1143 public void setXpathComments(Comments path) { 1144 throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); 1145 } 1146 removeValueAtDPath(String xpath)1147 public void removeValueAtDPath(String xpath) { 1148 throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); 1149 } 1150 freeze()1151 public XMLSource freeze() { 1152 return this; // No-op. ResolvingSource is already read-only. 1153 } 1154 1155 @Override valueChanged(String xpath, XMLSource nonResolvingSource)1156 public void valueChanged(String xpath, XMLSource nonResolvingSource) { 1157 synchronized (getSourceLocaleIDCache) { 1158 AliasLocation location = getSourceLocaleIDCache.remove(xpath); 1159 if (location == null) return; 1160 // Paths aliasing to this path (directly or indirectly) may be affected, 1161 // so clear them as well. 1162 // There's probably a more elegant way to fix the paths than simply 1163 // throwing everything out. 1164 Set<String> dependentPaths = getDirectAliases(new String[] { xpath }); 1165 if (dependentPaths.size() > 0) { 1166 for (String path : dependentPaths) { 1167 getSourceLocaleIDCache.remove(path); 1168 } 1169 } 1170 } 1171 } 1172 1173 /** 1174 * Creates a new ResolvingSource with the given locale resolution chain. 1175 * 1176 * @param sourceList 1177 * the list of XMLSources to look in during resolution, 1178 * ordered from the current locale up to root. 1179 */ ResolvingSource(List<XMLSource> sourceList)1180 public ResolvingSource(List<XMLSource> sourceList) { 1181 // Sanity check for root. 1182 if (sourceList == null || !sourceList.get(sourceList.size() - 1).getLocaleID().equals("root")) { 1183 throw new IllegalArgumentException("Last element should be root"); 1184 } 1185 currentSource = sourceList.get(0); // Convenience variable 1186 sources = new LinkedHashMap<String, XMLSource>(); 1187 for (XMLSource source : sourceList) { 1188 sources.put(source.getLocaleID(), source); 1189 } 1190 1191 // Add listeners to all locales except root, since we don't expect 1192 // root to change programatically. 1193 for (int i = 0, limit = sourceList.size() - 1; i < limit; i++) { 1194 sourceList.get(i).addListener(this); 1195 } 1196 } 1197 getLocaleID()1198 public String getLocaleID() { 1199 return currentSource.getLocaleID(); 1200 } 1201 1202 private static final String[] keyDisplayNames = { 1203 "calendar", 1204 "cf", 1205 "collation", 1206 "currency", 1207 "hc", 1208 "lb", 1209 "ms", 1210 "numbers" 1211 }; 1212 private static final String[][] typeDisplayNames = { 1213 { "account", "cf" }, 1214 { "ahom", "numbers" }, 1215 { "arab", "numbers" }, 1216 { "arabext", "numbers" }, 1217 { "armn", "numbers" }, 1218 { "armnlow", "numbers" }, 1219 { "bali", "numbers" }, 1220 { "beng", "numbers" }, 1221 { "big5han", "collation" }, 1222 { "brah", "numbers" }, 1223 { "buddhist", "calendar" }, 1224 { "cakm", "numbers" }, 1225 { "cham", "numbers" }, 1226 { "chinese", "calendar" }, 1227 { "compat", "collation" }, 1228 { "coptic", "calendar" }, 1229 { "cyrl", "numbers" }, 1230 { "dangi", "calendar" }, 1231 { "deva", "numbers" }, 1232 { "dictionary", "collation" }, 1233 { "ducet", "collation" }, 1234 { "emoji", "collation" }, 1235 { "eor", "collation" }, 1236 { "ethi", "numbers" }, 1237 { "ethiopic", "calendar" }, 1238 { "ethiopic-amete-alem", "calendar" }, 1239 { "fullwide", "numbers" }, 1240 { "gb2312han", "collation" }, 1241 { "geor", "numbers" }, 1242 { "gong", "numbers" }, 1243 { "gonm", "numbers" }, 1244 { "gregorian", "calendar" }, 1245 { "grek", "numbers" }, 1246 { "greklow", "numbers" }, 1247 { "gujr", "numbers" }, 1248 { "guru", "numbers" }, 1249 { "h11", "hc" }, 1250 { "h12", "hc" }, 1251 { "h23", "hc" }, 1252 { "h24", "hc" }, 1253 { "hanidec", "numbers" }, 1254 { "hans", "numbers" }, 1255 { "hansfin", "numbers" }, 1256 { "hant", "numbers" }, 1257 { "hantfin", "numbers" }, 1258 { "hebr", "numbers" }, 1259 { "hebrew", "calendar" }, 1260 { "hmng", "numbers" }, 1261 { "indian", "calendar" }, 1262 { "islamic", "calendar" }, 1263 { "islamic-civil", "calendar" }, 1264 { "islamic-rgsa", "calendar" }, 1265 { "islamic-tbla", "calendar" }, 1266 { "islamic-umalqura", "calendar" }, 1267 { "iso8601", "calendar" }, 1268 { "japanese", "calendar" }, 1269 { "java", "numbers" }, 1270 { "jpan", "numbers" }, 1271 { "jpanfin", "numbers" }, 1272 { "kali", "numbers" }, 1273 { "khmr", "numbers" }, 1274 { "knda", "numbers" }, 1275 { "lana", "numbers" }, 1276 { "lanatham", "numbers" }, 1277 { "laoo", "numbers" }, 1278 { "latn", "numbers" }, 1279 { "lepc", "numbers" }, 1280 { "limb", "numbers" }, 1281 { "loose", "lb" }, 1282 { "mathbold", "numbers" }, 1283 { "mathdbl", "numbers" }, 1284 { "mathmono", "numbers" }, 1285 { "mathsanb", "numbers" }, 1286 { "mathsans", "numbers" }, 1287 { "metric", "ms" }, 1288 { "mlym", "numbers" }, 1289 { "modi", "numbers" }, 1290 { "mong", "numbers" }, 1291 { "mroo", "numbers" }, 1292 { "mtei", "numbers" }, 1293 { "mymr", "numbers" }, 1294 { "mymrshan", "numbers" }, 1295 { "mymrtlng", "numbers" }, 1296 { "nkoo", "numbers" }, 1297 { "normal", "lb" }, 1298 { "olck", "numbers" }, 1299 { "orya", "numbers" }, 1300 { "osma", "numbers" }, 1301 { "persian", "calendar" }, 1302 { "phonebook", "collation" }, 1303 { "pinyin", "collation" }, 1304 { "reformed", "collation" }, 1305 { "roc", "calendar" }, 1306 { "rohg", "numbers" }, 1307 { "roman", "numbers" }, 1308 { "romanlow", "numbers" }, 1309 { "saur", "numbers" }, 1310 { "search", "collation" }, 1311 { "searchjl", "collation" }, 1312 { "shrd", "numbers" }, 1313 { "sind", "numbers" }, 1314 { "sinh", "numbers" }, 1315 { "sora", "numbers" }, 1316 { "standard", "cf" }, 1317 { "standard", "collation" }, 1318 { "strict", "lb" }, 1319 { "stroke", "collation" }, 1320 { "sund", "numbers" }, 1321 { "takr", "numbers" }, 1322 { "talu", "numbers" }, 1323 { "taml", "numbers" }, 1324 { "tamldec", "numbers" }, 1325 { "telu", "numbers" }, 1326 { "thai", "numbers" }, 1327 { "tibt", "numbers" }, 1328 { "tirh", "numbers" }, 1329 { "traditional", "collation" }, 1330 { "unihan", "collation" }, 1331 { "uksystem", "ms" }, 1332 { "ussystem", "ms" }, 1333 { "vaii", "numbers" }, 1334 { "wara", "numbers" }, 1335 { "zhuyin", "collation" } }; 1336 1337 private static final boolean SKIP_SINGLEZONES = false; 1338 private static XMLSource constructedItems = new SimpleXMLSource(CODE_FALLBACK_ID); 1339 1340 static { 1341 StandardCodes sc = StandardCodes.make(); 1342 Map<String, Set<String>> countries_zoneSet = sc.getCountryToZoneSet(); 1343 Map<String, String> zone_countries = sc.getZoneToCounty(); 1344 1345 // Set types = sc.getAvailableTypes(); 1346 for (int typeNo = 0; typeNo <= CLDRFile.TZ_START; ++typeNo) { 1347 String type = CLDRFile.getNameName(typeNo); 1348 // int typeNo = typeNameToCode(type); 1349 // if (typeNo < 0) continue; 1350 String type2 = (typeNo == CLDRFile.CURRENCY_SYMBOL) ? CLDRFile.getNameName(CLDRFile.CURRENCY_NAME) 1351 : (typeNo >= CLDRFile.TZ_START) ? "tzid" 1352 : type; 1353 Set<String> codes = sc.getSurveyToolDisplayCodes(type2); 1354 // String prefix = CLDRFile.NameTable[typeNo][0]; 1355 // String postfix = CLDRFile.NameTable[typeNo][1]; 1356 // String prefix2 = "//ldml" + prefix.substring(6); // [@version=\"" + GEN_VERSION + "\"] 1357 for (Iterator<String> codeIt = codes.iterator(); codeIt.hasNext();) { 1358 String code = codeIt.next(); 1359 String value = code; 1360 if (typeNo == CLDRFile.TZ_EXEMPLAR) { // skip single-zone countries 1361 if (SKIP_SINGLEZONES) { 1362 String country = (String) zone_countries.get(code); 1363 Set<String> s = countries_zoneSet.get(country); 1364 if (s != null && s.size() == 1) continue; 1365 } 1366 value = TimezoneFormatter.getFallbackName(value); 1367 } addFallbackCode(typeNo, code, value)1368 addFallbackCode(typeNo, code, value); 1369 } 1370 } 1371 1372 // Add commonlyUsed 1373 // //ldml/dates/timeZoneNames/metazone[@type="New_Zealand"]/commonlyUsed 1374 // should get this from supplemental metadata, but for now... 1375 // String[] metazones = 1376 // "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+"); 1377 // for (String metazone : metazones) { 1378 // constructedItems.putValueAtPath( 1379 // "//ldml/dates/timeZoneNames/metazone[@type=\"" 1380 // + metazone 1381 // + "\"]/commonlyUsed", 1382 // "false"); 1383 // } 1384 1385 String[] extraCodes = { "ar_001", "de_AT", "de_CH", "en_AU", "en_CA", "en_GB", "en_US", "es_419", "es_ES", "es_MX", 1386 "fr_CA", "fr_CH", "frc", "lou", "nds_NL", "nl_BE", "pt_BR", "pt_PT", "ro_MD", "sw_CD", "zh_Hans", "zh_Hant" }; 1387 for (String extraCode : extraCodes) { addFallbackCode(CLDRFile.LANGUAGE_NAME, extraCode, extraCode)1388 addFallbackCode(CLDRFile.LANGUAGE_NAME, extraCode, extraCode); 1389 } 1390 1391 addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_GB", "en_GB", "short")1392 addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_GB", "en_GB", "short"); addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_US", "en_US", "short")1393 addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_US", "en_US", "short"); addFallbackCode(CLDRFile.LANGUAGE_NAME, "az", "az", "short")1394 addFallbackCode(CLDRFile.LANGUAGE_NAME, "az", "az", "short"); 1395 addFallbackCode(CLDRFile.SCRIPT_NAME, "Hans", "Hans", "stand-alone")1396 addFallbackCode(CLDRFile.SCRIPT_NAME, "Hans", "Hans", "stand-alone"); addFallbackCode(CLDRFile.SCRIPT_NAME, "Hant", "Hant", "stand-alone")1397 addFallbackCode(CLDRFile.SCRIPT_NAME, "Hant", "Hant", "stand-alone"); 1398 addFallbackCode(CLDRFile.TERRITORY_NAME, "GB", "GB", "short")1399 addFallbackCode(CLDRFile.TERRITORY_NAME, "GB", "GB", "short"); addFallbackCode(CLDRFile.TERRITORY_NAME, "HK", "HK", "short")1400 addFallbackCode(CLDRFile.TERRITORY_NAME, "HK", "HK", "short"); addFallbackCode(CLDRFile.TERRITORY_NAME, "MO", "MO", "short")1401 addFallbackCode(CLDRFile.TERRITORY_NAME, "MO", "MO", "short"); addFallbackCode(CLDRFile.TERRITORY_NAME, "PS", "PS", "short")1402 addFallbackCode(CLDRFile.TERRITORY_NAME, "PS", "PS", "short"); addFallbackCode(CLDRFile.TERRITORY_NAME, "US", "US", "short")1403 addFallbackCode(CLDRFile.TERRITORY_NAME, "US", "US", "short"); 1404 addFallbackCode(CLDRFile.TERRITORY_NAME, "CD", "CD", "variant")1405 addFallbackCode(CLDRFile.TERRITORY_NAME, "CD", "CD", "variant"); // add other geopolitical items addFallbackCode(CLDRFile.TERRITORY_NAME, "CG", "CG", "variant")1406 addFallbackCode(CLDRFile.TERRITORY_NAME, "CG", "CG", "variant"); addFallbackCode(CLDRFile.TERRITORY_NAME, "CI", "CI", "variant")1407 addFallbackCode(CLDRFile.TERRITORY_NAME, "CI", "CI", "variant"); addFallbackCode(CLDRFile.TERRITORY_NAME, "CZ", "CZ", "variant")1408 addFallbackCode(CLDRFile.TERRITORY_NAME, "CZ", "CZ", "variant"); addFallbackCode(CLDRFile.TERRITORY_NAME, "FK", "FK", "variant")1409 addFallbackCode(CLDRFile.TERRITORY_NAME, "FK", "FK", "variant"); addFallbackCode(CLDRFile.TERRITORY_NAME, "MK", "MK", "variant")1410 addFallbackCode(CLDRFile.TERRITORY_NAME, "MK", "MK", "variant"); addFallbackCode(CLDRFile.TERRITORY_NAME, "TL", "TL", "variant")1411 addFallbackCode(CLDRFile.TERRITORY_NAME, "TL", "TL", "variant"); 1412 addFallbackCode(CLDRFile.TERRITORY_NAME, "XA", "XA")1413 addFallbackCode(CLDRFile.TERRITORY_NAME, "XA", "XA"); addFallbackCode(CLDRFile.TERRITORY_NAME, "XB", "XB")1414 addFallbackCode(CLDRFile.TERRITORY_NAME, "XB", "XB"); 1415 1416 addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr/era[@type=\"0\"]", "BCE", "variant"); 1417 addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr/era[@type=\"1\"]", "CE", "variant"); 1418 addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames/era[@type=\"0\"]", "BCE", "variant"); 1419 addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames/era[@type=\"1\"]", "CE", "variant"); 1420 addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNarrow/era[@type=\"0\"]", "BCE", "variant"); 1421 addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNarrow/era[@type=\"1\"]", "CE", "variant"); 1422 1423 //String defaultCurrPattern = "¤ #,##0.00"; // use root value; can't get the locale's currency pattern in this static context; "" and "∅∅∅" cause errors. 1424 for (int i = 0; i < keyDisplayNames.length; ++i) { 1425 constructedItems.putValueAtPath( 1426 "//ldml/localeDisplayNames/keys/key" + 1427 "[@type=\"" + keyDisplayNames[i] + "\"]", 1428 keyDisplayNames[i]); 1429 } 1430 for (int i = 0; i < typeDisplayNames.length; ++i) { 1431 constructedItems.putValueAtPath( 1432 "//ldml/localeDisplayNames/types/type" 1433 + "[@key=\"" + typeDisplayNames[i][1] + "\"]" 1434 + "[@type=\"" + typeDisplayNames[i][0] + "\"]", 1435 typeDisplayNames[i][0]); 1436 } 1437 // String[][] relativeValues = { 1438 // // {"Three days ago", "-3"}, 1439 // { "The day before yesterday", "-2" }, 1440 // { "Yesterday", "-1" }, 1441 // { "Today", "0" }, 1442 // { "Tomorrow", "1" }, 1443 // { "The day after tomorrow", "2" }, 1444 // // {"Three days from now", "3"}, 1445 // }; 1446 // for (int i = 0; i < relativeValues.length; ++i) { 1447 // constructedItems.putValueAtPath( 1448 // "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/fields/field[@type=\"day\"]/relative[@type=\"" 1449 // + relativeValues[i][1] + "\"]", 1450 // relativeValues[i][0]); 1451 // } 1452 constructedItems.freeze()1453 constructedItems.freeze(); 1454 allowDuplicates = Collections.unmodifiableMap(allowDuplicates); 1455 // System.out.println("constructedItems: " + constructedItems); 1456 } 1457 addFallbackCode(int typeNo, String code, String value)1458 private static void addFallbackCode(int typeNo, String code, String value) { 1459 addFallbackCode(typeNo, code, value, null); 1460 } 1461 addFallbackCode(int typeNo, String code, String value, String alt)1462 private static void addFallbackCode(int typeNo, String code, String value, String alt) { 1463 // String path = prefix + code + postfix; 1464 String fullpath = CLDRFile.getKey(typeNo, code); 1465 String distinguishingPath = addFallbackCodeToConstructedItems(fullpath, value, alt); 1466 if (typeNo == CLDRFile.LANGUAGE_NAME || typeNo == CLDRFile.SCRIPT_NAME || typeNo == CLDRFile.TERRITORY_NAME) { 1467 allowDuplicates.put(distinguishingPath, code); 1468 } 1469 } 1470 addFallbackCode(String fullpath, String value, String alt)1471 private static void addFallbackCode(String fullpath, String value, String alt) { // assumes no allowDuplicates for this 1472 addFallbackCodeToConstructedItems(fullpath, value, alt); // ignore unneeded return value 1473 } 1474 addFallbackCodeToConstructedItems(String fullpath, String value, String alt)1475 private static String addFallbackCodeToConstructedItems(String fullpath, String value, String alt) { 1476 if (alt != null) { 1477 // Insert the @alt= string after the last occurrence of "]" 1478 StringBuffer fullpathBuf = new StringBuffer(fullpath); 1479 fullpath = fullpathBuf.insert(fullpathBuf.lastIndexOf("]") + 1, "[@alt=\"" + alt + "\"]").toString(); 1480 } 1481 // System.out.println(fullpath + "\t=> " + code); 1482 return constructedItems.putValueAtPath(fullpath, value); 1483 } 1484 1485 @Override isHere(String path)1486 public boolean isHere(String path) { 1487 return currentSource.isHere(path); // only test one level 1488 } 1489 1490 @Override getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result)1491 public void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result) { 1492 // NOTE: No caching is currently performed here because the unresolved 1493 // locales already cache their value-path mappings, and it's not 1494 // clear yet how much further caching would speed this up. 1495 1496 // Add all non-aliased paths with the specified value. 1497 List<XMLSource> children = new ArrayList<XMLSource>(); 1498 Set<String> filteredPaths = new HashSet<String>(); 1499 for (XMLSource source : sources.values()) { 1500 Set<String> pathsWithValue = new HashSet<String>(); 1501 source.getPathsWithValue(valueToMatch, pathPrefix, pathsWithValue); 1502 // Don't add a path with the value if it is overridden by a child locale. 1503 for (String pathWithValue : pathsWithValue) { 1504 if (!sourcesHavePath(pathWithValue, children)) { 1505 filteredPaths.add(pathWithValue); 1506 } 1507 } 1508 children.add(source); 1509 } 1510 1511 // Find all paths that alias to the specified value, then filter by 1512 // path prefix. 1513 Set<String> aliases = new HashSet<String>(); 1514 Set<String> oldAliases = new HashSet<String>(filteredPaths); 1515 Set<String> newAliases; 1516 do { 1517 String[] sortedPaths = new String[oldAliases.size()]; 1518 oldAliases.toArray(sortedPaths); 1519 Arrays.sort(sortedPaths); 1520 newAliases = getDirectAliases(sortedPaths); 1521 oldAliases = newAliases; 1522 aliases.addAll(newAliases); 1523 } while (newAliases.size() > 0); 1524 1525 // get the aliases, but only the ones that have values that match 1526 String norm = null; 1527 for (String alias : aliases) { 1528 if (alias.startsWith(pathPrefix)) { 1529 if (norm == null) { 1530 norm = SimpleXMLSource.normalize(valueToMatch); 1531 } 1532 String value = getValueAtDPath(alias); 1533 if (SimpleXMLSource.normalize(value).equals(norm)) { 1534 filteredPaths.add(alias); 1535 } 1536 } 1537 } 1538 1539 result.addAll(filteredPaths); 1540 } 1541 sourcesHavePath(String xpath, List<XMLSource> sources)1542 private boolean sourcesHavePath(String xpath, List<XMLSource> sources) { 1543 for (XMLSource source : sources) { 1544 if (source.hasValueAtDPath(xpath)) return true; 1545 } 1546 return false; 1547 } 1548 1549 @Override getDtdVersionInfo()1550 public VersionInfo getDtdVersionInfo() { 1551 return currentSource.getDtdVersionInfo(); 1552 } 1553 } 1554 1555 /** 1556 * See CLDRFile isWinningPath for documentation 1557 * 1558 * @param path 1559 * @return 1560 */ isWinningPath(String path)1561 public boolean isWinningPath(String path) { 1562 return getWinningPath(path).equals(path); 1563 } 1564 1565 /** 1566 * See CLDRFile getWinningPath for documentation. 1567 * Default implementation is that it removes draft and [@alt="...proposed..." if possible 1568 * 1569 * @param path 1570 * @return 1571 */ getWinningPath(String path)1572 public String getWinningPath(String path) { 1573 String newPath = CLDRFile.getNondraftNonaltXPath(path); 1574 if (!newPath.equals(path)) { 1575 String value = getValueAtPath(newPath); // ensure that it still works 1576 if (value != null) { 1577 return newPath; 1578 } 1579 } 1580 return path; 1581 } 1582 1583 /** 1584 * Adds a listener to this XML source. 1585 */ addListener(Listener listener)1586 public void addListener(Listener listener) { 1587 listeners.add(new WeakReference<Listener>(listener)); 1588 } 1589 1590 /** 1591 * Notifies all listeners that a change has occurred. This method should be 1592 * called by the XMLSource being updated after any change 1593 * (usually in putValueAtDPath() and removeValueAtDPath()). 1594 * This should only be called by XMLSource / CLDRFile 1595 * 1596 * @param xpath 1597 * the xpath where the change occurred. 1598 */ notifyListeners(String xpath)1599 protected void notifyListeners(String xpath) { 1600 int i = 0; 1601 while (i < listeners.size()) { 1602 Listener listener = listeners.get(i).get(); 1603 if (listener == null) { // listener has been garbage-collected. 1604 listeners.remove(i); 1605 } else { 1606 listener.valueChanged(xpath, this); 1607 i++; 1608 } 1609 } 1610 } 1611 1612 /** 1613 * return true if the path in this file (without resolution). Default implementation is to just see if the path has 1614 * a value. 1615 * The resolved source must just test the top level. 1616 * 1617 * @param path 1618 * @return 1619 */ isHere(String path)1620 public boolean isHere(String path) { 1621 return getValueAtPath(path) != null; 1622 } 1623 1624 /** 1625 * Find all the distinguished paths having values matching valueToMatch, and add them to result. 1626 * 1627 * @param valueToMatch 1628 * @param pathPrefix 1629 * @param result 1630 */ getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result)1631 public abstract void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result); 1632 getDtdVersionInfo()1633 public VersionInfo getDtdVersionInfo() { 1634 return null; 1635 } 1636 getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound)1637 public String getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) { 1638 return null; // only a resolving xmlsource will return a value 1639 } 1640 1641 // HACK, should be field on XMLSource getDtdType()1642 public DtdType getDtdType() { 1643 final Iterator<String> it = iterator(); 1644 if (it.hasNext()) { 1645 String path = it.next(); 1646 return DtdType.fromPath(path); 1647 } 1648 return null; 1649 } 1650 } 1651