1 /* 2 ********************************************************************** 3 * Copyright (c) 2002-2019, International Business Machines 4 * Corporation and others. All Rights Reserved. 5 ********************************************************************** 6 * Author: Mark Davis 7 ********************************************************************** 8 */ 9 package org.unicode.cldr.util; 10 11 import com.google.common.base.Joiner; 12 import com.google.common.base.Splitter; 13 import com.google.common.collect.ImmutableMap; 14 import com.google.common.collect.ImmutableMap.Builder; 15 import com.google.common.collect.ImmutableSet; 16 import com.google.common.util.concurrent.UncheckedExecutionException; 17 import com.ibm.icu.impl.Relation; 18 import com.ibm.icu.impl.Row; 19 import com.ibm.icu.impl.Row.R2; 20 import com.ibm.icu.impl.Utility; 21 import com.ibm.icu.text.MessageFormat; 22 import com.ibm.icu.text.PluralRules; 23 import com.ibm.icu.text.SimpleDateFormat; 24 import com.ibm.icu.text.Transform; 25 import com.ibm.icu.text.UnicodeSet; 26 import com.ibm.icu.util.Calendar; 27 import com.ibm.icu.util.Freezable; 28 import com.ibm.icu.util.ICUUncheckedIOException; 29 import com.ibm.icu.util.Output; 30 import com.ibm.icu.util.ULocale; 31 import com.ibm.icu.util.VersionInfo; 32 import java.io.File; 33 import java.io.FileInputStream; 34 import java.io.FilenameFilter; 35 import java.io.InputStream; 36 import java.io.PrintWriter; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Collection; 40 import java.util.Collections; 41 import java.util.Comparator; 42 import java.util.Date; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.Iterator; 46 import java.util.LinkedHashMap; 47 import java.util.LinkedHashSet; 48 import java.util.LinkedList; 49 import java.util.List; 50 import java.util.Locale; 51 import java.util.Map; 52 import java.util.Set; 53 import java.util.TreeMap; 54 import java.util.TreeSet; 55 import java.util.concurrent.ConcurrentHashMap; 56 import java.util.regex.Matcher; 57 import java.util.regex.Pattern; 58 import java.util.stream.Collectors; 59 import org.unicode.cldr.test.CheckMetazones; 60 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod; 61 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature; 62 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope; 63 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget; 64 import org.unicode.cldr.util.LocaleInheritanceInfo.Reason; 65 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo; 66 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count; 67 import org.unicode.cldr.util.SupplementalDataInfo.PluralType; 68 import org.unicode.cldr.util.With.SimpleIterator; 69 import org.unicode.cldr.util.XMLFileReader.AllHandler; 70 import org.unicode.cldr.util.XMLSource.ResolvingSource; 71 import org.unicode.cldr.util.XPathParts.Comments; 72 import org.xml.sax.Attributes; 73 import org.xml.sax.Locator; 74 import org.xml.sax.SAXException; 75 import org.xml.sax.SAXParseException; 76 import org.xml.sax.XMLReader; 77 import org.xml.sax.helpers.XMLReaderFactory; 78 79 /** 80 * This is a class that represents the contents of a CLDR file, as <key,value> pairs, where the key 81 * is a "cleaned" xpath (with non-distinguishing attributes removed), and the value is an object 82 * that contains the full xpath plus a value, which is a string, or a node (the latter for atomic 83 * elements). 84 * 85 * <p><b>WARNING: The API on this class is likely to change.</b> Having the full xpath on the value 86 * is clumsy; I need to change it to having the key be an object that contains the full xpath, but 87 * then sorts as if it were clean. 88 * 89 * <p>Each instance also contains a set of associated comments for each xpath. 90 * 91 * @author medavis 92 */ 93 94 /* 95 * Notes: 96 * http://xml.apache.org/xerces2-j/faq-grammars.html#faq-3 97 * http://developers.sun.com/dev/coolstuff/xml/readme.html 98 * http://lists.xml.org/archives/xml-dev/200007/msg00284.html 99 * http://java.sun.com/j2se/1.4.2/docs/api/org/xml/sax/DTDHandler.html 100 */ 101 102 public class CLDRFile implements Freezable<CLDRFile>, Iterable<String>, LocaleStringProvider { 103 104 private static final String GETNAME_LOCALE_SEPARATOR = 105 "//ldml/localeDisplayNames/localeDisplayPattern/localeSeparator"; 106 private static final String GETNAME_LOCALE_PATTERN = 107 "//ldml/localeDisplayNames/localeDisplayPattern/localePattern"; 108 private static final String GETNAME_LOCALE_KEY_TYPE_PATTERN = 109 "//ldml/localeDisplayNames/localeDisplayPattern/localeKeyTypePattern"; 110 111 private static final ImmutableSet<String> casesNominativeOnly = 112 ImmutableSet.of(GrammaticalFeature.grammaticalCase.getDefault(null)); 113 /** 114 * Variable to control whether File reads are buffered; this will about halve the time spent in 115 * loadFromFile() and Factory.make() from about 20 % to about 10 %. It will also noticeably 116 * improve the different unit tests take in the TestAll fixture. TRUE - use buffering (default) 117 * FALSE - do not use buffering 118 */ 119 private static final boolean USE_LOADING_BUFFER = true; 120 121 private static final boolean DEBUG = false; 122 123 public static final Pattern ALT_PROPOSED_PATTERN = 124 PatternCache.get(".*\\[@alt=\"[^\"]*proposed[^\"]*\"].*"); 125 public static final Pattern DRAFT_PATTERN = PatternCache.get("\\[@draft=\"([^\"]*)\"\\]"); 126 public static final Pattern XML_SPACE_PATTERN = 127 PatternCache.get("\\[@xml:space=\"([^\"]*)\"\\]"); 128 129 private static boolean LOG_PROGRESS = false; 130 131 public static boolean HACK_ORDER = false; 132 private static boolean DEBUG_LOGGING = false; 133 134 public static final String SUPPLEMENTAL_NAME = "supplementalData"; 135 public static final String SUPPLEMENTAL_METADATA = "supplementalMetadata"; 136 public static final String SUPPLEMENTAL_PREFIX = "supplemental"; 137 public static final String GEN_VERSION = "45"; 138 public static final List<String> SUPPLEMENTAL_NAMES = 139 Arrays.asList( 140 "characters", 141 "coverageLevels", 142 "dayPeriods", 143 "genderList", 144 "grammaticalFeatures", 145 "languageInfo", 146 "languageGroup", 147 "likelySubtags", 148 "metaZones", 149 "numberingSystems", 150 "ordinals", 151 "pluralRanges", 152 "plurals", 153 "postalCodeData", 154 "rgScope", 155 "supplementalData", 156 "supplementalMetadata", 157 "telephoneCodeData", 158 "units", 159 "windowsZones"); 160 161 private Set<String> extraPaths = null; 162 163 private boolean locked; 164 private DtdType dtdType; 165 private DtdData dtdData; 166 167 XMLSource dataSource; // TODO(jchye): make private 168 169 private File supplementalDirectory; 170 171 /** 172 * Does the value in question either match or inherent the current value? 173 * 174 * <p>To match, the value in question and the current value must be non-null and equal. 175 * 176 * <p>To inherit the current value, the value in question must be INHERITANCE_MARKER and the 177 * current value must equal the bailey value. 178 * 179 * <p>This CLDRFile is only used here for getBaileyValue, not to get curValue 180 * 181 * @param value the value in question 182 * @param curValue the current value, that is, XMLSource.getValueAtDPath(xpathString) 183 * @param xpathString the path identifier 184 * @return true if it matches or inherits, else false 185 */ equalsOrInheritsCurrentValue(String value, String curValue, String xpathString)186 public boolean equalsOrInheritsCurrentValue(String value, String curValue, String xpathString) { 187 if (value == null || curValue == null) { 188 return false; 189 } 190 if (value.equals(curValue)) { 191 return true; 192 } 193 if (value.equals(CldrUtility.INHERITANCE_MARKER)) { 194 String baileyValue = getBaileyValue(xpathString, null, null); 195 if (baileyValue == null) { 196 /* This may happen for Invalid XPath; InvalidXPathException may be thrown. */ 197 return false; 198 } 199 if (curValue.equals(baileyValue)) { 200 return true; 201 } 202 } 203 return false; 204 } 205 getResolvingDataSource()206 public XMLSource getResolvingDataSource() { 207 if (!isResolved()) { 208 throw new IllegalArgumentException( 209 "CLDRFile must be resolved for getResolvingDataSource"); 210 } 211 // dataSource instanceof XMLSource.ResolvingSource 212 return dataSource; 213 } 214 215 public enum DraftStatus { 216 unconfirmed, 217 provisional, 218 contributed, 219 approved; 220 forString(String string)221 public static DraftStatus forString(String string) { 222 return string == null 223 ? DraftStatus.approved 224 : DraftStatus.valueOf(string.toLowerCase(Locale.ENGLISH)); 225 } 226 227 /** 228 * Get the draft status from a full xpath 229 * 230 * @param xpath 231 * @return 232 */ forXpath(String xpath)233 public static DraftStatus forXpath(String xpath) { 234 final String status = 235 XPathParts.getFrozenInstance(xpath).getAttributeValue(-1, "draft"); 236 return forString(status); 237 } 238 239 /** Return the XPath suffix for this draft status or "" for approved. */ asXpath()240 public String asXpath() { 241 if (this == approved) { 242 return ""; 243 } else { 244 return "[@draft=\"" + name() + "\"]"; 245 } 246 } 247 248 /** update this XPath with this draft status */ updateXPath(final String fullXpath)249 public String updateXPath(final String fullXpath) { 250 final XPathParts xpp = XPathParts.getFrozenInstance(fullXpath).cloneAsThawed(); 251 final String oldDraft = xpp.getAttributeValue(-1, "draft"); 252 if (forString(oldDraft) == this) { 253 return fullXpath; // no change; 254 } 255 if (this == approved) { 256 xpp.removeAttribute(-1, "draft"); 257 } else { 258 xpp.setAttribute(-1, "draft", this.name()); 259 } 260 return xpp.toString(); 261 } 262 } 263 264 @Override toString()265 public String toString() { 266 return "{" 267 + "locked=" 268 + locked 269 + " locale=" 270 + dataSource.getLocaleID() 271 + " dataSource=" 272 + dataSource.toString() 273 + "}"; 274 } 275 toString(String regex)276 public String toString(String regex) { 277 return "{" 278 + "locked=" 279 + locked 280 + " locale=" 281 + dataSource.getLocaleID() 282 + " regex=" 283 + regex 284 + " dataSource=" 285 + dataSource.toString(regex) 286 + "}"; 287 } 288 289 // for refactoring 290 setNonInheriting(boolean isSupplemental)291 public CLDRFile setNonInheriting(boolean isSupplemental) { 292 if (locked) { 293 throw new UnsupportedOperationException("Attempt to modify locked object"); 294 } 295 dataSource.setNonInheriting(isSupplemental); 296 return this; 297 } 298 isNonInheriting()299 public boolean isNonInheriting() { 300 return dataSource.isNonInheriting(); 301 } 302 303 private static final boolean DEBUG_CLDR_FILE = false; 304 private String creationTime = null; // only used if DEBUG_CLDR_FILE 305 306 /** 307 * Construct a new CLDRFile. 308 * 309 * @param dataSource must not be null 310 */ CLDRFile(XMLSource dataSource)311 public CLDRFile(XMLSource dataSource) { 312 this.dataSource = dataSource; 313 314 if (DEBUG_CLDR_FILE) { 315 creationTime = 316 new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") 317 .format(Calendar.getInstance().getTime()); 318 System.out.println(" Created new CLDRFile(dataSource) at " + creationTime); 319 } 320 } 321 322 /** 323 * get Unresolved CLDRFile 324 * 325 * @param localeId 326 * @param dirs 327 * @param minimalDraftStatus 328 */ CLDRFile(String localeId, List<File> dirs, DraftStatus minimalDraftStatus)329 public CLDRFile(String localeId, List<File> dirs, DraftStatus minimalDraftStatus) { 330 // order matters 331 this.dataSource = XMLSource.getFrozenInstance(localeId, dirs, minimalDraftStatus); 332 this.dtdType = dataSource.getXMLNormalizingDtdType(); 333 this.dtdData = DtdData.getInstance(this.dtdType); 334 } 335 CLDRFile(XMLSource dataSource, XMLSource... resolvingParents)336 public CLDRFile(XMLSource dataSource, XMLSource... resolvingParents) { 337 List<XMLSource> sourceList = new ArrayList<>(); 338 sourceList.add(dataSource); 339 sourceList.addAll(Arrays.asList(resolvingParents)); 340 this.dataSource = new ResolvingSource(sourceList); 341 342 if (DEBUG_CLDR_FILE) { 343 creationTime = 344 new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") 345 .format(Calendar.getInstance().getTime()); 346 System.out.println( 347 " Created new CLDRFile(dataSource, XMLSource... resolvingParents) at " 348 + creationTime); 349 } 350 } 351 loadFromFile( File f, String localeName, DraftStatus minimalDraftStatus, XMLSource source)352 public static CLDRFile loadFromFile( 353 File f, String localeName, DraftStatus minimalDraftStatus, XMLSource source) { 354 String fullFileName = f.getAbsolutePath(); 355 try { 356 fullFileName = PathUtilities.getNormalizedPathString(f); 357 if (DEBUG_LOGGING) { 358 System.out.println("Parsing: " + fullFileName); 359 Log.logln(LOG_PROGRESS, "Parsing: " + fullFileName); 360 } 361 final CLDRFile cldrFile; 362 if (USE_LOADING_BUFFER) { 363 // Use Buffering - improves performance at little cost to memory footprint 364 // try (InputStream fis = new BufferedInputStream(new FileInputStream(f),32000);) { 365 try (InputStream fis = InputStreamFactory.createInputStream(f)) { 366 cldrFile = load(fullFileName, localeName, fis, minimalDraftStatus, source); 367 return cldrFile; 368 } 369 } else { 370 // previous version - do not use buffering 371 try (InputStream fis = new FileInputStream(f); ) { 372 cldrFile = load(fullFileName, localeName, fis, minimalDraftStatus, source); 373 return cldrFile; 374 } 375 } 376 377 } catch (Exception e) { 378 // use a StringBuilder to construct the message. 379 StringBuilder sb = new StringBuilder("Cannot read the file '"); 380 sb.append(fullFileName); 381 sb.append("': "); 382 sb.append(e.getMessage()); 383 throw new ICUUncheckedIOException(sb.toString(), e); 384 } 385 } 386 loadFromFiles( List<File> dirs, String localeName, DraftStatus minimalDraftStatus, XMLSource source)387 public static CLDRFile loadFromFiles( 388 List<File> dirs, String localeName, DraftStatus minimalDraftStatus, XMLSource source) { 389 try { 390 if (DEBUG_LOGGING) { 391 System.out.println("Parsing: " + dirs); 392 Log.logln(LOG_PROGRESS, "Parsing: " + dirs); 393 } 394 if (USE_LOADING_BUFFER) { 395 // Use Buffering - improves performance at little cost to memory footprint 396 // try (InputStream fis = new BufferedInputStream(new FileInputStream(f),32000);) { 397 CLDRFile cldrFile = new CLDRFile(source); 398 for (File dir : dirs) { 399 File f = new File(dir, localeName + ".xml"); 400 try (InputStream fis = InputStreamFactory.createInputStream(f)) { 401 cldrFile.loadFromInputStream( 402 PathUtilities.getNormalizedPathString(f), 403 localeName, 404 fis, 405 minimalDraftStatus, 406 false); 407 } 408 } 409 return cldrFile; 410 } else { 411 throw new IllegalArgumentException("Must use USE_LOADING_BUFFER"); 412 } 413 414 } catch (Exception e) { 415 // e.printStackTrace(); 416 // use a StringBuilder to construct the message. 417 StringBuilder sb = new StringBuilder("Cannot read the file '"); 418 sb.append(dirs); 419 throw new ICUUncheckedIOException(sb.toString(), e); 420 } 421 } 422 423 /** 424 * Produce a CLDRFile from a localeName, given a directory. (Normally a Factory is used to 425 * create CLDRFiles.) 426 * 427 * @param f 428 * @param localeName 429 * @param minimalDraftStatus 430 */ loadFromFile(File f, String localeName, DraftStatus minimalDraftStatus)431 public static CLDRFile loadFromFile(File f, String localeName, DraftStatus minimalDraftStatus) { 432 return loadFromFile(f, localeName, minimalDraftStatus, new SimpleXMLSource(localeName)); 433 } 434 loadFromFiles( List<File> dirs, String localeName, DraftStatus minimalDraftStatus)435 public static CLDRFile loadFromFiles( 436 List<File> dirs, String localeName, DraftStatus minimalDraftStatus) { 437 return loadFromFiles(dirs, localeName, minimalDraftStatus, new SimpleXMLSource(localeName)); 438 } 439 load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus)440 static CLDRFile load( 441 String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus) { 442 return load(fileName, localeName, fis, minimalDraftStatus, new SimpleXMLSource(localeName)); 443 } 444 445 /** 446 * Load a CLDRFile from a file input stream. 447 * 448 * @param localeName 449 * @param fis 450 */ load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, XMLSource source)451 private static CLDRFile load( 452 String fileName, 453 String localeName, 454 InputStream fis, 455 DraftStatus minimalDraftStatus, 456 XMLSource source) { 457 CLDRFile cldrFile = new CLDRFile(source); 458 return cldrFile.loadFromInputStream(fileName, localeName, fis, minimalDraftStatus, false); 459 } 460 461 /** 462 * Load a CLDRFile from a file input stream. 463 * 464 * @param localeName 465 * @param fis 466 */ load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, XMLSource source, boolean leniency)467 private static CLDRFile load( 468 String fileName, 469 String localeName, 470 InputStream fis, 471 DraftStatus minimalDraftStatus, 472 XMLSource source, 473 boolean leniency) { 474 CLDRFile cldrFile = new CLDRFile(source); 475 return cldrFile.loadFromInputStream( 476 fileName, localeName, fis, minimalDraftStatus, leniency); 477 } 478 load( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, boolean leniency)479 static CLDRFile load( 480 String fileName, 481 String localeName, 482 InputStream fis, 483 DraftStatus minimalDraftStatus, 484 boolean leniency) { 485 return load( 486 fileName, 487 localeName, 488 fis, 489 minimalDraftStatus, 490 new SimpleXMLSource(localeName), 491 leniency); 492 } 493 494 /** 495 * Low-level function, only normally used for testing. 496 * 497 * @param fileName 498 * @param localeName 499 * @param fis 500 * @param minimalDraftStatus 501 * @param leniency if true, skip dtd validation 502 * @return 503 */ loadFromInputStream( String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus, boolean leniency)504 public CLDRFile loadFromInputStream( 505 String fileName, 506 String localeName, 507 InputStream fis, 508 DraftStatus minimalDraftStatus, 509 boolean leniency) { 510 CLDRFile cldrFile = this; 511 MyDeclHandler DEFAULT_DECLHANDLER = new MyDeclHandler(cldrFile, minimalDraftStatus); 512 XMLFileReader.read(fileName, fis, -1, !leniency, DEFAULT_DECLHANDLER); 513 if (DEFAULT_DECLHANDLER.isSupplemental < 0) { 514 throw new IllegalArgumentException( 515 "root of file must be either ldml or supplementalData"); 516 } 517 cldrFile.setNonInheriting(DEFAULT_DECLHANDLER.isSupplemental > 0); 518 if (DEFAULT_DECLHANDLER.overrideCount > 0) { 519 throw new IllegalArgumentException( 520 "Internal problems: either data file has duplicate path, or" 521 + " CLDRFile.isDistinguishing() or CLDRFile.isOrdered() need updating: " 522 + DEFAULT_DECLHANDLER.overrideCount 523 + "; The exact problems are printed on the console above."); 524 } 525 if (localeName == null) { 526 cldrFile.dataSource.setLocaleID(cldrFile.getLocaleIDFromIdentity()); 527 } 528 return cldrFile; 529 } 530 531 /** 532 * Clone the object. Produces unlocked version 533 * 534 * @see com.ibm.icu.util.Freezable 535 */ 536 @Override cloneAsThawed()537 public CLDRFile cloneAsThawed() { 538 try { 539 CLDRFile result = (CLDRFile) super.clone(); 540 result.locked = false; 541 result.dataSource = result.dataSource.cloneAsThawed(); 542 return result; 543 } catch (CloneNotSupportedException e) { 544 throw new InternalError("should never happen"); 545 } 546 } 547 548 /** Prints the contents of the file (the xpaths/values) to the console. */ show()549 public CLDRFile show() { 550 for (Iterator<String> it2 = iterator(); it2.hasNext(); ) { 551 String xpath = it2.next(); 552 System.out.println(getFullXPath(xpath) + " =>\t" + getStringValue(xpath)); 553 } 554 return this; 555 } 556 557 private static final Map<String, Object> nullOptions = 558 Collections.unmodifiableMap(new TreeMap<String, Object>()); 559 560 /** 561 * Write the corresponding XML file out, with the normal formatting and indentation. Will update 562 * the identity element, including version, and other items. If the CLDRFile is empty, the DTD 563 * type will be //ldml. 564 */ write(PrintWriter pw)565 public void write(PrintWriter pw) { 566 write(pw, nullOptions); 567 } 568 569 /** 570 * Write the corresponding XML file out, with the normal formatting and indentation. Will update 571 * the identity element, including version, and other items. If the CLDRFile is empty, the DTD 572 * type will be //ldml. 573 * 574 * @param pw writer to print to 575 * @param options map of options for writing 576 * @return true if we write the file, false if we cancel due to skipping all paths 577 */ write(PrintWriter pw, Map<String, ?> options)578 public boolean write(PrintWriter pw, Map<String, ?> options) { 579 final CldrXmlWriter xmlWriter = new CldrXmlWriter(this, pw, options); 580 xmlWriter.write(); 581 return true; 582 } 583 584 /** Get a string value from an xpath. */ 585 @Override getStringValue(String xpath)586 public String getStringValue(String xpath) { 587 try { 588 String result = dataSource.getValueAtPath(xpath); 589 if (result == null && dataSource.isResolving()) { 590 final String fallbackPath = getFallbackPath(xpath, false, true); 591 // often fallbackPath equals xpath -- in such cases, isn't it a waste of time to 592 // call getValueAtPath again? 593 if (fallbackPath != null) { 594 result = dataSource.getValueAtPath(fallbackPath); 595 } 596 } 597 if (isResolved() 598 && GlossonymConstructor.valueIsBogus(result) 599 && GlossonymConstructor.pathIsEligible(xpath)) { 600 final String constructedValue = new GlossonymConstructor(this).getValue(xpath); 601 if (constructedValue != null) { 602 result = constructedValue; 603 } 604 } 605 return result; 606 } catch (Exception e) { 607 throw new UncheckedExecutionException("Bad path: " + xpath, e); 608 } 609 } 610 611 /** 612 * Get GeorgeBailey value: that is, what the value would be if it were not directly contained in 613 * the file at that path. If the value is null or INHERITANCE_MARKER (with resolving), then 614 * baileyValue = resolved value. A non-resolving CLDRFile will always return null. 615 */ getBaileyValue( String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound)616 public String getBaileyValue( 617 String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) { 618 String result = dataSource.getBaileyValue(xpath, pathWhereFound, localeWhereFound); 619 if ((result == null || result.equals(CldrUtility.INHERITANCE_MARKER)) 620 && dataSource.isResolving()) { 621 final String fallbackPath = 622 getFallbackPath( 623 xpath, false, 624 false); // return null if there is no different sideways path 625 if (xpath.equals(fallbackPath)) { 626 getFallbackPath(xpath, false, true); 627 throw new IllegalArgumentException(); // should never happen 628 } 629 if (fallbackPath != null) { 630 result = dataSource.getValueAtPath(fallbackPath); 631 if (result != null) { 632 Status status = new Status(); 633 if (localeWhereFound != null) { 634 localeWhereFound.value = dataSource.getSourceLocaleID(fallbackPath, status); 635 } 636 if (pathWhereFound != null) { 637 pathWhereFound.value = status.pathWhereFound; 638 } 639 } 640 } 641 } 642 if (isResolved() 643 && GlossonymConstructor.valueIsBogus(result) 644 && GlossonymConstructor.pathIsEligible(xpath)) { 645 final GlossonymConstructor gc = new GlossonymConstructor(this); 646 final String constructedValue = 647 gc.getValueAndTrack(xpath, pathWhereFound, localeWhereFound); 648 if (constructedValue != null) { 649 result = constructedValue; 650 } 651 } 652 return result; 653 } 654 655 /** 656 * Return a list of all paths which contributed to the value, as well as all bailey values. This 657 * is used to explain inheritance and bailey values. The list must be interpreted in order. When 658 * {@link LocaleInheritanceInfo.Reason#isTerminal()} return true, that indicates a successful 659 * lookup and partitions values from subsequent bailey values. 660 * 661 * @see #getBaileyValue(String, Output, Output) 662 * @see #getSourceLocaleIdExtended(String, Status, boolean) 663 */ getPathsWhereFound(String xpath)664 public List<LocaleInheritanceInfo> getPathsWhereFound(String xpath) { 665 if (!isResolved()) { 666 throw new IllegalArgumentException( 667 "getPathsWhereFound() is only valid on a resolved CLDRFile"); 668 } 669 LinkedList<LocaleInheritanceInfo> list = new LinkedList<>(); 670 // first, call getSourceLocaleIdExtended to populate the list 671 Status status = new Status(); 672 getSourceLocaleIdExtended(xpath, status, false, list); 673 final String path1 = status.pathWhereFound; 674 // For now, the only special case is Glossonym 675 if (path1.equals(GlossonymConstructor.PSEUDO_PATH)) { 676 // it's a Glossonym, so as the GlossonymConstructor what the paths are. Sort paths in 677 // reverse order. 678 final Set<String> xpaths = 679 new GlossonymConstructor(this) 680 .getPathsWhereFound( 681 xpath, new TreeSet<String>(Comparator.reverseOrder())); 682 for (final String subpath : xpaths) { 683 final String locale2 = getSourceLocaleIdExtended(subpath, status, true); 684 final String path2 = status.pathWhereFound; 685 // Paths are in reverse order (c-b-a) so we insert them at the top of our list. 686 list.addFirst(new LocaleInheritanceInfo(locale2, path2, Reason.constructed)); 687 } 688 689 // now the list contains: 690 // constructed: a 691 // constructed: b 692 // constructed: c 693 // (none) - this is where the glossonym was 694 // (bailey value(s)) 695 } 696 return list; 697 } 698 699 static final class SimpleAltPicker implements Transform<String, String> { 700 public final String alt; 701 SimpleAltPicker(String alt)702 public SimpleAltPicker(String alt) { 703 this.alt = alt; 704 } 705 706 @Override transform(@uppressWarnings"unused") String source)707 public String transform(@SuppressWarnings("unused") String source) { 708 return alt; 709 } 710 } 711 712 /** 713 * Only call if xpath doesn't exist in the current file. 714 * 715 * <p>For now, just handle counts and cases: see getCountPath Also handle extraPaths 716 * 717 * @param xpath 718 * @param winning TODO 719 * @param checkExtraPaths TODO 720 * @return 721 */ getFallbackPath(String xpath, boolean winning, boolean checkExtraPaths)722 private String getFallbackPath(String xpath, boolean winning, boolean checkExtraPaths) { 723 if (GrammaticalFeature.pathHasFeature(xpath) != null) { 724 return getCountPathWithFallback(xpath, Count.other, winning); 725 } 726 if (checkExtraPaths && getRawExtraPaths().contains(xpath)) { 727 return xpath; 728 } 729 return null; 730 } 731 732 /** 733 * Get the full path from a distinguished path. 734 * 735 * @param xpath the distinguished path 736 * @return the full path 737 * <p>Examples: 738 * <p>xpath = //ldml/localeDisplayNames/scripts/script[@type="Adlm"] result = 739 * //ldml/localeDisplayNames/scripts/script[@type="Adlm"][@draft="unconfirmed"] 740 * <p>xpath = 741 * //ldml/dates/calendars/calendar[@type="hebrew"]/dateFormats/dateFormatLength[@type="full"]/dateFormat[@type="standard"]/pattern[@type="standard"] 742 * result = 743 * //ldml/dates/calendars/calendar[@type="hebrew"]/dateFormats/dateFormatLength[@type="full"]/dateFormat[@type="standard"]/pattern[@type="standard"][@numbers="hebr"] 744 */ getFullXPath(String xpath)745 public String getFullXPath(String xpath) { 746 if (xpath == null) { 747 throw new NullPointerException("Null distinguishing xpath"); 748 } 749 String result = dataSource.getFullPath(xpath); 750 return result != null 751 ? result 752 : xpath; // we can't add any non-distinguishing values if there is nothing there. 753 // if (result == null && dataSource.isResolving()) { 754 // String fallback = getFallbackPath(xpath, true); 755 // if (fallback != null) { 756 // // TODO, add attributes from fallback into main 757 // result = xpath; 758 // } 759 // } 760 // return result; 761 } 762 763 /** 764 * Get the last modified date (if available) from a distinguished path. 765 * 766 * @return date or null if not available. 767 */ getLastModifiedDate(String xpath)768 public Date getLastModifiedDate(String xpath) { 769 return dataSource.getChangeDateAtDPath(xpath); 770 } 771 772 /** 773 * Find out where the value was found (for resolving locales). Returns {@link 774 * XMLSource#CODE_FALLBACK_ID} as the location if nothing is found 775 * 776 * @param distinguishedXPath path (must be distinguished!) 777 * @param status the distinguished path where the item was found. Pass in null if you don't 778 * care. 779 */ 780 @Override getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status)781 public String getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status) { 782 return getSourceLocaleIdExtended( 783 distinguishedXPath, status, true /* skipInheritanceMarker */); 784 } 785 786 /** 787 * Find out where the value was found (for resolving locales). Returns {@link 788 * XMLSource#CODE_FALLBACK_ID} as the location if nothing is found 789 * 790 * @param distinguishedXPath path (must be distinguished!) 791 * @param status the distinguished path where the item was found. Pass in null if you don't 792 * care. 793 * @param skipInheritanceMarker if true, skip sources in which value is INHERITANCE_MARKER 794 * @return the locale id as a string 795 */ getSourceLocaleIdExtended( String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker)796 public String getSourceLocaleIdExtended( 797 String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker) { 798 return getSourceLocaleIdExtended(distinguishedXPath, status, skipInheritanceMarker, null); 799 } 800 getSourceLocaleIdExtended( String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker, List<LocaleInheritanceInfo> list)801 public String getSourceLocaleIdExtended( 802 String distinguishedXPath, 803 CLDRFile.Status status, 804 boolean skipInheritanceMarker, 805 List<LocaleInheritanceInfo> list) { 806 String result = 807 dataSource.getSourceLocaleIdExtended( 808 distinguishedXPath, status, skipInheritanceMarker, list); 809 if (result == XMLSource.CODE_FALLBACK_ID && dataSource.isResolving()) { 810 final String fallbackPath = getFallbackPath(distinguishedXPath, false, true); 811 if (fallbackPath != null && !fallbackPath.equals(distinguishedXPath)) { 812 if (list != null) { 813 list.add( 814 new LocaleInheritanceInfo( 815 getLocaleID(), distinguishedXPath, Reason.fallback, null)); 816 } 817 result = 818 dataSource.getSourceLocaleIdExtended( 819 fallbackPath, status, skipInheritanceMarker, list); 820 } 821 if (result == XMLSource.CODE_FALLBACK_ID 822 && getConstructedValue(distinguishedXPath) != null) { 823 if (status != null) { 824 status.pathWhereFound = GlossonymConstructor.PSEUDO_PATH; 825 } 826 return getLocaleID(); 827 } 828 } 829 return result; 830 } 831 832 /** 833 * return true if the path in this file (without resolution) 834 * 835 * @param path 836 * @return 837 */ isHere(String path)838 public boolean isHere(String path) { 839 return dataSource.isHere(path); 840 } 841 842 /** 843 * Add a new element to a CLDRFile. 844 * 845 * @param currentFullXPath 846 * @param value 847 */ add(String currentFullXPath, String value)848 public CLDRFile add(String currentFullXPath, String value) { 849 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 850 // StringValue v = new StringValue(value, currentFullXPath); 851 Log.logln( 852 LOG_PROGRESS, 853 "ADDING: \t" + currentFullXPath + " \t" + value + "\t" + currentFullXPath); 854 // xpath = xpath.intern(); 855 try { 856 dataSource.putValueAtPath(currentFullXPath, value); 857 } catch (RuntimeException e) { 858 throw (IllegalArgumentException) 859 new IllegalArgumentException( 860 "failed adding " + currentFullXPath + ",\t" + value) 861 .initCause(e); 862 } 863 return this; 864 } 865 866 /** Note where this element was parsed. */ addSourceLocation(String currentFullXPath, XMLSource.SourceLocation location)867 public CLDRFile addSourceLocation(String currentFullXPath, XMLSource.SourceLocation location) { 868 dataSource.addSourceLocation(currentFullXPath, location); 869 return this; 870 } 871 872 /** 873 * Get the line and column for a path 874 * 875 * @param path xpath or fullpath 876 */ getSourceLocation(String path)877 public XMLSource.SourceLocation getSourceLocation(String path) { 878 final String fullPath = getFullXPath(path); 879 return dataSource.getSourceLocation(fullPath); 880 } 881 addComment(String xpath, String comment, Comments.CommentType type)882 public CLDRFile addComment(String xpath, String comment, Comments.CommentType type) { 883 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 884 // System.out.println("Adding comment: <" + xpath + "> '" + comment + "'"); 885 Log.logln(LOG_PROGRESS, "ADDING Comment: \t" + type + "\t" + xpath + " \t" + comment); 886 if (xpath == null || xpath.length() == 0) { 887 dataSource 888 .getXpathComments() 889 .setFinalComment( 890 CldrUtility.joinWithSeparation( 891 dataSource.getXpathComments().getFinalComment(), 892 XPathParts.NEWLINE, 893 comment)); 894 } else { 895 xpath = getDistinguishingXPath(xpath, null); 896 dataSource.getXpathComments().addComment(type, xpath, comment); 897 } 898 return this; 899 } 900 901 // TODO Change into enum, update docs 902 public static final int MERGE_KEEP_MINE = 0, 903 MERGE_REPLACE_MINE = 1, 904 MERGE_ADD_ALTERNATE = 2, 905 MERGE_REPLACE_MY_DRAFT = 3; 906 907 /** 908 * Merges elements from another CLDR file. Note: when both have the same xpath key, the keepMine 909 * determines whether "my" values are kept or the other files values are kept. 910 * 911 * @param other 912 * @param conflict_resolution 913 */ putAll(CLDRFile other, int conflict_resolution)914 public CLDRFile putAll(CLDRFile other, int conflict_resolution) { 915 916 if (locked) { 917 throw new UnsupportedOperationException("Attempt to modify locked object"); 918 } 919 if (conflict_resolution == MERGE_KEEP_MINE) { 920 dataSource.putAll(other.dataSource, MERGE_KEEP_MINE); 921 } else if (conflict_resolution == MERGE_REPLACE_MINE) { 922 dataSource.putAll(other.dataSource, MERGE_REPLACE_MINE); 923 } else if (conflict_resolution == MERGE_REPLACE_MY_DRAFT) { 924 // first find all my alt=..proposed items 925 Set<String> hasDraftVersion = new HashSet<>(); 926 for (Iterator<String> it = dataSource.iterator(); it.hasNext(); ) { 927 String cpath = it.next(); 928 String fullpath = getFullXPath(cpath); 929 if (fullpath.indexOf("[@draft") >= 0) { 930 hasDraftVersion.add( 931 getNondraftNonaltXPath(cpath)); // strips the alt and the draft 932 } 933 } 934 // only replace draft items! 935 // this is either an item with draft in the fullpath 936 // or an item with draft and alt in the full path 937 for (Iterator<String> it = other.iterator(); it.hasNext(); ) { 938 String cpath = it.next(); 939 cpath = getNondraftNonaltXPath(cpath); 940 String newValue = other.getStringValue(cpath); 941 String newFullPath = getNondraftNonaltXPath(other.getFullXPath(cpath)); 942 // another hack; need to add references back in 943 newFullPath = addReferencesIfNeeded(newFullPath, getFullXPath(cpath)); 944 945 if (!hasDraftVersion.contains(cpath)) { 946 if (cpath.startsWith("//ldml/identity/")) 947 continue; // skip, since the error msg is not needed. 948 String myVersion = getStringValue(cpath); 949 if (myVersion == null || !newValue.equals(myVersion)) { 950 Log.logln( 951 getLocaleID() 952 + "\tDenied attempt to replace non-draft" 953 + CldrUtility.LINE_SEPARATOR 954 + "\tcurr: [" 955 + cpath 956 + ",\t" 957 + myVersion 958 + "]" 959 + CldrUtility.LINE_SEPARATOR 960 + "\twith: [" 961 + newValue 962 + "]"); 963 continue; 964 } 965 } 966 Log.logln(getLocaleID() + "\tVETTED: [" + newFullPath + ",\t" + newValue + "]"); 967 dataSource.putValueAtPath(newFullPath, newValue); 968 } 969 } else if (conflict_resolution == MERGE_ADD_ALTERNATE) { 970 for (Iterator<String> it = other.iterator(); it.hasNext(); ) { 971 String key = it.next(); 972 String otherValue = other.getStringValue(key); 973 String myValue = dataSource.getValueAtPath(key); 974 if (myValue == null) { 975 dataSource.putValueAtPath(other.getFullXPath(key), otherValue); 976 } else if (!(myValue.equals(otherValue) 977 && equalsIgnoringDraft(getFullXPath(key), other.getFullXPath(key))) 978 && !key.startsWith("//ldml/identity")) { 979 for (int i = 0; ; ++i) { 980 String prop = "proposed" + (i == 0 ? "" : String.valueOf(i)); 981 XPathParts parts = 982 XPathParts.getFrozenInstance(other.getFullXPath(key)) 983 .cloneAsThawed(); // not frozen, for addAttribut 984 String fullPath = parts.addAttribute("alt", prop).toString(); 985 String path = getDistinguishingXPath(fullPath, null); 986 if (dataSource.getValueAtPath(path) != null) { 987 continue; 988 } 989 dataSource.putValueAtPath(fullPath, otherValue); 990 break; 991 } 992 } 993 } 994 } else { 995 throw new IllegalArgumentException("Illegal operand: " + conflict_resolution); 996 } 997 998 dataSource 999 .getXpathComments() 1000 .setInitialComment( 1001 CldrUtility.joinWithSeparation( 1002 dataSource.getXpathComments().getInitialComment(), 1003 XPathParts.NEWLINE, 1004 other.dataSource.getXpathComments().getInitialComment())); 1005 dataSource 1006 .getXpathComments() 1007 .setFinalComment( 1008 CldrUtility.joinWithSeparation( 1009 dataSource.getXpathComments().getFinalComment(), 1010 XPathParts.NEWLINE, 1011 other.dataSource.getXpathComments().getFinalComment())); 1012 dataSource.getXpathComments().joinAll(other.dataSource.getXpathComments()); 1013 return this; 1014 } 1015 1016 /** */ addReferencesIfNeeded(String newFullPath, String fullXPath)1017 private String addReferencesIfNeeded(String newFullPath, String fullXPath) { 1018 if (fullXPath == null || fullXPath.indexOf("[@references=") < 0) { 1019 return newFullPath; 1020 } 1021 XPathParts parts = XPathParts.getFrozenInstance(fullXPath); 1022 String accummulatedReferences = null; 1023 for (int i = 0; i < parts.size(); ++i) { 1024 Map<String, String> attributes = parts.getAttributes(i); 1025 String references = attributes.get("references"); 1026 if (references == null) { 1027 continue; 1028 } 1029 if (accummulatedReferences == null) { 1030 accummulatedReferences = references; 1031 } else { 1032 accummulatedReferences += ", " + references; 1033 } 1034 } 1035 if (accummulatedReferences == null) { 1036 return newFullPath; 1037 } 1038 XPathParts newParts = XPathParts.getFrozenInstance(newFullPath); 1039 Map<String, String> attributes = newParts.getAttributes(newParts.size() - 1); 1040 String references = attributes.get("references"); 1041 if (references == null) references = accummulatedReferences; 1042 else references += ", " + accummulatedReferences; 1043 attributes.put("references", references); 1044 System.out.println( 1045 "Changing " + newFullPath + " plus " + fullXPath + " to " + newParts.toString()); 1046 return newParts.toString(); 1047 } 1048 1049 /** Removes an element from a CLDRFile. */ remove(String xpath)1050 public CLDRFile remove(String xpath) { 1051 remove(xpath, false); 1052 return this; 1053 } 1054 1055 /** Removes an element from a CLDRFile. */ remove(String xpath, boolean butComment)1056 public CLDRFile remove(String xpath, boolean butComment) { 1057 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 1058 if (butComment) { 1059 appendFinalComment( 1060 dataSource.getFullPath(xpath) + "::<" + dataSource.getValueAtPath(xpath) + ">"); 1061 } 1062 dataSource.removeValueAtPath(xpath); 1063 return this; 1064 } 1065 1066 /** Removes all xpaths from a CLDRFile. */ removeAll(Set<String> xpaths, boolean butComment)1067 public CLDRFile removeAll(Set<String> xpaths, boolean butComment) { 1068 if (butComment) appendFinalComment("Illegal attributes removed:"); 1069 for (Iterator<String> it = xpaths.iterator(); it.hasNext(); ) { 1070 remove(it.next(), butComment); 1071 } 1072 return this; 1073 } 1074 1075 /** Code should explicitly include CODE_FALLBACK */ 1076 public static final Pattern specialsToKeep = 1077 PatternCache.get( 1078 "/(" 1079 + "measurementSystemName" 1080 + "|codePattern" 1081 + "|calendar\\[\\@type\\=\"[^\"]*\"\\]/(?!dateTimeFormats/appendItems)" 1082 + // gregorian 1083 "|numbers/symbols/(decimal/group)" 1084 + "|timeZoneNames/(hourFormat|gmtFormat|regionFormat)" 1085 + "|pattern" 1086 + ")"); 1087 1088 public static final Pattern specialsToPushFromRoot = 1089 PatternCache.get( 1090 "/(" 1091 + "calendar\\[\\@type\\=\"gregorian\"\\]/" 1092 + "(?!fields)" 1093 + "(?!dateTimeFormats/appendItems)" 1094 + "(?!.*\\[@type=\"format\"].*\\[@type=\"narrow\"])" 1095 + "(?!.*\\[@type=\"stand-alone\"].*\\[@type=\"(abbreviated|wide)\"])" 1096 + "|numbers/symbols/(decimal/group)" 1097 + "|timeZoneNames/(hourFormat|gmtFormat|regionFormat)" 1098 + ")"); 1099 1100 private static final boolean MINIMIZE_ALT_PROPOSED = false; 1101 1102 public interface RetentionTest { 1103 public enum Retention { 1104 RETAIN, 1105 REMOVE, 1106 RETAIN_IF_DIFFERENT 1107 } 1108 getRetention(String path)1109 public Retention getRetention(String path); 1110 } 1111 1112 /** Removes all items with same value */ removeDuplicates( CLDRFile other, boolean butComment, RetentionTest keepIfMatches, Collection<String> removedItems)1113 public CLDRFile removeDuplicates( 1114 CLDRFile other, 1115 boolean butComment, 1116 RetentionTest keepIfMatches, 1117 Collection<String> removedItems) { 1118 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 1119 // Matcher specialPathMatcher = dontRemoveSpecials ? specialsToKeep.matcher("") : null; 1120 boolean first = true; 1121 if (removedItems == null) { 1122 removedItems = new ArrayList<>(); 1123 } else { 1124 removedItems.clear(); 1125 } 1126 Set<String> checked = new HashSet<>(); 1127 for (Iterator<String> it = iterator(); 1128 it.hasNext(); ) { // see what items we have that the other also has 1129 String curXpath = it.next(); 1130 boolean logicDuplicate = true; 1131 1132 if (!checked.contains(curXpath)) { 1133 // we compare logic Group and only remove when all are duplicate 1134 Set<String> logicGroups = LogicalGrouping.getPaths(this, curXpath); 1135 if (logicGroups != null) { 1136 Iterator<String> iter = logicGroups.iterator(); 1137 while (iter.hasNext() && logicDuplicate) { 1138 String xpath = iter.next(); 1139 switch (keepIfMatches.getRetention(xpath)) { 1140 case RETAIN: 1141 logicDuplicate = false; 1142 continue; 1143 case RETAIN_IF_DIFFERENT: 1144 String currentValue = dataSource.getValueAtPath(xpath); 1145 if (currentValue == null) { 1146 logicDuplicate = false; 1147 continue; 1148 } 1149 String otherXpath = xpath; 1150 String otherValue = other.dataSource.getValueAtPath(otherXpath); 1151 if (!currentValue.equals(otherValue)) { 1152 if (MINIMIZE_ALT_PROPOSED) { 1153 otherXpath = CLDRFile.getNondraftNonaltXPath(xpath); 1154 if (otherXpath.equals(xpath)) { 1155 logicDuplicate = false; 1156 continue; 1157 } 1158 otherValue = other.dataSource.getValueAtPath(otherXpath); 1159 if (!currentValue.equals(otherValue)) { 1160 logicDuplicate = false; 1161 continue; 1162 } 1163 } else { 1164 logicDuplicate = false; 1165 continue; 1166 } 1167 } 1168 String keepValue = 1169 XMLSource.getPathsAllowingDuplicates().get(xpath); 1170 if (keepValue != null && keepValue.equals(currentValue)) { 1171 logicDuplicate = false; 1172 continue; 1173 } 1174 // we've now established that the values are the same 1175 String currentFullXPath = dataSource.getFullPath(xpath); 1176 String otherFullXPath = other.dataSource.getFullPath(otherXpath); 1177 if (!equalsIgnoringDraft(currentFullXPath, otherFullXPath)) { 1178 logicDuplicate = false; 1179 continue; 1180 } 1181 if (DEBUG) { 1182 keepIfMatches.getRetention(xpath); 1183 } 1184 break; 1185 case REMOVE: 1186 if (DEBUG) { 1187 keepIfMatches.getRetention(xpath); 1188 } 1189 break; 1190 } 1191 } 1192 1193 if (first) { 1194 first = false; 1195 if (butComment) appendFinalComment("Duplicates removed:"); 1196 } 1197 } 1198 // we can't remove right away, since that disturbs the iterator. 1199 checked.addAll(logicGroups); 1200 if (logicDuplicate) { 1201 removedItems.addAll(logicGroups); 1202 } 1203 // remove(xpath, butComment); 1204 } 1205 } 1206 // now remove them safely 1207 for (String xpath : removedItems) { 1208 remove(xpath, butComment); 1209 } 1210 return this; 1211 } 1212 1213 /** 1214 * @return Returns the finalComment. 1215 */ getFinalComment()1216 public String getFinalComment() { 1217 return dataSource.getXpathComments().getFinalComment(); 1218 } 1219 1220 /** 1221 * @return Returns the finalComment. 1222 */ getInitialComment()1223 public String getInitialComment() { 1224 return dataSource.getXpathComments().getInitialComment(); 1225 } 1226 1227 /** 1228 * @return Returns the xpath_comments. Cloned for safety. 1229 */ getXpath_comments()1230 public XPathParts.Comments getXpath_comments() { 1231 return (XPathParts.Comments) dataSource.getXpathComments().clone(); 1232 } 1233 1234 /** 1235 * @return Returns the locale ID. In the case of a supplemental data file, it is 1236 * SUPPLEMENTAL_NAME. 1237 */ 1238 @Override getLocaleID()1239 public String getLocaleID() { 1240 return dataSource.getLocaleID(); 1241 } 1242 1243 /** 1244 * @return the Locale ID, as declared in the //ldml/identity element 1245 */ getLocaleIDFromIdentity()1246 public String getLocaleIDFromIdentity() { 1247 ULocale.Builder lb = new ULocale.Builder(); 1248 for (Iterator<String> i = iterator("//ldml/identity/"); i.hasNext(); ) { 1249 XPathParts xpp = XPathParts.getFrozenInstance(i.next()); 1250 String k = xpp.getElement(-1); 1251 String v = xpp.getAttributeValue(-1, "type"); 1252 if (k.equals("language")) { 1253 lb = lb.setLanguage(v); 1254 } else if (k.equals("script")) { 1255 lb = lb.setScript(v); 1256 } else if (k.equals("territory")) { 1257 lb = lb.setRegion(v); 1258 } else if (k.equals("variant")) { 1259 lb = lb.setVariant(v); 1260 } 1261 } 1262 return lb.build().toString(); // TODO: CLDRLocale ? 1263 } 1264 1265 /** 1266 * @see com.ibm.icu.util.Freezable#isFrozen() 1267 */ 1268 @Override isFrozen()1269 public synchronized boolean isFrozen() { 1270 return locked; 1271 } 1272 1273 /** 1274 * @see com.ibm.icu.util.Freezable#freeze() 1275 */ 1276 @Override freeze()1277 public synchronized CLDRFile freeze() { 1278 locked = true; 1279 dataSource.freeze(); 1280 return this; 1281 } 1282 clearComments()1283 public CLDRFile clearComments() { 1284 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 1285 dataSource.setXpathComments(new XPathParts.Comments()); 1286 return this; 1287 } 1288 1289 /** Sets a final comment, replacing everything that was there. */ setFinalComment(String comment)1290 public CLDRFile setFinalComment(String comment) { 1291 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 1292 dataSource.getXpathComments().setFinalComment(comment); 1293 return this; 1294 } 1295 1296 /** Adds a comment to the final list of comments. */ appendFinalComment(String comment)1297 public CLDRFile appendFinalComment(String comment) { 1298 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 1299 dataSource 1300 .getXpathComments() 1301 .setFinalComment( 1302 CldrUtility.joinWithSeparation( 1303 dataSource.getXpathComments().getFinalComment(), 1304 XPathParts.NEWLINE, 1305 comment)); 1306 return this; 1307 } 1308 1309 /** Sets the initial comment, replacing everything that was there. */ setInitialComment(String comment)1310 public CLDRFile setInitialComment(String comment) { 1311 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 1312 dataSource.getXpathComments().setInitialComment(comment); 1313 return this; 1314 } 1315 1316 // ========== STATIC UTILITIES ========== 1317 1318 /** 1319 * Utility to restrict to files matching a given regular expression. The expression does not 1320 * contain ".xml". Note that supplementalData is always skipped, and root is always included. 1321 */ getMatchingXMLFiles(File sourceDirs[], Matcher m)1322 public static Set<String> getMatchingXMLFiles(File sourceDirs[], Matcher m) { 1323 Set<String> s = new TreeSet<>(); 1324 1325 for (File dir : sourceDirs) { 1326 if (!dir.exists()) { 1327 throw new IllegalArgumentException("Directory doesn't exist:\t" + dir.getPath()); 1328 } 1329 if (!dir.isDirectory()) { 1330 throw new IllegalArgumentException( 1331 "Input isn't a file directory:\t" + dir.getPath()); 1332 } 1333 File[] files = dir.listFiles(); 1334 for (int i = 0; i < files.length; ++i) { 1335 String name = files[i].getName(); 1336 if (!name.endsWith(".xml") || name.startsWith(".")) continue; 1337 // if (name.startsWith(SUPPLEMENTAL_NAME)) continue; 1338 String locale = name.substring(0, name.length() - 4); // drop .xml 1339 if (!m.reset(locale).matches()) continue; 1340 s.add(locale); 1341 } 1342 } 1343 return s; 1344 } 1345 1346 @Override iterator()1347 public Iterator<String> iterator() { 1348 return dataSource.iterator(); 1349 } 1350 iterator(String prefix)1351 public synchronized Iterator<String> iterator(String prefix) { 1352 return dataSource.iterator(prefix); 1353 } 1354 iterator(Matcher pathFilter)1355 public Iterator<String> iterator(Matcher pathFilter) { 1356 return dataSource.iterator(pathFilter); 1357 } 1358 iterator(String prefix, Comparator<String> comparator)1359 public Iterator<String> iterator(String prefix, Comparator<String> comparator) { 1360 Iterator<String> it = 1361 (prefix == null || prefix.length() == 0) 1362 ? dataSource.iterator() 1363 : dataSource.iterator(prefix); 1364 if (comparator == null) return it; 1365 Set<String> orderedSet = new TreeSet<>(comparator); 1366 it.forEachRemaining(orderedSet::add); 1367 return orderedSet.iterator(); 1368 } 1369 fullIterable()1370 public Iterable<String> fullIterable() { 1371 return new FullIterable(this); 1372 } 1373 1374 public static class FullIterable implements Iterable<String>, SimpleIterator<String> { 1375 private final CLDRFile file; 1376 private final Iterator<String> fileIterator; 1377 private Iterator<String> extraPaths; 1378 FullIterable(CLDRFile file)1379 FullIterable(CLDRFile file) { 1380 this.file = file; 1381 this.fileIterator = file.iterator(); 1382 } 1383 1384 @Override iterator()1385 public Iterator<String> iterator() { 1386 return With.toIterator(this); 1387 } 1388 1389 @Override next()1390 public String next() { 1391 if (fileIterator.hasNext()) { 1392 return fileIterator.next(); 1393 } 1394 if (extraPaths == null) { 1395 extraPaths = file.getExtraPaths().iterator(); 1396 } 1397 if (extraPaths.hasNext()) { 1398 return extraPaths.next(); 1399 } 1400 return null; 1401 } 1402 } 1403 getDistinguishingXPath(String xpath, String[] normalizedPath)1404 public static String getDistinguishingXPath(String xpath, String[] normalizedPath) { 1405 return DistinguishedXPath.getDistinguishingXPath(xpath, normalizedPath); 1406 } 1407 equalsIgnoringDraft(String path1, String path2)1408 private static boolean equalsIgnoringDraft(String path1, String path2) { 1409 if (path1 == path2) { 1410 return true; 1411 } 1412 if (path1 == null || path2 == null) { 1413 return false; 1414 } 1415 // TODO: optimize 1416 if (path1.indexOf("[@draft=") < 0 && path2.indexOf("[@draft=") < 0) { 1417 return path1.equals(path2); 1418 } 1419 return getNondraftNonaltXPath(path1).equals(getNondraftNonaltXPath(path2)); 1420 } 1421 1422 /* 1423 * TODO: clarify the need for syncObject. 1424 * Formerly, an XPathParts object named "nondraftParts" was used for this purpose, but 1425 * there was no evident reason for it to be an XPathParts object rather than any other 1426 * kind of object. 1427 */ 1428 private static Object syncObject = new Object(); 1429 getNondraftNonaltXPath(String xpath)1430 public static String getNondraftNonaltXPath(String xpath) { 1431 if (xpath.indexOf("draft=\"") < 0 && xpath.indexOf("alt=\"") < 0) { 1432 return xpath; 1433 } 1434 synchronized (syncObject) { 1435 XPathParts parts = 1436 XPathParts.getFrozenInstance(xpath) 1437 .cloneAsThawed(); // can't be frozen since we call removeAttributes 1438 String restore; 1439 HashSet<String> toRemove = new HashSet<>(); 1440 for (int i = 0; i < parts.size(); ++i) { 1441 if (parts.getAttributeCount(i) == 0) { 1442 continue; 1443 } 1444 Map<String, String> attributes = parts.getAttributes(i); 1445 toRemove.clear(); 1446 restore = null; 1447 for (Iterator<String> it = attributes.keySet().iterator(); it.hasNext(); ) { 1448 String attribute = it.next(); 1449 if (attribute.equals("draft")) { 1450 toRemove.add(attribute); 1451 } else if (attribute.equals("alt")) { 1452 String value = attributes.get(attribute); 1453 int proposedPos = value.indexOf("proposed"); 1454 if (proposedPos >= 0) { 1455 toRemove.add(attribute); 1456 if (proposedPos > 0) { 1457 restore = 1458 value.substring( 1459 0, proposedPos - 1); // is of form xxx-proposedyyy 1460 } 1461 } 1462 } 1463 } 1464 parts.removeAttributes(i, toRemove); 1465 if (restore != null) { 1466 attributes.put("alt", restore); 1467 } 1468 } 1469 return parts.toString(); 1470 } 1471 } 1472 1473 /** 1474 * Determine if an attribute is a distinguishing attribute. 1475 * 1476 * @param elementName 1477 * @param attribute 1478 * @return 1479 */ isDistinguishing(DtdType type, String elementName, String attribute)1480 public static boolean isDistinguishing(DtdType type, String elementName, String attribute) { 1481 return DtdData.getInstance(type).isDistinguishing(elementName, attribute); 1482 } 1483 1484 /** Utility to create a validating XML reader. */ createXMLReader(boolean validating)1485 public static XMLReader createXMLReader(boolean validating) { 1486 String[] testList = { 1487 "org.apache.xerces.parsers.SAXParser", 1488 "org.apache.crimson.parser.XMLReaderImpl", 1489 "gnu.xml.aelfred2.XmlReader", 1490 "com.bluecast.xml.Piccolo", 1491 "oracle.xml.parser.v2.SAXParser", 1492 "" 1493 }; 1494 XMLReader result = null; 1495 for (int i = 0; i < testList.length; ++i) { 1496 try { 1497 result = 1498 (testList[i].length() != 0) 1499 ? XMLReaderFactory.createXMLReader(testList[i]) 1500 : XMLReaderFactory.createXMLReader(); 1501 result.setFeature("http://xml.org/sax/features/validation", validating); 1502 break; 1503 } catch (SAXException e1) { 1504 } 1505 } 1506 if (result == null) 1507 throw new NoClassDefFoundError( 1508 "No SAX parser is available, or unable to set validation correctly"); 1509 return result; 1510 } 1511 1512 /** 1513 * Return a directory to supplemental data used by this CLDRFile. If the CLDRFile is not 1514 * normally disk-based, the returned directory may be temporary and not guaranteed to exist past 1515 * the lifetime of the CLDRFile. The directory should be considered read-only. 1516 */ getSupplementalDirectory()1517 public File getSupplementalDirectory() { 1518 if (supplementalDirectory == null) { 1519 // ask CLDRConfig. 1520 supplementalDirectory = 1521 CLDRConfig.getInstance().getSupplementalDataInfo().getDirectory(); 1522 } 1523 return supplementalDirectory; 1524 } 1525 setSupplementalDirectory(File supplementalDirectory)1526 public CLDRFile setSupplementalDirectory(File supplementalDirectory) { 1527 this.supplementalDirectory = supplementalDirectory; 1528 return this; 1529 } 1530 1531 /** 1532 * Convenience function to return a list of XML files in the Supplemental directory. 1533 * 1534 * @return all files ending in ".xml" 1535 * @see #getSupplementalDirectory() 1536 */ getSupplementalXMLFiles()1537 public File[] getSupplementalXMLFiles() { 1538 return getSupplementalDirectory() 1539 .listFiles( 1540 new FilenameFilter() { 1541 @Override 1542 public boolean accept( 1543 @SuppressWarnings("unused") File dir, String name) { 1544 return name.endsWith(".xml"); 1545 } 1546 }); 1547 } 1548 1549 /** 1550 * Convenience function to return a specific supplemental file 1551 * 1552 * @param filename the file to return 1553 * @return the file (may not exist) 1554 * @see #getSupplementalDirectory() 1555 */ 1556 public File getSupplementalFile(String filename) { 1557 return new File(getSupplementalDirectory(), filename); 1558 } 1559 1560 public static boolean isSupplementalName(String localeName) { 1561 return SUPPLEMENTAL_NAMES.contains(localeName); 1562 } 1563 1564 // static String[] keys = {"calendar", "collation", "currency"}; 1565 // 1566 // static String[] calendar_keys = {"buddhist", "chinese", "gregorian", "hebrew", "islamic", 1567 // "islamic-civil", 1568 // "japanese"}; 1569 // static String[] collation_keys = {"phonebook", "traditional", "direct", "pinyin", "stroke", 1570 // "posix", "big5han", 1571 // "gb2312han"}; 1572 1573 /* */ 1574 /** 1575 * Value that contains a node. WARNING: this is not done yet, and may change. In particular, we 1576 * don't want to return a Node, since that is mutable, and makes caching unsafe!! 1577 */ 1578 /* 1579 * static public class NodeValue extends Value { 1580 * private Node nodeValue; 1581 */ 1582 /** 1583 * Creation. WARNING, may change. 1584 * 1585 * @param value 1586 * @param currentFullXPath 1587 */ 1588 /* 1589 * public NodeValue(Node value, String currentFullXPath) { 1590 * super(currentFullXPath); 1591 * this.nodeValue = value; 1592 * } 1593 */ 1594 /** boilerplate */ 1595 1596 /* 1597 * public boolean hasSameValue(Object other) { 1598 * if (super.hasSameValue(other)) return false; 1599 * return nodeValue.equals(((NodeValue)other).nodeValue); 1600 * } 1601 */ 1602 /** boilerplate */ 1603 /* 1604 * public String getStringValue() { 1605 * return nodeValue.toString(); 1606 * } 1607 * (non-Javadoc) 1608 * 1609 * @see org.unicode.cldr.util.CLDRFile.Value#changePath(java.lang.String) 1610 * 1611 * public Value changePath(String string) { 1612 * return new NodeValue(nodeValue, string); 1613 * } 1614 * } 1615 */ 1616 1617 private static class MyDeclHandler implements AllHandler { 1618 private static UnicodeSet whitespace = new UnicodeSet("[:whitespace:]"); 1619 private DraftStatus minimalDraftStatus; 1620 private static final boolean SHOW_START_END = false; 1621 private int commentStack; 1622 private boolean justPopped = false; 1623 private String lastChars = ""; 1624 // private String currentXPath = "/"; 1625 private String currentFullXPath = "/"; 1626 private String comment = null; 1627 private Map<String, String> attributeOrder; 1628 private DtdData dtdData; 1629 private CLDRFile target; 1630 private String lastActiveLeafNode; 1631 private String lastLeafNode; 1632 private int isSupplemental = -1; 1633 private int[] orderedCounter = 1634 new int[30]; // just make deep enough to handle any CLDR file. 1635 private String[] orderedString = 1636 new String[30]; // just make deep enough to handle any CLDR file. 1637 private int level = 0; 1638 private int overrideCount = 0; 1639 private Locator documentLocator = null; 1640 1641 MyDeclHandler(CLDRFile target, DraftStatus minimalDraftStatus) { 1642 this.target = target; 1643 this.minimalDraftStatus = minimalDraftStatus; 1644 } 1645 1646 private String show(Attributes attributes) { 1647 if (attributes == null) return "null"; 1648 String result = ""; 1649 for (int i = 0; i < attributes.getLength(); ++i) { 1650 String attribute = attributes.getQName(i); 1651 String value = attributes.getValue(i); 1652 result += "[@" + attribute + "=\"" + value + "\"]"; // TODO quote the value?? 1653 } 1654 return result; 1655 } 1656 1657 private void push(String qName, Attributes attributes) { 1658 // SHOW_ALL && 1659 Log.logln(LOG_PROGRESS, "push\t" + qName + "\t" + show(attributes)); 1660 ++level; 1661 if (!qName.equals(orderedString[level])) { 1662 // orderedCounter[level] = 0; 1663 orderedString[level] = qName; 1664 } 1665 if (lastChars.length() != 0) { 1666 if (whitespace.containsAll(lastChars)) lastChars = ""; 1667 else 1668 throw new IllegalArgumentException( 1669 "Must not have mixed content: " 1670 + qName 1671 + ", " 1672 + show(attributes) 1673 + ", Content: " 1674 + lastChars); 1675 } 1676 // currentXPath += "/" + qName; 1677 currentFullXPath += "/" + qName; 1678 // if (!isSupplemental) ldmlComparator.addElement(qName); 1679 if (dtdData.isOrdered(qName)) { 1680 currentFullXPath += orderingAttribute(); 1681 } 1682 if (attributes.getLength() > 0) { 1683 attributeOrder.clear(); 1684 for (int i = 0; i < attributes.getLength(); ++i) { 1685 String attribute = attributes.getQName(i); 1686 String value = attributes.getValue(i); 1687 1688 // if (!isSupplemental) ldmlComparator.addAttribute(attribute); // must do 1689 // BEFORE put 1690 // ldmlComparator.addValue(value); 1691 // special fix to remove version 1692 // <!ATTLIST version number CDATA #REQUIRED > 1693 // <!ATTLIST version cldrVersion CDATA #FIXED "24" > 1694 if (attribute.equals("cldrVersion") && (qName.equals("version"))) { 1695 ((SimpleXMLSource) target.dataSource) 1696 .setDtdVersionInfo(VersionInfo.getInstance(value)); 1697 } else { 1698 putAndFixDeprecatedAttribute(qName, attribute, value); 1699 } 1700 } 1701 for (Iterator<String> it = attributeOrder.keySet().iterator(); it.hasNext(); ) { 1702 String attribute = it.next(); 1703 String value = attributeOrder.get(attribute); 1704 String both = 1705 "[@" + attribute + "=\"" + value + "\"]"; // TODO quote the value?? 1706 currentFullXPath += both; 1707 // distinguishing = key, registry, alt, and type (except for the type attribute 1708 // on the elements 1709 // default and mapping). 1710 // if (isDistinguishing(qName, attribute)) { 1711 // currentXPath += both; 1712 // } 1713 } 1714 } 1715 if (comment != null) { 1716 if (currentFullXPath.equals("//ldml") 1717 || currentFullXPath.equals("//supplementalData")) { 1718 target.setInitialComment(comment); 1719 } else { 1720 target.addComment( 1721 currentFullXPath, comment, XPathParts.Comments.CommentType.PREBLOCK); 1722 } 1723 comment = null; 1724 } 1725 justPopped = false; 1726 lastActiveLeafNode = null; 1727 Log.logln(LOG_PROGRESS, "currentFullXPath\t" + currentFullXPath); 1728 } 1729 1730 private String orderingAttribute() { 1731 return "[@_q=\"" + (orderedCounter[level]++) + "\"]"; 1732 } 1733 1734 private void putAndFixDeprecatedAttribute(String element, String attribute, String value) { 1735 if (attribute.equals("draft")) { 1736 if (value.equals("true")) value = "approved"; 1737 else if (value.equals("false")) value = "unconfirmed"; 1738 } else if (attribute.equals("type")) { 1739 if (changedTypes.contains(element) 1740 && isSupplemental < 1) { // measurementSystem for example did not 1741 // change from 'type' to 'choice'. 1742 attribute = "choice"; 1743 } 1744 } 1745 // else if (element.equals("dateFormatItem")) { 1746 // if (attribute.equals("id")) { 1747 // String newValue = dateGenerator.getBaseSkeleton(value); 1748 // if (!fixedSkeletons.contains(newValue)) { 1749 // fixedSkeletons.add(newValue); 1750 // if (!value.equals(newValue)) { 1751 // System.out.println(value + " => " + newValue); 1752 // } 1753 // value = newValue; 1754 // } 1755 // } 1756 // } 1757 attributeOrder.put(attribute, value); 1758 } 1759 1760 // private Set<String> fixedSkeletons = new HashSet(); 1761 1762 // private DateTimePatternGenerator dateGenerator = 1763 // DateTimePatternGenerator.getEmptyInstance(); 1764 1765 /** Types which changed from 'type' to 'choice', but not in supplemental data. */ 1766 private static Set<String> changedTypes = 1767 new HashSet<>( 1768 Arrays.asList( 1769 new String[] { 1770 "abbreviationFallback", 1771 "default", 1772 "mapping", 1773 "measurementSystem", 1774 "preferenceOrdering" 1775 })); 1776 1777 Matcher draftMatcher = DRAFT_PATTERN.matcher(""); 1778 1779 /** 1780 * Adds a parsed XPath to the CLDRFile. 1781 * 1782 * @param fullXPath 1783 * @param value 1784 */ 1785 private void addPath(String fullXPath, String value) { 1786 String former = target.getStringValue(fullXPath); 1787 if (former != null) { 1788 String formerPath = target.getFullXPath(fullXPath); 1789 if (!former.equals(value) || !fullXPath.equals(formerPath)) { 1790 if (!fullXPath.startsWith("//ldml/identity/version") 1791 && !fullXPath.startsWith("//ldml/identity/generation")) { 1792 warnOnOverride(former, formerPath); 1793 } 1794 } 1795 } 1796 value = trimWhitespaceSpecial(value); 1797 target.add(fullXPath, value) 1798 .addSourceLocation(fullXPath, new XMLSource.SourceLocation(documentLocator)); 1799 } 1800 1801 private void pop(String qName) { 1802 Log.logln(LOG_PROGRESS, "pop\t" + qName); 1803 --level; 1804 1805 if (lastChars.length() != 0 || justPopped == false) { 1806 boolean acceptItem = minimalDraftStatus == DraftStatus.unconfirmed; 1807 if (!acceptItem) { 1808 if (draftMatcher.reset(currentFullXPath).find()) { 1809 DraftStatus foundStatus = DraftStatus.valueOf(draftMatcher.group(1)); 1810 if (minimalDraftStatus.compareTo(foundStatus) <= 0) { 1811 // what we found is greater than or equal to our status 1812 acceptItem = true; 1813 } 1814 } else { 1815 acceptItem = 1816 true; // if not found, then the draft status is approved, so it is 1817 // always ok 1818 } 1819 } 1820 if (acceptItem) { 1821 // Change any deprecated orientation attributes into values 1822 // for backwards compatibility. 1823 boolean skipAdd = false; 1824 if (currentFullXPath.startsWith("//ldml/layout/orientation")) { 1825 XPathParts parts = XPathParts.getFrozenInstance(currentFullXPath); 1826 String value = parts.getAttributeValue(-1, "characters"); 1827 if (value != null) { 1828 addPath("//ldml/layout/orientation/characterOrder", value); 1829 skipAdd = true; 1830 } 1831 value = parts.getAttributeValue(-1, "lines"); 1832 if (value != null) { 1833 addPath("//ldml/layout/orientation/lineOrder", value); 1834 skipAdd = true; 1835 } 1836 } 1837 if (!skipAdd) { 1838 addPath(currentFullXPath, lastChars); 1839 } 1840 lastLeafNode = lastActiveLeafNode = currentFullXPath; 1841 } 1842 lastChars = ""; 1843 } else { 1844 Log.logln( 1845 LOG_PROGRESS && lastActiveLeafNode != null, 1846 "pop: zeroing last leafNode: " + lastActiveLeafNode); 1847 lastActiveLeafNode = null; 1848 if (comment != null) { 1849 target.addComment( 1850 lastLeafNode, comment, XPathParts.Comments.CommentType.POSTBLOCK); 1851 comment = null; 1852 } 1853 } 1854 // currentXPath = stripAfter(currentXPath, qName); 1855 currentFullXPath = stripAfter(currentFullXPath, qName); 1856 justPopped = true; 1857 } 1858 1859 static Pattern WHITESPACE_WITH_LF = PatternCache.get("\\s*\\u000a\\s*"); 1860 Matcher whitespaceWithLf = WHITESPACE_WITH_LF.matcher(""); 1861 static final UnicodeSet CONTROLS = new UnicodeSet("[:cc:]"); 1862 1863 /** 1864 * Trim leading whitespace if there is a linefeed among them, then the same with trailing. 1865 * 1866 * @param source 1867 * @return 1868 */ 1869 private String trimWhitespaceSpecial(String source) { 1870 if (DEBUG && CONTROLS.containsSome(source)) { 1871 System.out.println("*** " + source); 1872 } 1873 if (!source.contains("\n")) { 1874 return source; 1875 } 1876 source = whitespaceWithLf.reset(source).replaceAll("\n"); 1877 return source; 1878 } 1879 1880 private void warnOnOverride(String former, String formerPath) { 1881 String distinguishing = CLDRFile.getDistinguishingXPath(formerPath, null); 1882 System.out.println( 1883 "\tERROR in " 1884 + target.getLocaleID() 1885 + ";\toverriding old value <" 1886 + former 1887 + "> at path " 1888 + distinguishing 1889 + "\twith\t<" 1890 + lastChars 1891 + ">" 1892 + CldrUtility.LINE_SEPARATOR 1893 + "\told fullpath: " 1894 + formerPath 1895 + CldrUtility.LINE_SEPARATOR 1896 + "\tnew fullpath: " 1897 + currentFullXPath); 1898 overrideCount += 1; 1899 } 1900 1901 private static String stripAfter(String input, String qName) { 1902 int pos = findLastSlash(input); 1903 if (qName != null) { 1904 // assert input.substring(pos+1).startsWith(qName); 1905 if (!input.substring(pos + 1).startsWith(qName)) { 1906 throw new IllegalArgumentException("Internal Error: should never get here."); 1907 } 1908 } 1909 return input.substring(0, pos); 1910 } 1911 1912 private static int findLastSlash(String input) { 1913 int braceStack = 0; 1914 char inQuote = 0; 1915 for (int i = input.length() - 1; i >= 0; --i) { 1916 char ch = input.charAt(i); 1917 switch (ch) { 1918 case '\'': 1919 case '"': 1920 if (inQuote == 0) { 1921 inQuote = ch; 1922 } else if (inQuote == ch) { 1923 inQuote = 0; // come out of quote 1924 } 1925 break; 1926 case '/': 1927 if (inQuote == 0 && braceStack == 0) { 1928 return i; 1929 } 1930 break; 1931 case '[': 1932 if (inQuote == 0) { 1933 --braceStack; 1934 } 1935 break; 1936 case ']': 1937 if (inQuote == 0) { 1938 ++braceStack; 1939 } 1940 break; 1941 } 1942 } 1943 return -1; 1944 } 1945 1946 // SAX items we need to catch 1947 1948 @Override 1949 public void startElement(String uri, String localName, String qName, Attributes attributes) 1950 throws SAXException { 1951 Log.logln( 1952 LOG_PROGRESS || SHOW_START_END, 1953 "startElement uri\t" 1954 + uri 1955 + "\tlocalName " 1956 + localName 1957 + "\tqName " 1958 + qName 1959 + "\tattributes " 1960 + show(attributes)); 1961 try { 1962 if (isSupplemental < 0) { // set by first element 1963 attributeOrder = 1964 new TreeMap<>( 1965 // HACK for ldmlIcu 1966 dtdData.dtdType == DtdType.ldml 1967 ? CLDRFile.getAttributeOrdering() 1968 : dtdData.getAttributeComparator()); 1969 isSupplemental = target.dtdType == DtdType.ldml ? 0 : 1; 1970 } 1971 push(qName, attributes); 1972 } catch (RuntimeException e) { 1973 e.printStackTrace(); 1974 throw e; 1975 } 1976 } 1977 1978 @Override 1979 public void endElement(String uri, String localName, String qName) throws SAXException { 1980 Log.logln( 1981 LOG_PROGRESS || SHOW_START_END, 1982 "endElement uri\t" + uri + "\tlocalName " + localName + "\tqName " + qName); 1983 try { 1984 pop(qName); 1985 } catch (RuntimeException e) { 1986 // e.printStackTrace(); 1987 throw e; 1988 } 1989 } 1990 1991 // static final char XML_LINESEPARATOR = (char) 0xA; 1992 // static final String XML_LINESEPARATOR_STRING = String.valueOf(XML_LINESEPARATOR); 1993 1994 @Override 1995 public void characters(char[] ch, int start, int length) throws SAXException { 1996 try { 1997 String value = new String(ch, start, length); 1998 Log.logln(LOG_PROGRESS, "characters:\t" + value); 1999 // we will strip leading and trailing line separators in another place. 2000 // if (value.indexOf(XML_LINESEPARATOR) >= 0) { 2001 // value = value.replace(XML_LINESEPARATOR, '\u0020'); 2002 // } 2003 lastChars += value; 2004 justPopped = false; 2005 } catch (RuntimeException e) { 2006 e.printStackTrace(); 2007 throw e; 2008 } 2009 } 2010 2011 @Override 2012 public void startDTD(String name, String publicId, String systemId) throws SAXException { 2013 Log.logln( 2014 LOG_PROGRESS, 2015 "startDTD name: " 2016 + name 2017 + ", publicId: " 2018 + publicId 2019 + ", systemId: " 2020 + systemId); 2021 commentStack++; 2022 target.dtdType = DtdType.fromElement(name); 2023 target.dtdData = dtdData = DtdData.getInstance(target.dtdType); 2024 } 2025 2026 @Override 2027 public void endDTD() throws SAXException { 2028 Log.logln(LOG_PROGRESS, "endDTD"); 2029 commentStack--; 2030 } 2031 2032 @Override 2033 public void comment(char[] ch, int start, int length) throws SAXException { 2034 final String string = new String(ch, start, length); 2035 Log.logln(LOG_PROGRESS, commentStack + " comment " + string); 2036 try { 2037 if (commentStack != 0) return; 2038 String comment0 = trimWhitespaceSpecial(string).trim(); 2039 if (lastActiveLeafNode != null) { 2040 target.addComment( 2041 lastActiveLeafNode, comment0, XPathParts.Comments.CommentType.LINE); 2042 } else { 2043 comment = 2044 (comment == null ? comment0 : comment + XPathParts.NEWLINE + comment0); 2045 } 2046 } catch (RuntimeException e) { 2047 e.printStackTrace(); 2048 throw e; 2049 } 2050 } 2051 2052 @Override 2053 public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { 2054 if (LOG_PROGRESS) 2055 Log.logln( 2056 LOG_PROGRESS, 2057 "ignorableWhitespace length: " 2058 + length 2059 + ": " 2060 + Utility.hex(new String(ch, start, length))); 2061 // if (lastActiveLeafNode != null) { 2062 for (int i = start; i < start + length; ++i) { 2063 if (ch[i] == '\n') { 2064 Log.logln( 2065 LOG_PROGRESS && lastActiveLeafNode != null, 2066 "\\n: zeroing last leafNode: " + lastActiveLeafNode); 2067 lastActiveLeafNode = null; 2068 break; 2069 } 2070 } 2071 // } 2072 } 2073 2074 @Override 2075 public void startDocument() throws SAXException { 2076 Log.logln(LOG_PROGRESS, "startDocument"); 2077 commentStack = 0; // initialize 2078 } 2079 2080 @Override 2081 public void endDocument() throws SAXException { 2082 Log.logln(LOG_PROGRESS, "endDocument"); 2083 try { 2084 if (comment != null) 2085 target.addComment(null, comment, XPathParts.Comments.CommentType.LINE); 2086 } catch (RuntimeException e) { 2087 e.printStackTrace(); 2088 throw e; 2089 } 2090 } 2091 2092 // ==== The following are just for debuggin ===== 2093 2094 @Override 2095 public void elementDecl(String name, String model) throws SAXException { 2096 Log.logln(LOG_PROGRESS, "Attribute\t" + name + "\t" + model); 2097 } 2098 2099 @Override 2100 public void attributeDecl( 2101 String eName, String aName, String type, String mode, String value) 2102 throws SAXException { 2103 Log.logln( 2104 LOG_PROGRESS, 2105 "Attribute\t" 2106 + eName 2107 + "\t" 2108 + aName 2109 + "\t" 2110 + type 2111 + "\t" 2112 + mode 2113 + "\t" 2114 + value); 2115 } 2116 2117 @Override 2118 public void internalEntityDecl(String name, String value) throws SAXException { 2119 Log.logln(LOG_PROGRESS, "Internal Entity\t" + name + "\t" + value); 2120 } 2121 2122 @Override 2123 public void externalEntityDecl(String name, String publicId, String systemId) 2124 throws SAXException { 2125 Log.logln(LOG_PROGRESS, "Internal Entity\t" + name + "\t" + publicId + "\t" + systemId); 2126 } 2127 2128 @Override 2129 public void processingInstruction(String target, String data) throws SAXException { 2130 Log.logln(LOG_PROGRESS, "processingInstruction: " + target + ", " + data); 2131 } 2132 2133 @Override 2134 public void skippedEntity(String name) throws SAXException { 2135 Log.logln(LOG_PROGRESS, "skippedEntity: " + name); 2136 } 2137 2138 @Override 2139 public void setDocumentLocator(Locator locator) { 2140 Log.logln(LOG_PROGRESS, "setDocumentLocator Locator " + locator); 2141 documentLocator = locator; 2142 } 2143 2144 @Override 2145 public void startPrefixMapping(String prefix, String uri) throws SAXException { 2146 Log.logln(LOG_PROGRESS, "startPrefixMapping prefix: " + prefix + ", uri: " + uri); 2147 } 2148 2149 @Override 2150 public void endPrefixMapping(String prefix) throws SAXException { 2151 Log.logln(LOG_PROGRESS, "endPrefixMapping prefix: " + prefix); 2152 } 2153 2154 @Override 2155 public void startEntity(String name) throws SAXException { 2156 Log.logln(LOG_PROGRESS, "startEntity name: " + name); 2157 } 2158 2159 @Override 2160 public void endEntity(String name) throws SAXException { 2161 Log.logln(LOG_PROGRESS, "endEntity name: " + name); 2162 } 2163 2164 @Override 2165 public void startCDATA() throws SAXException { 2166 Log.logln(LOG_PROGRESS, "startCDATA"); 2167 } 2168 2169 @Override 2170 public void endCDATA() throws SAXException { 2171 Log.logln(LOG_PROGRESS, "endCDATA"); 2172 } 2173 2174 /* 2175 * (non-Javadoc) 2176 * 2177 * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException) 2178 */ 2179 @Override 2180 public void error(SAXParseException exception) throws SAXException { 2181 Log.logln(LOG_PROGRESS || true, "error: " + showSAX(exception)); 2182 throw exception; 2183 } 2184 2185 /* 2186 * (non-Javadoc) 2187 * 2188 * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException) 2189 */ 2190 @Override 2191 public void fatalError(SAXParseException exception) throws SAXException { 2192 Log.logln(LOG_PROGRESS, "fatalError: " + showSAX(exception)); 2193 throw exception; 2194 } 2195 2196 /* 2197 * (non-Javadoc) 2198 * 2199 * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException) 2200 */ 2201 @Override 2202 public void warning(SAXParseException exception) throws SAXException { 2203 Log.logln(LOG_PROGRESS, "warning: " + showSAX(exception)); 2204 throw exception; 2205 } 2206 } 2207 2208 /** Show a SAX exception in a readable form. */ 2209 public static String showSAX(SAXParseException exception) { 2210 return exception.getMessage() 2211 + ";\t SystemID: " 2212 + exception.getSystemId() 2213 + ";\t PublicID: " 2214 + exception.getPublicId() 2215 + ";\t LineNumber: " 2216 + exception.getLineNumber() 2217 + ";\t ColumnNumber: " 2218 + exception.getColumnNumber(); 2219 } 2220 2221 /** Says whether the whole file is draft */ 2222 public boolean isDraft() { 2223 String item = iterator().next(); 2224 return item.startsWith("//ldml[@draft=\"unconfirmed\"]"); 2225 } 2226 2227 // public Collection keySet(Matcher regexMatcher, Collection output) { 2228 // if (output == null) output = new ArrayList(0); 2229 // for (Iterator it = keySet().iterator(); it.hasNext();) { 2230 // String path = (String)it.next(); 2231 // if (regexMatcher.reset(path).matches()) { 2232 // output.add(path); 2233 // } 2234 // } 2235 // return output; 2236 // } 2237 2238 // public Collection keySet(String regexPattern, Collection output) { 2239 // return keySet(PatternCache.get(regexPattern).matcher(""), output); 2240 // } 2241 2242 /** 2243 * Gets the type of a given xpath, eg script, territory, ... TODO move to separate class 2244 * 2245 * @param xpath 2246 * @return 2247 */ 2248 public static int getNameType(String xpath) { 2249 for (int i = 0; i < NameTable.length; ++i) { 2250 if (!xpath.startsWith(NameTable[i][0])) continue; 2251 if (xpath.indexOf(NameTable[i][1], NameTable[i][0].length()) >= 0) return i; 2252 } 2253 return -1; 2254 } 2255 2256 /** Gets the display name for a type */ 2257 public static String getNameTypeName(int index) { 2258 try { 2259 return getNameName(index); 2260 } catch (Exception e) { 2261 return "Illegal Type Name: " + index; 2262 } 2263 } 2264 2265 public static final int NO_NAME = -1, 2266 LANGUAGE_NAME = 0, 2267 SCRIPT_NAME = 1, 2268 TERRITORY_NAME = 2, 2269 VARIANT_NAME = 3, 2270 CURRENCY_NAME = 4, 2271 CURRENCY_SYMBOL = 5, 2272 TZ_EXEMPLAR = 6, 2273 TZ_START = TZ_EXEMPLAR, 2274 TZ_GENERIC_LONG = 7, 2275 TZ_GENERIC_SHORT = 8, 2276 TZ_STANDARD_LONG = 9, 2277 TZ_STANDARD_SHORT = 10, 2278 TZ_DAYLIGHT_LONG = 11, 2279 TZ_DAYLIGHT_SHORT = 12, 2280 TZ_LIMIT = 13, 2281 KEY_NAME = 13, 2282 KEY_TYPE_NAME = 14, 2283 SUBDIVISION_NAME = 15, 2284 LIMIT_TYPES = 15; 2285 2286 private static final String[][] NameTable = { 2287 {"//ldml/localeDisplayNames/languages/language[@type=\"", "\"]", "language"}, 2288 {"//ldml/localeDisplayNames/scripts/script[@type=\"", "\"]", "script"}, 2289 {"//ldml/localeDisplayNames/territories/territory[@type=\"", "\"]", "territory"}, 2290 {"//ldml/localeDisplayNames/variants/variant[@type=\"", "\"]", "variant"}, 2291 {"//ldml/numbers/currencies/currency[@type=\"", "\"]/displayName", "currency"}, 2292 {"//ldml/numbers/currencies/currency[@type=\"", "\"]/symbol", "currency-symbol"}, 2293 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/exemplarCity", "exemplar-city"}, 2294 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/generic", "tz-generic-long"}, 2295 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/generic", "tz-generic-short"}, 2296 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/standard", "tz-standard-long"}, 2297 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/standard", "tz-standard-short"}, 2298 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/daylight", "tz-daylight-long"}, 2299 {"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/daylight", "tz-daylight-short"}, 2300 {"//ldml/localeDisplayNames/keys/key[@type=\"", "\"]", "key"}, 2301 {"//ldml/localeDisplayNames/types/type[@key=\"", "\"][@type=\"", "\"]", "key|type"}, 2302 {"//ldml/localeDisplayNames/subdivisions/subdivision[@type=\"", "\"]", "subdivision"}, 2303 2304 /** 2305 * <long> <generic>Newfoundland Time</generic> <standard>Newfoundland Standard 2306 * Time</standard> <daylight>Newfoundland Daylight Time</daylight> </long> - <short> 2307 * <generic>NT</generic> <standard>NST</standard> <daylight>NDT</daylight> </short> 2308 */ 2309 }; 2310 2311 // private static final String[] TYPE_NAME = {"language", "script", "territory", "variant", 2312 // "currency", 2313 // "currency-symbol", 2314 // "tz-exemplar", 2315 // "tz-generic-long", "tz-generic-short"}; 2316 2317 public Iterator<String> getAvailableIterator(int type) { 2318 return iterator(NameTable[type][0]); 2319 } 2320 2321 /** 2322 * @return the xpath used to access data of a given type 2323 */ 2324 public static String getKey(int type, String code) { 2325 switch (type) { 2326 case VARIANT_NAME: 2327 code = code.toUpperCase(Locale.ROOT); 2328 break; 2329 case KEY_NAME: 2330 code = fixKeyName(code); 2331 break; 2332 case TZ_DAYLIGHT_LONG: 2333 case TZ_DAYLIGHT_SHORT: 2334 case TZ_EXEMPLAR: 2335 case TZ_GENERIC_LONG: 2336 case TZ_GENERIC_SHORT: 2337 case TZ_STANDARD_LONG: 2338 case TZ_STANDARD_SHORT: 2339 code = getLongTzid(code); 2340 break; 2341 } 2342 String[] nameTableRow = NameTable[type]; 2343 if (code.contains("|")) { 2344 String[] codes = code.split("\\|"); 2345 return nameTableRow[0] 2346 + fixKeyName(codes[0]) 2347 + nameTableRow[1] 2348 + codes[1] 2349 + nameTableRow[2]; 2350 } else { 2351 return nameTableRow[0] + code + nameTableRow[1]; 2352 } 2353 } 2354 2355 static final Relation<R2<String, String>, String> bcp47AliasMap = 2356 CLDRConfig.getInstance().getSupplementalDataInfo().getBcp47Aliases(); 2357 2358 public static String getLongTzid(String code) { 2359 if (!code.contains("/")) { 2360 Set<String> codes = bcp47AliasMap.get(Row.of("tz", code)); 2361 if (codes != null && !codes.isEmpty()) { 2362 code = codes.iterator().next(); 2363 } 2364 } 2365 return code; 2366 } 2367 2368 static final ImmutableMap<String, String> FIX_KEY_NAME; 2369 2370 static { 2371 Builder<String, String> temp = ImmutableMap.builder(); 2372 for (String s : 2373 Arrays.asList( 2374 "colAlternate", 2375 "colBackwards", 2376 "colCaseFirst", 2377 "colCaseLevel", 2378 "colNormalization", 2379 "colNumeric", 2380 "colReorder", 2381 "colStrength")) { 2382 temp.put(s.toLowerCase(Locale.ROOT), s); 2383 } 2384 FIX_KEY_NAME = temp.build(); 2385 } 2386 2387 private static String fixKeyName(String code) { 2388 String result = FIX_KEY_NAME.get(code); 2389 return result == null ? code : result; 2390 } 2391 2392 /** 2393 * @return the code used to access data of a given type from the path. Null if not found. 2394 */ 2395 public static String getCode(String path) { 2396 int type = getNameType(path); 2397 if (type < 0) { 2398 throw new IllegalArgumentException("Illegal type in path: " + path); 2399 } 2400 String[] nameTableRow = NameTable[type]; 2401 int start = nameTableRow[0].length(); 2402 int end = path.indexOf(nameTableRow[1], start); 2403 return path.substring(start, end); 2404 } 2405 2406 /** 2407 * @param type a string such as "language", "script", "territory", "region", ... 2408 * @return the corresponding integer 2409 */ 2410 public static int typeNameToCode(String type) { 2411 if (type.equalsIgnoreCase("region")) { 2412 type = "territory"; 2413 } 2414 for (int i = 0; i < LIMIT_TYPES; ++i) { 2415 if (type.equalsIgnoreCase(getNameName(i))) { 2416 return i; 2417 } 2418 } 2419 return -1; 2420 } 2421 2422 /** For use in getting short names. */ 2423 public static final Transform<String, String> SHORT_ALTS = 2424 new Transform<>() { 2425 @Override 2426 public String transform(@SuppressWarnings("unused") String source) { 2427 return "short"; 2428 } 2429 }; 2430 2431 /** Returns the name of a type. */ 2432 public static String getNameName(int choice) { 2433 String[] nameTableRow = NameTable[choice]; 2434 return nameTableRow[nameTableRow.length - 1]; 2435 } 2436 2437 /** 2438 * Get standard ordering for elements. 2439 * 2440 * @return ordered collection with items. 2441 * @deprecated 2442 */ 2443 @Deprecated 2444 public static List<String> getElementOrder() { 2445 return Collections.emptyList(); // elementOrdering.getOrder(); // already unmodifiable 2446 } 2447 2448 /** 2449 * Get standard ordering for attributes. 2450 * 2451 * @return ordered collection with items. 2452 */ 2453 public static List<String> getAttributeOrder() { 2454 return getAttributeOrdering().getOrder(); // already unmodifiable 2455 } 2456 2457 public static boolean isOrdered(String element, DtdType type) { 2458 return DtdData.getInstance(type).isOrdered(element); 2459 } 2460 2461 private static Comparator<String> ldmlComparator = 2462 DtdData.getInstance(DtdType.ldmlICU).getDtdComparator(null); 2463 2464 private static final Map<String, Map<String, String>> defaultSuppressionMap; 2465 2466 static { 2467 String[][] data = { 2468 {"ldml", "version", GEN_VERSION}, 2469 {"version", "cldrVersion", "*"}, 2470 {"orientation", "characters", "left-to-right"}, 2471 {"orientation", "lines", "top-to-bottom"}, 2472 {"weekendStart", "time", "00:00"}, 2473 {"weekendEnd", "time", "24:00"}, 2474 {"dateFormat", "type", "standard"}, 2475 {"timeFormat", "type", "standard"}, 2476 {"dateTimeFormat", "type", "standard"}, 2477 {"decimalFormat", "type", "standard"}, 2478 {"scientificFormat", "type", "standard"}, 2479 {"percentFormat", "type", "standard"}, 2480 {"pattern", "type", "standard"}, 2481 {"currency", "type", "standard"}, 2482 {"transform", "visibility", "external"}, 2483 {"*", "_q", "*"}, 2484 }; 2485 Map<String, Map<String, String>> tempmain = asMap(data, true); 2486 defaultSuppressionMap = Collections.unmodifiableMap(tempmain); 2487 } 2488 2489 public static Map<String, Map<String, String>> getDefaultSuppressionMap() { 2490 return defaultSuppressionMap; 2491 } 2492 2493 @SuppressWarnings({"rawtypes", "unchecked"}) 2494 private static Map asMap(String[][] data, boolean tree) { 2495 Map tempmain = tree ? (Map) new TreeMap() : new HashMap(); 2496 int len = data[0].length; // must be same for all elements 2497 for (int i = 0; i < data.length; ++i) { 2498 Map temp = tempmain; 2499 if (len != data[i].length) { 2500 throw new IllegalArgumentException("Must be square array: fails row " + i); 2501 } 2502 for (int j = 0; j < len - 2; ++j) { 2503 Map newTemp = (Map) temp.get(data[i][j]); 2504 if (newTemp == null) { 2505 temp.put(data[i][j], newTemp = tree ? (Map) new TreeMap() : new HashMap()); 2506 } 2507 temp = newTemp; 2508 } 2509 temp.put(data[i][len - 2], data[i][len - 1]); 2510 } 2511 return tempmain; 2512 } 2513 2514 /** Removes a comment. */ 2515 public CLDRFile removeComment(String string) { 2516 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 2517 dataSource.getXpathComments().removeComment(string); 2518 return this; 2519 } 2520 2521 /** 2522 * @param draftStatus TODO 2523 */ 2524 public CLDRFile makeDraft(DraftStatus draftStatus) { 2525 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 2526 for (Iterator<String> it = dataSource.iterator(); it.hasNext(); ) { 2527 String path = it.next(); 2528 XPathParts parts = 2529 XPathParts.getFrozenInstance(dataSource.getFullPath(path)) 2530 .cloneAsThawed(); // not frozen, for addAttribute 2531 parts.addAttribute("draft", draftStatus.toString()); 2532 dataSource.putValueAtPath(parts.toString(), dataSource.getValueAtPath(path)); 2533 } 2534 return this; 2535 } 2536 2537 public UnicodeSet getExemplarSet(String type, WinningChoice winningChoice) { 2538 return getExemplarSet(type, winningChoice, UnicodeSet.CASE); 2539 } 2540 2541 public UnicodeSet getExemplarSet(ExemplarType type, WinningChoice winningChoice) { 2542 return getExemplarSet(type, winningChoice, UnicodeSet.CASE); 2543 } 2544 2545 static final UnicodeSet HACK_CASE_CLOSURE_SET = 2546 new UnicodeSet( 2547 "[ſẛffẞ{i̇}\u1F71\u1F73\u1F75\u1F77\u1F79\u1F7B\u1F7D\u1FBB\u1FBE\u1FC9\u1FCB\u1FD3\u1FDB\u1FE3\u1FEB\u1FF9\u1FFB\u2126\u212A\u212B]") 2548 .freeze(); 2549 2550 public enum ExemplarType { 2551 main, 2552 auxiliary, 2553 index, 2554 punctuation, 2555 numbers; 2556 2557 public static ExemplarType fromString(String type) { 2558 return type.isEmpty() ? main : valueOf(type); 2559 } 2560 } 2561 2562 public UnicodeSet getExemplarSet(String type, WinningChoice winningChoice, int option) { 2563 return getExemplarSet(ExemplarType.fromString(type), winningChoice, option); 2564 } 2565 2566 public UnicodeSet getExemplarSet(ExemplarType type, WinningChoice winningChoice, int option) { 2567 UnicodeSet result = getRawExemplarSet(type, winningChoice); 2568 if (result.isEmpty()) { 2569 return result.cloneAsThawed(); 2570 } 2571 UnicodeSet toNuke = new UnicodeSet(HACK_CASE_CLOSURE_SET).removeAll(result); 2572 result.closeOver(UnicodeSet.CASE); 2573 result.removeAll(toNuke); 2574 result.remove(0x20); 2575 return result; 2576 } 2577 2578 public UnicodeSet getRawExemplarSet(ExemplarType type, WinningChoice winningChoice) { 2579 String path = getExemplarPath(type); 2580 if (winningChoice == WinningChoice.WINNING) { 2581 path = getWinningPath(path); 2582 } 2583 String v = getStringValueWithBailey(path); 2584 if (v == null) { 2585 return UnicodeSet.EMPTY; 2586 } 2587 UnicodeSet result = SimpleUnicodeSetFormatter.parseLenient(v); 2588 return result; 2589 } 2590 2591 public static String getExemplarPath(ExemplarType type) { 2592 return "//ldml/characters/exemplarCharacters" 2593 + (type == ExemplarType.main ? "" : "[@type=\"" + type + "\"]"); 2594 } 2595 2596 public enum NumberingSystem { 2597 latin(null), 2598 defaultSystem("//ldml/numbers/defaultNumberingSystem"), 2599 nativeSystem("//ldml/numbers/otherNumberingSystems/native"), 2600 traditional("//ldml/numbers/otherNumberingSystems/traditional"), 2601 finance("//ldml/numbers/otherNumberingSystems/finance"); 2602 public final String path; 2603 2604 private NumberingSystem(String path) { 2605 this.path = path; 2606 } 2607 } 2608 2609 public UnicodeSet getExemplarsNumeric(NumberingSystem system) { 2610 String numberingSystem = system.path == null ? "latn" : getStringValue(system.path); 2611 if (numberingSystem == null) { 2612 return UnicodeSet.EMPTY; 2613 } 2614 return getExemplarsNumeric(numberingSystem); 2615 } 2616 2617 public UnicodeSet getExemplarsNumeric(String numberingSystem) { 2618 UnicodeSet result = new UnicodeSet(); 2619 SupplementalDataInfo sdi = CLDRConfig.getInstance().getSupplementalDataInfo(); 2620 String[] symbolPaths = { 2621 "decimal", "group", "percentSign", "perMille", "plusSign", "minusSign", 2622 // "infinity" 2623 }; 2624 2625 String digits = sdi.getDigits(numberingSystem); 2626 if (digits != null) { // TODO, get other characters, see ticket:8316 2627 result.addAll(digits); 2628 } 2629 for (String path : symbolPaths) { 2630 String fullPath = 2631 "//ldml/numbers/symbols[@numberSystem=\"" + numberingSystem + "\"]/" + path; 2632 String value = getStringValue(fullPath); 2633 if (value != null) { 2634 result.add(value); 2635 } 2636 } 2637 2638 return result; 2639 } 2640 2641 public String getCurrentMetazone(String zone) { 2642 for (Iterator<String> it2 = iterator(); it2.hasNext(); ) { 2643 String xpath = it2.next(); 2644 if (xpath.startsWith( 2645 "//ldml/dates/timeZoneNames/zone[@type=\"" + zone + "\"]/usesMetazone")) { 2646 XPathParts parts = XPathParts.getFrozenInstance(xpath); 2647 if (!parts.containsAttribute("to")) { 2648 return parts.getAttributeValue(4, "mzone"); 2649 } 2650 } 2651 } 2652 return null; 2653 } 2654 2655 public boolean isResolved() { 2656 return dataSource.isResolving(); 2657 } 2658 2659 // WARNING: this must go AFTER attributeOrdering is set; otherwise it uses a null comparator!! 2660 /* 2661 * TODO: clarify the warning. There is nothing named "attributeOrdering" in this file. 2662 * This member distinguishedXPath is accessed only by the function getNonDistinguishingAttributes. 2663 */ 2664 private static final DistinguishedXPath distinguishedXPath = new DistinguishedXPath(); 2665 2666 public static final String distinguishedXPathStats() { 2667 return DistinguishedXPath.stats(); 2668 } 2669 2670 private static class DistinguishedXPath { 2671 2672 public static final String stats() { 2673 return "distinguishingMap:" 2674 + distinguishingMap.size() 2675 + " " 2676 + "normalizedPathMap:" 2677 + normalizedPathMap.size(); 2678 } 2679 2680 private static Map<String, String> distinguishingMap = new ConcurrentHashMap<>(); 2681 private static Map<String, String> normalizedPathMap = new ConcurrentHashMap<>(); 2682 2683 static { 2684 distinguishingMap.put("", ""); // seed this to make the code simpler 2685 } 2686 2687 public static String getDistinguishingXPath(String xpath, String[] normalizedPath) { 2688 // For example, this removes [@xml:space="preserve"] from a path with element 2689 // foreignSpaceReplacement. 2690 // synchronized (distinguishingMap) { 2691 String result = distinguishingMap.get(xpath); 2692 if (result == null) { 2693 XPathParts distinguishingParts = 2694 XPathParts.getFrozenInstance(xpath) 2695 .cloneAsThawed(); // not frozen, for removeAttributes 2696 2697 DtdType type = distinguishingParts.getDtdData().dtdType; 2698 Set<String> toRemove = new HashSet<>(); 2699 2700 // first clean up draft and alt 2701 String draft = null; 2702 String alt = null; 2703 String references = ""; 2704 // note: we only need to clean up items that are NOT on the last element, 2705 // so we go up to size() - 1. 2706 2707 // note: each successive item overrides the previous one. That's intended 2708 2709 for (int i = 0; i < distinguishingParts.size() - 1; ++i) { 2710 if (distinguishingParts.getAttributeCount(i) == 0) { 2711 continue; 2712 } 2713 toRemove.clear(); 2714 Map<String, String> attributes = distinguishingParts.getAttributes(i); 2715 for (String attribute : attributes.keySet()) { 2716 if (attribute.equals("draft")) { 2717 draft = attributes.get(attribute); 2718 toRemove.add(attribute); 2719 } else if (attribute.equals("alt")) { 2720 alt = attributes.get(attribute); 2721 toRemove.add(attribute); 2722 } else if (attribute.equals("references")) { 2723 if (references.length() != 0) references += " "; 2724 references += attributes.get("references"); 2725 toRemove.add(attribute); 2726 } 2727 } 2728 distinguishingParts.removeAttributes(i, toRemove); 2729 } 2730 if (draft != null || alt != null || references.length() != 0) { 2731 // get the last element that is not ordered. 2732 int placementIndex = distinguishingParts.size() - 1; 2733 while (true) { 2734 String element = distinguishingParts.getElement(placementIndex); 2735 if (!DtdData.getInstance(type).isOrdered(element)) break; 2736 --placementIndex; 2737 } 2738 if (draft != null) { 2739 distinguishingParts.putAttributeValue(placementIndex, "draft", draft); 2740 } 2741 if (alt != null) { 2742 distinguishingParts.putAttributeValue(placementIndex, "alt", alt); 2743 } 2744 if (references.length() != 0) { 2745 distinguishingParts.putAttributeValue( 2746 placementIndex, "references", references); 2747 } 2748 String newXPath = distinguishingParts.toString(); 2749 if (!newXPath.equals(xpath)) { 2750 normalizedPathMap.put(xpath, newXPath); // store differences 2751 } 2752 } 2753 2754 // now remove non-distinguishing attributes (if non-inheriting) 2755 for (int i = 0; i < distinguishingParts.size(); ++i) { 2756 if (distinguishingParts.getAttributeCount(i) == 0) { 2757 continue; 2758 } 2759 String element = distinguishingParts.getElement(i); 2760 toRemove.clear(); 2761 for (String attribute : distinguishingParts.getAttributeKeys(i)) { 2762 if (!isDistinguishing(type, element, attribute)) { 2763 toRemove.add(attribute); 2764 } 2765 } 2766 distinguishingParts.removeAttributes(i, toRemove); 2767 } 2768 2769 result = distinguishingParts.toString(); 2770 if (result.equals(xpath)) { // don't save the copy if we don't have to. 2771 result = xpath; 2772 } 2773 distinguishingMap.put(xpath, result); 2774 } 2775 if (normalizedPath != null) { 2776 normalizedPath[0] = normalizedPathMap.get(xpath); 2777 if (normalizedPath[0] == null) { 2778 normalizedPath[0] = xpath; 2779 } 2780 } 2781 return result; 2782 } 2783 2784 public Map<String, String> getNonDistinguishingAttributes( 2785 String fullPath, Map<String, String> result, Set<String> skipList) { 2786 if (result == null) { 2787 result = new LinkedHashMap<>(); 2788 } else { 2789 result.clear(); 2790 } 2791 XPathParts distinguishingParts = XPathParts.getFrozenInstance(fullPath); 2792 DtdType type = distinguishingParts.getDtdData().dtdType; 2793 for (int i = 0; i < distinguishingParts.size(); ++i) { 2794 String element = distinguishingParts.getElement(i); 2795 Map<String, String> attributes = distinguishingParts.getAttributes(i); 2796 for (Iterator<String> it = attributes.keySet().iterator(); it.hasNext(); ) { 2797 String attribute = it.next(); 2798 if (!isDistinguishing(type, element, attribute) 2799 && !skipList.contains(attribute)) { 2800 result.put(attribute, attributes.get(attribute)); 2801 } 2802 } 2803 } 2804 return result; 2805 } 2806 } 2807 2808 /** Fillin value for {@link CLDRFile#getSourceLocaleID(String, Status)} */ 2809 public static class Status { 2810 /** 2811 * XPath where originally found. May be {@link GlossonymConstructor#PSEUDO_PATH} if the 2812 * value was constructed. 2813 * 2814 * @see GlossonymnConstructor 2815 */ 2816 public String pathWhereFound; 2817 2818 @Override 2819 public String toString() { 2820 return pathWhereFound; 2821 } 2822 } 2823 2824 public static boolean isLOG_PROGRESS() { 2825 return LOG_PROGRESS; 2826 } 2827 2828 public static void setLOG_PROGRESS(boolean log_progress) { 2829 LOG_PROGRESS = log_progress; 2830 } 2831 2832 public boolean isEmpty() { 2833 return !dataSource.iterator().hasNext(); 2834 } 2835 2836 public Map<String, String> getNonDistinguishingAttributes( 2837 String fullPath, Map<String, String> result, Set<String> skipList) { 2838 return distinguishedXPath.getNonDistinguishingAttributes(fullPath, result, skipList); 2839 } 2840 2841 public String getDtdVersion() { 2842 return dataSource.getDtdVersionInfo().toString(); 2843 } 2844 2845 public VersionInfo getDtdVersionInfo() { 2846 VersionInfo result = dataSource.getDtdVersionInfo(); 2847 if (result != null || isEmpty()) { 2848 return result; 2849 } 2850 // for old files, pick the version from the @version attribute 2851 String path = dataSource.iterator().next(); 2852 String full = getFullXPath(path); 2853 XPathParts parts = XPathParts.getFrozenInstance(full); 2854 String versionString = parts.findFirstAttributeValue("version"); 2855 return versionString == null ? null : VersionInfo.getInstance(versionString); 2856 } 2857 2858 private boolean contains(Map<String, String> a, Map<String, String> b) { 2859 for (Iterator<String> it = b.keySet().iterator(); it.hasNext(); ) { 2860 String key = it.next(); 2861 String otherValue = a.get(key); 2862 if (otherValue == null) { 2863 return false; 2864 } 2865 String value = b.get(key); 2866 if (!otherValue.equals(value)) { 2867 return false; 2868 } 2869 } 2870 return true; 2871 } 2872 2873 public String getFullXPath(String path, boolean ignoreOtherLeafAttributes) { 2874 String result = getFullXPath(path); 2875 if (result != null) return result; 2876 XPathParts parts = XPathParts.getFrozenInstance(path); 2877 Map<String, String> lastAttributes = parts.getAttributes(parts.size() - 1); 2878 String base = 2879 parts.toString(parts.size() - 1) 2880 + "/" 2881 + parts.getElement(parts.size() - 1); // trim final element 2882 for (Iterator<String> it = iterator(base); it.hasNext(); ) { 2883 String otherPath = it.next(); 2884 XPathParts other = XPathParts.getFrozenInstance(otherPath); 2885 if (other.size() != parts.size()) { 2886 continue; 2887 } 2888 Map<String, String> lastOtherAttributes = other.getAttributes(other.size() - 1); 2889 if (!contains(lastOtherAttributes, lastAttributes)) { 2890 continue; 2891 } 2892 if (result == null) { 2893 result = getFullXPath(otherPath); 2894 } else { 2895 throw new IllegalArgumentException("Multiple values for path: " + path); 2896 } 2897 } 2898 return result; 2899 } 2900 2901 /** 2902 * Return true if this item is the "winner" in the survey tool 2903 * 2904 * @param path 2905 * @return 2906 */ 2907 public boolean isWinningPath(String path) { 2908 return dataSource.isWinningPath(path); 2909 } 2910 2911 /** 2912 * Returns the "winning" path, for use in the survey tool tests, out of all those paths that 2913 * only differ by having "alt proposed". The exact meaning may be tweaked over time, but the 2914 * user's choice (vote) has precedence, then any undisputed choice, then the "best" choice of 2915 * the remainders. A value is always returned if there is a valid path, and the returned value 2916 * is always a valid path <i>in the resolved file</i>; that is, it may be valid in the parent, 2917 * or valid because of aliasing. 2918 * 2919 * @param path 2920 * @return path, perhaps with an alt proposed added. 2921 */ 2922 public String getWinningPath(String path) { 2923 return dataSource.getWinningPath(path); 2924 } 2925 2926 /** 2927 * Shortcut for getting the string value for the winning path 2928 * 2929 * @param path 2930 * @return 2931 */ 2932 public String getWinningValue(String path) { 2933 final String winningPath = getWinningPath(path); 2934 return winningPath == null ? null : getStringValue(winningPath); 2935 } 2936 2937 /** 2938 * Shortcut for getting the string value for the winning path. If the winning value is an {@link 2939 * CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned. 2940 * 2941 * @param path 2942 * @return the winning value 2943 */ 2944 public String getWinningValueWithBailey(String path) { 2945 final String winningPath = getWinningPath(path); 2946 return winningPath == null ? null : getStringValueWithBailey(winningPath); 2947 } 2948 2949 /** 2950 * Shortcut for getting the string value for a path. If the string value is an {@link 2951 * CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned. 2952 * 2953 * @param path 2954 * @return the string value 2955 */ 2956 public String getStringValueWithBailey(String path) { 2957 return getStringValueWithBailey(path, null, null); 2958 } 2959 2960 /** 2961 * Shortcut for getting the string value for a path. If the string value is an {@link 2962 * CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned. 2963 * 2964 * @param path the given xpath 2965 * @param pathWhereFound if not null, to be filled in with the path where the value is actually 2966 * found. May be {@link GlossonymConstructor#PSEUDO_PATH} if constructed. 2967 * @param localeWhereFound if not null, to be filled in with the locale where the value is 2968 * actually found. May be {@link XMLSource#CODE_FALLBACK_ID} if not in root. 2969 * @return the string value 2970 */ 2971 public String getStringValueWithBailey( 2972 String path, Output<String> pathWhereFound, Output<String> localeWhereFound) { 2973 String value = getStringValue(path); 2974 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 2975 value = getBaileyValue(path, pathWhereFound, localeWhereFound); 2976 } else if (localeWhereFound != null || pathWhereFound != null) { 2977 final Status status = new Status(); 2978 final String localeWhereFound2 = getSourceLocaleID(path, status); 2979 if (localeWhereFound != null) { 2980 localeWhereFound.value = localeWhereFound2; 2981 } 2982 if (pathWhereFound != null) { 2983 pathWhereFound.value = status.pathWhereFound; 2984 } 2985 } 2986 return value; 2987 } 2988 2989 /** 2990 * Return the distinguished paths that have the specified value. The pathPrefix and pathMatcher 2991 * can be used to restrict the returned paths to those matching. The pathMatcher can be null 2992 * (equals .*). 2993 * 2994 * @param valueToMatch 2995 * @param pathPrefix 2996 * @return 2997 */ 2998 public Set<String> getPathsWithValue( 2999 String valueToMatch, String pathPrefix, Matcher pathMatcher, Set<String> result) { 3000 if (result == null) { 3001 result = new HashSet<>(); 3002 } 3003 dataSource.getPathsWithValue(valueToMatch, pathPrefix, result); 3004 if (pathMatcher == null) { 3005 return result; 3006 } 3007 for (Iterator<String> it = result.iterator(); it.hasNext(); ) { 3008 String path = it.next(); 3009 if (!pathMatcher.reset(path).matches()) { 3010 it.remove(); 3011 } 3012 } 3013 return result; 3014 } 3015 3016 /** 3017 * Return the distinguished paths that match the pathPrefix and pathMatcher The pathMatcher can 3018 * be null (equals .*). 3019 */ 3020 public Set<String> getPaths(String pathPrefix, Matcher pathMatcher, Set<String> result) { 3021 if (result == null) { 3022 result = new HashSet<>(); 3023 } 3024 for (Iterator<String> it = dataSource.iterator(pathPrefix); it.hasNext(); ) { 3025 String path = it.next(); 3026 if (pathMatcher != null && !pathMatcher.reset(path).matches()) { 3027 continue; 3028 } 3029 result.add(path); 3030 } 3031 return result; 3032 } 3033 3034 public enum WinningChoice { 3035 NORMAL, 3036 WINNING 3037 } 3038 3039 /** 3040 * Used in TestUser to get the "winning" path. Simple implementation just for testing. 3041 * 3042 * @author markdavis 3043 */ 3044 static class WinningComparator implements Comparator<String> { 3045 String user; 3046 3047 public WinningComparator(String user) { 3048 this.user = user; 3049 } 3050 3051 /** 3052 * if it contains the user, sort first. Otherwise use normal string sorting. A better 3053 * implementation would look at the number of votes next, and whither there was an approved 3054 * or provisional path. 3055 */ 3056 @Override 3057 public int compare(String o1, String o2) { 3058 if (o1.contains(user)) { 3059 if (!o2.contains(user)) { 3060 return -1; // if it contains user 3061 } 3062 } else if (o2.contains(user)) { 3063 return 1; // if it contains user 3064 } 3065 return o1.compareTo(o2); 3066 } 3067 } 3068 3069 /** 3070 * This is a test class used to simulate what the survey tool would do. 3071 * 3072 * @author markdavis 3073 */ 3074 public static class TestUser extends CLDRFile { 3075 3076 Map<String, String> userOverrides = new HashMap<>(); 3077 3078 public TestUser(CLDRFile baseFile, String user, boolean resolved) { 3079 super(resolved ? baseFile.dataSource : baseFile.dataSource.getUnresolving()); 3080 if (!baseFile.isResolved()) { 3081 throw new IllegalArgumentException("baseFile must be resolved"); 3082 } 3083 Relation<String, String> pathMap = 3084 Relation.of( 3085 new HashMap<String, Set<String>>(), 3086 TreeSet.class, 3087 new WinningComparator(user)); 3088 for (String path : baseFile) { 3089 String newPath = getNondraftNonaltXPath(path); 3090 pathMap.put(newPath, path); 3091 } 3092 // now reduce the storage by just getting the winning ones 3093 // so map everything but the first path to the first path 3094 for (String path : pathMap.keySet()) { 3095 String winner = null; 3096 for (String rowPath : pathMap.getAll(path)) { 3097 if (winner == null) { 3098 winner = rowPath; 3099 continue; 3100 } 3101 userOverrides.put(rowPath, winner); 3102 } 3103 } 3104 } 3105 3106 @Override 3107 public String getWinningPath(String path) { 3108 String trial = userOverrides.get(path); 3109 if (trial != null) { 3110 return trial; 3111 } 3112 return path; 3113 } 3114 } 3115 3116 /** 3117 * Returns the extra paths, skipping those that are already represented in the locale. 3118 * 3119 * @return 3120 */ 3121 public Collection<String> getExtraPaths() { 3122 Set<String> toAddTo = new HashSet<>(); 3123 toAddTo.addAll(getRawExtraPaths()); 3124 for (String path : this) { 3125 toAddTo.remove(path); 3126 } 3127 return toAddTo; 3128 } 3129 3130 /** 3131 * Returns the extra paths, skipping those that are already represented in the locale. 3132 * 3133 * @return 3134 */ 3135 public Collection<String> getExtraPaths(String prefix, Collection<String> toAddTo) { 3136 for (String item : getRawExtraPaths()) { 3137 if (item.startsWith(prefix) 3138 && dataSource.getValueAtPath(item) == null) { // don't use getStringValue, since 3139 // it recurses. 3140 toAddTo.add(item); 3141 } 3142 } 3143 return toAddTo; 3144 } 3145 3146 // extraPaths contains the raw extra paths. 3147 // It requires filtering in those cases where we don't want duplicate paths. 3148 /** 3149 * Returns the raw extra paths, irrespective of what paths are already represented in the 3150 * locale. 3151 * 3152 * @return 3153 */ 3154 public Set<String> getRawExtraPaths() { 3155 if (extraPaths == null) { 3156 extraPaths = 3157 ImmutableSet.<String>builder() 3158 .addAll(getRawExtraPathsPrivate()) 3159 .addAll(CONST_EXTRA_PATHS) 3160 .build(); 3161 if (DEBUG) { 3162 System.out.println(getLocaleID() + "\textras: " + extraPaths.size()); 3163 } 3164 } 3165 return extraPaths; 3166 } 3167 3168 /** 3169 * Add (possibly over four thousand) extra paths to the given collection. These are paths that 3170 * typically don't have a reasonable fallback value that could be added to root. Some of them 3171 * are common to all locales, and some of them are specific to the given locale, based on 3172 * features like the plural rules for the locale. 3173 * 3174 * <p>The ones that are constant for all locales should go into CONST_EXTRA_PATHS. 3175 * 3176 * @return toAddTo (the collection) 3177 * <p>Called only by getRawExtraPaths. 3178 * <p>"Raw" refers to the fact that some of the paths may duplicate paths that are already 3179 * in this CLDRFile (in the xml and/or votes), in which case they will later get filtered by 3180 * getExtraPaths (removed from toAddTo) rather than re-added. 3181 * <p>NOTE: values may be null for some "extra" paths in locales for which no explicit 3182 * values have been submitted. Both unit tests and Survey Tool client code generate errors 3183 * or warnings for null value, but allow null value for certain exceptional extra paths. See 3184 * the functions named extraPathAllowsNullValue in TestPaths.java and in the JavaScript 3185 * client code. Make sure that updates here are reflected there and vice versa. 3186 * <p>Reference: https://unicode-org.atlassian.net/browse/CLDR-11238 3187 */ 3188 private List<String> getRawExtraPathsPrivate() { 3189 Set<String> toAddTo = new HashSet<>(); 3190 SupplementalDataInfo supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo(); 3191 // units 3192 PluralInfo plurals = supplementalData.getPlurals(PluralType.cardinal, getLocaleID()); 3193 if (plurals == null && DEBUG) { 3194 System.err.println( 3195 "No " 3196 + PluralType.cardinal 3197 + " plurals for " 3198 + getLocaleID() 3199 + " in " 3200 + supplementalData.getDirectory().getAbsolutePath()); 3201 } 3202 Set<Count> pluralCounts = Collections.emptySet(); 3203 if (plurals != null) { 3204 pluralCounts = plurals.getAdjustedCounts(); 3205 Set<Count> pluralCountsRaw = plurals.getCounts(); 3206 if (pluralCountsRaw.size() != 1) { 3207 // we get all the root paths with count 3208 addPluralCounts(toAddTo, pluralCounts, pluralCountsRaw, this); 3209 } 3210 } 3211 // dayPeriods 3212 String locale = getLocaleID(); 3213 DayPeriodInfo dayPeriods = 3214 supplementalData.getDayPeriods(DayPeriodInfo.Type.format, locale); 3215 if (dayPeriods != null) { 3216 LinkedHashSet<DayPeriod> items = new LinkedHashSet<>(dayPeriods.getPeriods()); 3217 items.add(DayPeriod.am); 3218 items.add(DayPeriod.pm); 3219 for (String context : new String[] {"format", "stand-alone"}) { 3220 for (String width : new String[] {"narrow", "abbreviated", "wide"}) { 3221 for (DayPeriod dayPeriod : items) { 3222 // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="am"] 3223 toAddTo.add( 3224 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/" 3225 + "dayPeriodContext[@type=\"" 3226 + context 3227 + "\"]/dayPeriodWidth[@type=\"" 3228 + width 3229 + "\"]/dayPeriod[@type=\"" 3230 + dayPeriod 3231 + "\"]"); 3232 } 3233 } 3234 } 3235 } 3236 3237 // metazones 3238 Set<String> zones = supplementalData.getAllMetazones(); 3239 3240 for (String zone : zones) { 3241 final boolean metazoneUsesDST = CheckMetazones.metazoneUsesDST(zone); 3242 for (String width : new String[] {"long", "short"}) { 3243 for (String type : new String[] {"generic", "standard", "daylight"}) { 3244 if (metazoneUsesDST || type.equals("standard")) { 3245 // Only add /standard for non-DST metazones 3246 final String path = 3247 "//ldml/dates/timeZoneNames/metazone[@type=\"" 3248 + zone 3249 + "\"]/" 3250 + width 3251 + "/" 3252 + type; 3253 toAddTo.add(path); 3254 } 3255 } 3256 } 3257 } 3258 3259 // // Individual zone overrides 3260 // final String[] overrides = { 3261 // "Pacific/Honolulu\"]/short/generic", 3262 // "Pacific/Honolulu\"]/short/standard", 3263 // "Pacific/Honolulu\"]/short/daylight", 3264 // "Europe/Dublin\"]/long/daylight", 3265 // "Europe/London\"]/long/daylight", 3266 // "Etc/UTC\"]/long/standard", 3267 // "Etc/UTC\"]/short/standard" 3268 // }; 3269 // for (String override : overrides) { 3270 // toAddTo.add("//ldml/dates/timeZoneNames/zone[@type=\"" + override); 3271 // } 3272 3273 // Currencies 3274 Set<String> codes = supplementalData.getBcp47Keys().getAll("cu"); 3275 for (String code : codes) { 3276 String currencyCode = code.toUpperCase(); 3277 toAddTo.add( 3278 "//ldml/numbers/currencies/currency[@type=\"" + currencyCode + "\"]/symbol"); 3279 toAddTo.add( 3280 "//ldml/numbers/currencies/currency[@type=\"" 3281 + currencyCode 3282 + "\"]/displayName"); 3283 if (!pluralCounts.isEmpty()) { 3284 for (Count count : pluralCounts) { 3285 toAddTo.add( 3286 "//ldml/numbers/currencies/currency[@type=\"" 3287 + currencyCode 3288 + "\"]/displayName[@count=\"" 3289 + count.toString() 3290 + "\"]"); 3291 } 3292 } 3293 } 3294 3295 // grammatical info 3296 3297 GrammarInfo grammarInfo = supplementalData.getGrammarInfo(getLocaleID(), true); 3298 if (grammarInfo != null) { 3299 if (grammarInfo.hasInfo(GrammaticalTarget.nominal)) { 3300 Collection<String> genders = 3301 grammarInfo.get( 3302 GrammaticalTarget.nominal, 3303 GrammaticalFeature.grammaticalGender, 3304 GrammaticalScope.units); 3305 Collection<String> rawCases = 3306 grammarInfo.get( 3307 GrammaticalTarget.nominal, 3308 GrammaticalFeature.grammaticalCase, 3309 GrammaticalScope.units); 3310 Collection<String> nomCases = rawCases.isEmpty() ? casesNominativeOnly : rawCases; 3311 Collection<Count> adjustedPlurals = pluralCounts; 3312 // There was code here allowing fewer plurals to be used, but is retracted for now 3313 // (needs more thorough integration in logical groups, etc.) 3314 // This note is left for 'blame' to find the old code in case we revive that. 3315 3316 // TODO use UnitPathType to get paths 3317 if (!genders.isEmpty()) { 3318 for (String unit : GrammarInfo.getUnitsToAddGrammar()) { 3319 toAddTo.add( 3320 "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"" 3321 + unit 3322 + "\"]/gender"); 3323 } 3324 for (Count plural : adjustedPlurals) { 3325 for (String gender : genders) { 3326 for (String case1 : nomCases) { 3327 final String grammaticalAttributes = 3328 GrammarInfo.getGrammaticalInfoAttributes( 3329 grammarInfo, 3330 UnitPathType.power, 3331 plural.toString(), 3332 gender, 3333 case1); 3334 toAddTo.add( 3335 "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1" 3336 + grammaticalAttributes); 3337 toAddTo.add( 3338 "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power3\"]/compoundUnitPattern1" 3339 + grammaticalAttributes); 3340 } 3341 } 3342 } 3343 // <genderMinimalPairs gender="masculine">Der {0} ist 3344 // …</genderMinimalPairs> 3345 for (String gender : genders) { 3346 toAddTo.add( 3347 "//ldml/numbers/minimalPairs/genderMinimalPairs[@gender=\"" 3348 + gender 3349 + "\"]"); 3350 } 3351 } 3352 if (!rawCases.isEmpty()) { 3353 for (String case1 : rawCases) { 3354 // <caseMinimalPairs case="nominative">{0} kostet 3355 // €3,50.</caseMinimalPairs> 3356 toAddTo.add( 3357 "//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\"" 3358 + case1 3359 + "\"]"); 3360 3361 for (Count plural : adjustedPlurals) { 3362 for (String unit : GrammarInfo.getUnitsToAddGrammar()) { 3363 toAddTo.add( 3364 "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"" 3365 + unit 3366 + "\"]/unitPattern" 3367 + GrammarInfo.getGrammaticalInfoAttributes( 3368 grammarInfo, 3369 UnitPathType.unit, 3370 plural.toString(), 3371 null, 3372 case1)); 3373 } 3374 } 3375 } 3376 } 3377 } 3378 } 3379 return toAddTo.stream().map(String::intern).collect(Collectors.toList()); 3380 } 3381 3382 private void addPluralCounts( 3383 Collection<String> toAddTo, 3384 final Set<Count> pluralCounts, 3385 final Set<Count> pluralCountsRaw, 3386 Iterable<String> file) { 3387 for (String path : file) { 3388 String countAttr = "[@count=\"other\"]"; 3389 int countPos = path.indexOf(countAttr); 3390 if (countPos < 0) { 3391 continue; 3392 } 3393 Set<Count> pluralCountsNeeded = 3394 path.startsWith("//ldml/numbers/minimalPairs") ? pluralCountsRaw : pluralCounts; 3395 if (pluralCountsNeeded.size() > 1) { 3396 String start = path.substring(0, countPos) + "[@count=\""; 3397 String end = "\"]" + path.substring(countPos + countAttr.length()); 3398 for (Count count : pluralCounts) { 3399 if (count == Count.other) { 3400 continue; 3401 } 3402 toAddTo.add(start + count + end); 3403 } 3404 } 3405 } 3406 } 3407 3408 /** 3409 * Get the path with the given count, case, or gender, with fallback. The fallback acts like an 3410 * alias in root. 3411 * 3412 * <p>Count: 3413 * 3414 * <p>It acts like there is an alias in root from count=n to count=one, then for currency 3415 * display names from count=one to no count <br> 3416 * For unitPatterns, falls back to Count.one. <br> 3417 * For others, falls back to Count.one, then no count. 3418 * 3419 * <p>Case 3420 * 3421 * <p>The fallback is to no case, which = nominative. 3422 * 3423 * <p>Case 3424 * 3425 * <p>The fallback is to no case, which = nominative. 3426 * 3427 * @param xpath 3428 * @param count Count may be null. Returns null if nothing is found. 3429 * @param winning TODO 3430 * @return 3431 */ 3432 public String getCountPathWithFallback(String xpath, Count count, boolean winning) { 3433 String result; 3434 XPathParts parts = 3435 XPathParts.getFrozenInstance(xpath) 3436 .cloneAsThawed(); // not frozen, addAttribute in getCountPathWithFallback2 3437 3438 // In theory we should do all combinations of gender, case, count (and eventually 3439 // definiteness), but for simplicity 3440 // we just successively try "zeroing" each one in a set order. 3441 // tryDefault modifies the parts in question 3442 Output<String> newPath = new Output<>(); 3443 if (tryDefault(parts, "gender", null, newPath)) { 3444 return newPath.value; 3445 } 3446 3447 if (tryDefault(parts, "case", null, newPath)) { 3448 return newPath.value; 3449 } 3450 3451 boolean isDisplayName = parts.contains("displayName"); 3452 3453 String actualCount = parts.getAttributeValue(-1, "count"); 3454 if (actualCount != null) { 3455 if (CldrUtility.DIGITS.containsAll(actualCount)) { 3456 try { 3457 int item = Integer.parseInt(actualCount); 3458 String locale = getLocaleID(); 3459 SupplementalDataInfo sdi = CLDRConfig.getInstance().getSupplementalDataInfo(); 3460 PluralRules rules = 3461 sdi.getPluralRules( 3462 new ULocale(locale), PluralRules.PluralType.CARDINAL); 3463 String keyword = rules.select(item); 3464 Count itemCount = Count.valueOf(keyword); 3465 result = getCountPathWithFallback2(parts, xpath, itemCount, winning); 3466 if (result != null && isNotRoot(result)) { 3467 return result; 3468 } 3469 } catch (NumberFormatException e) { 3470 } 3471 } 3472 3473 // try the given count first 3474 result = getCountPathWithFallback2(parts, xpath, count, winning); 3475 if (result != null && isNotRoot(result)) { 3476 return result; 3477 } 3478 // now try fallback 3479 if (count != Count.other) { 3480 result = getCountPathWithFallback2(parts, xpath, Count.other, winning); 3481 if (result != null && isNotRoot(result)) { 3482 return result; 3483 } 3484 } 3485 // now try deletion (for currency) 3486 if (isDisplayName) { 3487 result = getCountPathWithFallback2(parts, xpath, null, winning); 3488 } 3489 return result; 3490 } 3491 return null; 3492 } 3493 3494 /** 3495 * Modify the parts by setting the attribute in question to the default value (typically null to 3496 * clear). If there is a value for that path, use it. 3497 */ 3498 public boolean tryDefault( 3499 XPathParts parts, String attribute, String defaultValue, Output<String> newPath) { 3500 String oldValue = parts.getAttributeValue(-1, attribute); 3501 if (oldValue != null) { 3502 parts.setAttribute(-1, attribute, null); 3503 newPath.value = parts.toString(); 3504 if (dataSource.getValueAtPath(newPath.value) != null) { 3505 return true; 3506 } 3507 } 3508 return false; 3509 } 3510 3511 private String getCountPathWithFallback2( 3512 XPathParts parts, String xpathWithNoCount, Count count, boolean winning) { 3513 parts.addAttribute("count", count == null ? null : count.toString()); 3514 String newPath = parts.toString(); 3515 if (!newPath.equals(xpathWithNoCount)) { 3516 if (winning) { 3517 String temp = getWinningPath(newPath); 3518 if (temp != null) { 3519 newPath = temp; 3520 } 3521 } 3522 if (dataSource.getValueAtPath(newPath) != null) { 3523 return newPath; 3524 } 3525 // return getWinningPath(newPath); 3526 } 3527 return null; 3528 } 3529 3530 /** 3531 * Returns a value to be used for "filling in" a "Change" value in the survey tool. Currently 3532 * returns the following. 3533 * 3534 * <ul> 3535 * <li>The "winning" value (if not inherited). Example: if "Donnerstag" has the most votes for 3536 * 'thursday', then clicking on the empty field will fill in "Donnerstag" 3537 * <li>The singular form. Example: if the value for 'hour' is "heure", then clicking on the 3538 * entry field for 'hours' will insert "heure". 3539 * <li>The parent's value. Example: if I'm in [de_CH] and there are no proposals for 3540 * 'thursday', then clicking on the empty field will fill in "Donnerstag" from [de]. 3541 * <li>Otherwise don't fill in anything, and return null. 3542 * </ul> 3543 * 3544 * @return 3545 */ 3546 public String getFillInValue(String distinguishedPath) { 3547 String winningPath = getWinningPath(distinguishedPath); 3548 if (isNotRoot(winningPath)) { 3549 return getStringValue(winningPath); 3550 } 3551 String fallbackPath = getFallbackPath(winningPath, true, true); 3552 if (fallbackPath != null) { 3553 String value = getWinningValue(fallbackPath); 3554 if (value != null) { 3555 return value; 3556 } 3557 } 3558 return getStringValue(winningPath); 3559 } 3560 3561 /** 3562 * returns true if the source of the path exists, and is neither root nor code-fallback 3563 * 3564 * @param distinguishedPath 3565 * @return 3566 */ 3567 public boolean isNotRoot(String distinguishedPath) { 3568 String source = getSourceLocaleID(distinguishedPath, null); 3569 return source != null 3570 && !source.equals("root") 3571 && !source.equals(XMLSource.CODE_FALLBACK_ID); 3572 } 3573 3574 public boolean isAliasedAtTopLevel() { 3575 return iterator("//ldml/alias").hasNext(); 3576 } 3577 3578 public static Comparator<String> getComparator(DtdType dtdType) { 3579 if (dtdType == null) { 3580 return ldmlComparator; 3581 } 3582 switch (dtdType) { 3583 case ldml: 3584 case ldmlICU: 3585 return ldmlComparator; 3586 default: 3587 return DtdData.getInstance(dtdType).getDtdComparator(null); 3588 } 3589 } 3590 3591 public Comparator<String> getComparator() { 3592 return getComparator(dtdType); 3593 } 3594 3595 public DtdType getDtdType() { 3596 return dtdType != null ? dtdType : dataSource.getDtdType(); 3597 } 3598 3599 public DtdData getDtdData() { 3600 return dtdData != null ? dtdData : DtdData.getInstance(getDtdType()); 3601 } 3602 3603 public static Comparator<String> getPathComparator(String path) { 3604 DtdType fileDtdType = DtdType.fromPath(path); 3605 return getComparator(fileDtdType); 3606 } 3607 3608 public static MapComparator<String> getAttributeOrdering() { 3609 return DtdData.getInstance(DtdType.ldmlICU).getAttributeComparator(); 3610 } 3611 3612 public CLDRFile getUnresolved() { 3613 if (!isResolved()) { 3614 return this; 3615 } 3616 XMLSource source = dataSource.getUnresolving(); 3617 return new CLDRFile(source); 3618 } 3619 3620 public static Comparator<String> getAttributeValueComparator(String element, String attribute) { 3621 return DtdData.getAttributeValueComparator(DtdType.ldml, element, attribute); 3622 } 3623 3624 public void setDtdType(DtdType dtdType) { 3625 if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); 3626 this.dtdType = dtdType; 3627 } 3628 3629 public void disableCaching() { 3630 dataSource.disableCaching(); 3631 } 3632 3633 /** 3634 * Get a constructed value for the given path, if it is a path for which values can be 3635 * constructed 3636 * 3637 * @param xpath the given path, such as 3638 * //ldml/localeDisplayNames/languages/language[@type="zh_Hans"] 3639 * @return the constructed value, or null if this path doesn't have a constructed value 3640 */ 3641 public String getConstructedValue(String xpath) { 3642 if (isResolved() && GlossonymConstructor.pathIsEligible(xpath)) { 3643 return new GlossonymConstructor(this).getValue(xpath); 3644 } 3645 return null; 3646 } 3647 3648 /** 3649 * Get the string value for the given path in this locale, without resolving to any other path 3650 * or locale. 3651 * 3652 * @param xpath the given path 3653 * @return the string value, unresolved 3654 */ 3655 private String getStringValueUnresolved(String xpath) { 3656 CLDRFile sourceFileUnresolved = this.getUnresolved(); 3657 return sourceFileUnresolved.getStringValue(xpath); 3658 } 3659 3660 /** 3661 * Create an overriding LocaleStringProvider for testing and example generation 3662 * 3663 * @param pathAndValueOverrides 3664 * @return 3665 */ 3666 public LocaleStringProvider makeOverridingStringProvider( 3667 Map<String, String> pathAndValueOverrides) { 3668 return new OverridingStringProvider(pathAndValueOverrides); 3669 } 3670 3671 public class OverridingStringProvider implements LocaleStringProvider { 3672 private final Map<String, String> pathAndValueOverrides; 3673 3674 public OverridingStringProvider(Map<String, String> pathAndValueOverrides) { 3675 this.pathAndValueOverrides = pathAndValueOverrides; 3676 } 3677 3678 @Override 3679 public String getStringValue(String xpath) { 3680 String value = pathAndValueOverrides.get(xpath); 3681 return value != null ? value : CLDRFile.this.getStringValue(xpath); 3682 } 3683 3684 @Override 3685 public String getLocaleID() { 3686 return CLDRFile.this.getLocaleID(); 3687 } 3688 3689 @Override 3690 public String getSourceLocaleID(String xpath, Status status) { 3691 if (pathAndValueOverrides.containsKey(xpath)) { 3692 if (status != null) { 3693 status.pathWhereFound = xpath; 3694 } 3695 return getLocaleID() + "-override"; 3696 } 3697 return CLDRFile.this.getSourceLocaleID(xpath, status); 3698 } 3699 } 3700 3701 public String getKeyName(String key) { 3702 String result = getStringValue("//ldml/localeDisplayNames/keys/key[@type=\"" + key + "\"]"); 3703 if (result == null) { 3704 Relation<R2<String, String>, String> toAliases = 3705 SupplementalDataInfo.getInstance().getBcp47Aliases(); 3706 Set<String> aliases = toAliases.get(Row.of(key, "")); 3707 if (aliases != null) { 3708 for (String alias : aliases) { 3709 result = 3710 getStringValue( 3711 "//ldml/localeDisplayNames/keys/key[@type=\"" + alias + "\"]"); 3712 if (result != null) { 3713 break; 3714 } 3715 } 3716 } 3717 } 3718 return result; 3719 } 3720 3721 public String getKeyValueName(String key, String value) { 3722 String result = 3723 getStringValue( 3724 "//ldml/localeDisplayNames/types/type[@key=\"" 3725 + key 3726 + "\"][@type=\"" 3727 + value 3728 + "\"]"); 3729 if (result == null) { 3730 Relation<R2<String, String>, String> toAliases = 3731 SupplementalDataInfo.getInstance().getBcp47Aliases(); 3732 Set<String> keyAliases = toAliases.get(Row.of(key, "")); 3733 Set<String> valueAliases = toAliases.get(Row.of(key, value)); 3734 if (keyAliases != null || valueAliases != null) { 3735 if (keyAliases == null) { 3736 keyAliases = Collections.singleton(key); 3737 } 3738 if (valueAliases == null) { 3739 valueAliases = Collections.singleton(value); 3740 } 3741 for (String keyAlias : keyAliases) { 3742 for (String valueAlias : valueAliases) { 3743 result = 3744 getStringValue( 3745 "//ldml/localeDisplayNames/types/type[@key=\"" 3746 + keyAlias 3747 + "\"][@type=\"" 3748 + valueAlias 3749 + "\"]"); 3750 if (result != null) { 3751 break; 3752 } 3753 } 3754 } 3755 } 3756 } 3757 return result; 3758 } 3759 3760 /* 3761 ******************************************************************************************* 3762 * TODO: move the code below here -- that is, the many (currently ten as of 2022-06-01) 3763 * versions of getName and their subroutines and data -- to a new class in a separate file, 3764 * and enable tracking similar to existing "pathWhereFound/localeWhereFound" but more general. 3765 * 3766 * Reference: https://unicode-org.atlassian.net/browse/CLDR-15830 3767 ******************************************************************************************* 3768 */ 3769 3770 static final Joiner JOIN_HYPHEN = Joiner.on('-'); 3771 static final Joiner JOIN_UNDERBAR = Joiner.on('_'); 3772 3773 /** Utility for getting a name, given a type and code. */ 3774 public String getName(String type, String code) { 3775 return getName(typeNameToCode(type), code); 3776 } 3777 3778 public String getName(int type, String code) { 3779 return getName(type, code, null, null); 3780 } 3781 3782 public String getName(int type, String code, Set<String> paths) { 3783 return getName(type, code, null, paths); 3784 } 3785 3786 public String getName(int type, String code, Transform<String, String> altPicker) { 3787 return getName(type, code, altPicker, null); 3788 } 3789 3790 /** 3791 * Returns the name of the given bcp47 identifier. Note that extensions must be specified using 3792 * the old "\@key=type" syntax. 3793 * 3794 * @param localeOrTZID 3795 * @return 3796 */ 3797 public synchronized String getName(String localeOrTZID) { 3798 return getName(localeOrTZID, false); 3799 } 3800 3801 public String getName( 3802 LanguageTagParser lparser, 3803 boolean onlyConstructCompound, 3804 Transform<String, String> altPicker) { 3805 return getName(lparser, onlyConstructCompound, altPicker, null); 3806 } 3807 3808 /** 3809 * @param paths if non-null, will contain contributory paths on return 3810 */ 3811 public String getName( 3812 LanguageTagParser lparser, 3813 boolean onlyConstructCompound, 3814 Transform<String, String> altPicker, 3815 Set<String> paths) { 3816 return getName( 3817 lparser, 3818 onlyConstructCompound, 3819 altPicker, 3820 getWinningValueWithBailey(GETNAME_LOCALE_KEY_TYPE_PATTERN), 3821 getWinningValueWithBailey(GETNAME_LOCALE_PATTERN), 3822 getWinningValueWithBailey(GETNAME_LOCALE_SEPARATOR), 3823 paths); 3824 } 3825 3826 public synchronized String getName( 3827 String localeOrTZID, 3828 boolean onlyConstructCompound, 3829 String localeKeyTypePattern, 3830 String localePattern, 3831 String localeSeparator) { 3832 return getName( 3833 localeOrTZID, 3834 onlyConstructCompound, 3835 localeKeyTypePattern, 3836 localePattern, 3837 localeSeparator, 3838 null, 3839 null); 3840 } 3841 3842 /** 3843 * Returns the name of the given bcp47 identifier. Note that extensions must be specified using 3844 * the old "\@key=type" syntax. 3845 * 3846 * @param localeOrTZID the locale or timezone ID 3847 * @param onlyConstructCompound 3848 * @return 3849 */ 3850 public synchronized String getName(String localeOrTZID, boolean onlyConstructCompound) { 3851 return getName(localeOrTZID, onlyConstructCompound, null); 3852 } 3853 3854 /** 3855 * Returns the name of the given bcp47 identifier. Note that extensions must be specified using 3856 * the old "\@key=type" syntax. 3857 * 3858 * @param localeOrTZID the locale or timezone ID 3859 * @param onlyConstructCompound if true, returns "English (United Kingdom)" instead of "British 3860 * English" 3861 * @param altPicker Used to select particular alts. For example, SHORT_ALTS can be used to get 3862 * "English (U.K.)" instead of "English (United Kingdom)" 3863 * @return 3864 */ 3865 public synchronized String getName( 3866 String localeOrTZID, 3867 boolean onlyConstructCompound, 3868 Transform<String, String> altPicker) { 3869 return getName(localeOrTZID, onlyConstructCompound, altPicker, null); 3870 } 3871 3872 /** 3873 * Returns the name of the given bcp47 identifier. Note that extensions must be specified using 3874 * the old "\@key=type" syntax. 3875 * 3876 * @param localeOrTZID the locale or timezone ID 3877 * @param onlyConstructCompound if true, returns "English (United Kingdom)" instead of "British 3878 * English" 3879 * @param altPicker Used to select particular alts. For example, SHORT_ALTS can be used to get 3880 * "English (U.K.)" instead of "English (United Kingdom)" 3881 * @return 3882 */ 3883 public synchronized String getName( 3884 String localeOrTZID, 3885 boolean onlyConstructCompound, 3886 Transform<String, String> altPicker, 3887 Set<String> paths) { 3888 return getName( 3889 localeOrTZID, 3890 onlyConstructCompound, 3891 getWinningValueWithBailey(GETNAME_LOCALE_KEY_TYPE_PATTERN), 3892 getWinningValueWithBailey(GETNAME_LOCALE_PATTERN), 3893 getWinningValueWithBailey(GETNAME_LOCALE_SEPARATOR), 3894 altPicker, 3895 paths); 3896 } 3897 3898 /** 3899 * Returns the name of the given bcp47 identifier. Note that extensions must be specified using 3900 * the old "\@key=type" syntax. Only used by ExampleGenerator. 3901 * 3902 * @param localeOrTZID the locale or timezone ID 3903 * @param onlyConstructCompound 3904 * @param localeKeyTypePattern the pattern used to format key-type pairs 3905 * @param localePattern the pattern used to format primary/secondary subtags 3906 * @param localeSeparator the list separator for secondary subtags 3907 * @param paths if non-null, fillin with contributory paths 3908 * @return 3909 */ 3910 public synchronized String getName( 3911 String localeOrTZID, 3912 boolean onlyConstructCompound, 3913 String localeKeyTypePattern, 3914 String localePattern, 3915 String localeSeparator, 3916 Transform<String, String> altPicker, 3917 Set<String> paths) { 3918 // Hack for seed 3919 if (localePattern == null) { 3920 localePattern = "{0} ({1})"; 3921 } 3922 boolean isCompound = localeOrTZID.contains("_"); 3923 String name = 3924 isCompound && onlyConstructCompound 3925 ? null 3926 : getName(LANGUAGE_NAME, localeOrTZID, altPicker, paths); 3927 3928 // TODO - handle arbitrary combinations 3929 if (name != null && !name.contains("_") && !name.contains("-")) { 3930 name = replaceBracketsForName(name); 3931 return name; 3932 } 3933 LanguageTagParser lparser = new LanguageTagParser().set(localeOrTZID); 3934 return getName( 3935 lparser, 3936 onlyConstructCompound, 3937 altPicker, 3938 localeKeyTypePattern, 3939 localePattern, 3940 localeSeparator, 3941 paths); 3942 } 3943 3944 public String getName( 3945 LanguageTagParser lparser, 3946 boolean onlyConstructCompound, 3947 Transform<String, String> altPicker, 3948 String localeKeyTypePattern, 3949 String localePattern, 3950 String localeSeparator, 3951 Set<String> paths) { 3952 String name; 3953 String original = null; 3954 3955 // we need to check for prefixes, for lang+script or lang+country 3956 boolean haveScript = false; 3957 boolean haveRegion = false; 3958 // try lang+script 3959 if (onlyConstructCompound) { 3960 name = getName(LANGUAGE_NAME, original = lparser.getLanguage(), altPicker, paths); 3961 if (name == null) name = original; 3962 } else { 3963 String x = lparser.toString(LanguageTagParser.LANGUAGE_SCRIPT_REGION); 3964 name = getName(LANGUAGE_NAME, x, altPicker, paths); 3965 if (name != null) { 3966 haveScript = haveRegion = true; 3967 } else { 3968 name = 3969 getName( 3970 LANGUAGE_NAME, 3971 lparser.toString(LanguageTagParser.LANGUAGE_SCRIPT), 3972 altPicker, 3973 paths); 3974 if (name != null) { 3975 haveScript = true; 3976 } else { 3977 name = 3978 getName( 3979 LANGUAGE_NAME, 3980 lparser.toString(LanguageTagParser.LANGUAGE_REGION), 3981 altPicker, 3982 paths); 3983 if (name != null) { 3984 haveRegion = true; 3985 } else { 3986 name = 3987 getName( 3988 LANGUAGE_NAME, 3989 original = lparser.getLanguage(), 3990 altPicker, 3991 paths); 3992 if (name == null) { 3993 name = original; 3994 } 3995 } 3996 } 3997 } 3998 } 3999 name = replaceBracketsForName(name); 4000 String extras = ""; 4001 if (!haveScript) { 4002 extras = 4003 addDisplayName( 4004 lparser.getScript(), 4005 SCRIPT_NAME, 4006 localeSeparator, 4007 extras, 4008 altPicker, 4009 paths); 4010 } 4011 if (!haveRegion) { 4012 extras = 4013 addDisplayName( 4014 lparser.getRegion(), 4015 TERRITORY_NAME, 4016 localeSeparator, 4017 extras, 4018 altPicker, 4019 paths); 4020 } 4021 List<String> variants = lparser.getVariants(); 4022 for (String orig : variants) { 4023 extras = addDisplayName(orig, VARIANT_NAME, localeSeparator, extras, altPicker, paths); 4024 } 4025 4026 // Look for key-type pairs. 4027 main: 4028 for (Map.Entry<String, List<String>> extension : 4029 lparser.getLocaleExtensionsDetailed().entrySet()) { 4030 String key = extension.getKey(); 4031 if (key.equals("h0")) { 4032 continue; 4033 } 4034 List<String> keyValue = extension.getValue(); 4035 String oldFormatType = 4036 (key.equals("ca") ? JOIN_HYPHEN : JOIN_UNDERBAR) 4037 .join(keyValue); // default value 4038 // Check if key/type pairs exist in the CLDRFile first. 4039 String value = getKeyValueName(key, oldFormatType); 4040 if (value != null) { 4041 value = replaceBracketsForName(value); 4042 } else { 4043 // if we fail, then we construct from the key name and the value 4044 String kname = getKeyName(key); 4045 if (kname == null) { 4046 kname = key; // should not happen, but just in case 4047 } 4048 switch (key) { 4049 case "t": 4050 List<String> hybrid = lparser.getLocaleExtensionsDetailed().get("h0"); 4051 if (hybrid != null) { 4052 kname = getKeyValueName("h0", JOIN_UNDERBAR.join(hybrid)); 4053 } 4054 oldFormatType = getName(oldFormatType); 4055 break; 4056 case "h0": 4057 continue main; 4058 case "cu": 4059 oldFormatType = 4060 getName( 4061 CURRENCY_SYMBOL, 4062 oldFormatType.toUpperCase(Locale.ROOT), 4063 paths); 4064 break; 4065 case "tz": 4066 if (paths != null) { 4067 throw new IllegalArgumentException( 4068 "Error: getName(…) with paths doesn't handle timezones."); 4069 } 4070 oldFormatType = 4071 getTZName( 4072 oldFormatType, "VVVV"); // TODO: paths not handled here, yet 4073 break; 4074 case "kr": 4075 oldFormatType = getReorderName(localeSeparator, keyValue, paths); 4076 break; 4077 case "rg": 4078 case "sd": 4079 oldFormatType = getName(SUBDIVISION_NAME, oldFormatType, paths); 4080 break; 4081 default: 4082 oldFormatType = JOIN_HYPHEN.join(keyValue); 4083 } 4084 value = 4085 MessageFormat.format( 4086 localeKeyTypePattern, new Object[] {kname, oldFormatType}); 4087 if (paths != null) { 4088 paths.add(GETNAME_LOCALE_KEY_TYPE_PATTERN); 4089 } 4090 value = replaceBracketsForName(value); 4091 } 4092 if (paths != null && !extras.isEmpty()) { 4093 paths.add(GETNAME_LOCALE_SEPARATOR); 4094 } 4095 extras = 4096 extras.isEmpty() 4097 ? value 4098 : MessageFormat.format(localeSeparator, new Object[] {extras, value}); 4099 } 4100 // now handle stray extensions 4101 for (Map.Entry<String, List<String>> extension : 4102 lparser.getExtensionsDetailed().entrySet()) { 4103 String value = 4104 MessageFormat.format( 4105 localeKeyTypePattern, 4106 new Object[] { 4107 extension.getKey(), JOIN_HYPHEN.join(extension.getValue()) 4108 }); 4109 if (paths != null) { 4110 paths.add(GETNAME_LOCALE_KEY_TYPE_PATTERN); 4111 } 4112 extras = 4113 extras.isEmpty() 4114 ? value 4115 : MessageFormat.format(localeSeparator, new Object[] {extras, value}); 4116 } 4117 // fix this -- shouldn't be hardcoded! 4118 if (extras.length() == 0) { 4119 return name; 4120 } 4121 if (paths != null) { 4122 paths.add(GETNAME_LOCALE_PATTERN); 4123 } 4124 return MessageFormat.format(localePattern, new Object[] {name, extras}); 4125 } 4126 4127 private static final String replaceBracketsForName(String value) { 4128 value = value.replace('(', '[').replace(')', ']').replace('(', '[').replace(')', ']'); 4129 return value; 4130 } 4131 4132 /** 4133 * Utility for getting the name, given a code. 4134 * 4135 * @param type 4136 * @param code 4137 * @param codeToAlt - if not null, is called on the code. If the result is not null, then that 4138 * is used for an alt value. If the alt path has a value it is used, otherwise the normal 4139 * one is used. For example, the transform could return "short" for PS or HK or MO, but not 4140 * US or GB. 4141 * @param paths if non-null, will have contributory paths on return 4142 * @return 4143 */ 4144 public String getName( 4145 int type, String code, Transform<String, String> codeToAlt, Set<String> paths) { 4146 String path = getKey(type, code); 4147 String result = null; 4148 if (codeToAlt != null) { 4149 String alt = codeToAlt.transform(code); 4150 if (alt != null) { 4151 String altPath = path + "[@alt=\"" + alt + "\"]"; 4152 result = getStringValueWithBaileyNotConstructed(altPath); 4153 if (paths != null && result != null) { 4154 paths.add(altPath); 4155 } 4156 } 4157 } 4158 if (result == null) { 4159 result = getStringValueWithBaileyNotConstructed(path); 4160 if (paths != null && result != null) { 4161 paths.add(path); 4162 } 4163 } 4164 if (getLocaleID().equals("en")) { 4165 CLDRFile.Status status = new CLDRFile.Status(); 4166 String sourceLocale = getSourceLocaleID(path, status); 4167 if (result == null || !sourceLocale.equals("en")) { 4168 if (type == LANGUAGE_NAME) { 4169 Set<String> set = Iso639Data.getNames(code); 4170 if (set != null) { 4171 return set.iterator().next(); 4172 } 4173 Map<String, Map<String, String>> map = 4174 StandardCodes.getLStreg().get("language"); 4175 Map<String, String> info = map.get(code); 4176 if (info != null) { 4177 result = info.get("Description"); 4178 } 4179 } else if (type == TERRITORY_NAME) { 4180 result = getLstrFallback("region", code, paths); 4181 } else if (type == SCRIPT_NAME) { 4182 result = getLstrFallback("script", code, paths); 4183 } 4184 } 4185 } 4186 return result; 4187 } 4188 4189 static final Pattern CLEAN_DESCRIPTION = Pattern.compile("([^\\(\\[]*)[\\(\\[].*"); 4190 static final Splitter DESCRIPTION_SEP = Splitter.on('▪'); 4191 4192 private String getLstrFallback(String codeType, String code, Set<String> paths) { 4193 Map<String, String> info = StandardCodes.getLStreg().get(codeType).get(code); 4194 if (info != null) { 4195 String temp = info.get("Description"); 4196 if (!temp.equalsIgnoreCase("Private use")) { 4197 List<String> temp2 = DESCRIPTION_SEP.splitToList(temp); 4198 temp = temp2.get(0); 4199 final Matcher matcher = CLEAN_DESCRIPTION.matcher(temp); 4200 if (matcher.lookingAt()) { 4201 temp = matcher.group(1).trim(); 4202 } 4203 return temp; 4204 } 4205 } 4206 return null; 4207 } 4208 4209 /** 4210 * Gets timezone name. Not optimized. 4211 * 4212 * @param tzcode 4213 * @return 4214 */ 4215 private String getTZName(String tzcode, String format) { 4216 String longid = getLongTzid(tzcode); 4217 if (tzcode.length() == 4 && !tzcode.equals("gaza")) { 4218 return longid; 4219 } 4220 TimezoneFormatter tzf = new TimezoneFormatter(this); 4221 return tzf.getFormattedZone(longid, format, 0); 4222 } 4223 4224 private String getReorderName( 4225 String localeSeparator, List<String> keyValues, Set<String> paths) { 4226 String result = null; 4227 for (String value : keyValues) { 4228 String name = 4229 getName( 4230 SCRIPT_NAME, 4231 Character.toUpperCase(value.charAt(0)) + value.substring(1), 4232 paths); 4233 if (name == null) { 4234 name = getKeyValueName("kr", value); 4235 if (name == null) { 4236 name = value; 4237 } 4238 } 4239 result = 4240 result == null 4241 ? name 4242 : MessageFormat.format(localeSeparator, new Object[] {result, name}); 4243 } 4244 return result; 4245 } 4246 4247 /** 4248 * Adds the display name for a subtag to a string. 4249 * 4250 * @param subtag the subtag 4251 * @param type the type of the subtag 4252 * @param separatorPattern the pattern to be used for separating display names in the resultant 4253 * string 4254 * @param extras the string to be added to 4255 * @return the modified display name string 4256 */ 4257 private String addDisplayName( 4258 String subtag, 4259 int type, 4260 String separatorPattern, 4261 String extras, 4262 Transform<String, String> altPicker, 4263 Set<String> paths) { 4264 if (subtag.length() == 0) { 4265 return extras; 4266 } 4267 String sname = getName(type, subtag, altPicker, paths); 4268 if (sname == null) { 4269 sname = subtag; 4270 } 4271 sname = replaceBracketsForName(sname); 4272 4273 if (extras.length() == 0) { 4274 extras += sname; 4275 } else { 4276 extras = MessageFormat.format(separatorPattern, new Object[] {extras, sname}); 4277 } 4278 return extras; 4279 } 4280 4281 /** 4282 * Like getStringValueWithBailey, but reject constructed values, to prevent circularity problems 4283 * with getName 4284 * 4285 * <p>Since GlossonymConstructor uses getName to CREATE constructed values, circularity problems 4286 * would occur if getName in turn used GlossonymConstructor to get constructed Bailey values. 4287 * Note that getStringValueWithBailey only returns a constructed value if the value would 4288 * otherwise be "bogus", and getName has no use for bogus values, so there is no harm in 4289 * returning null rather than code-fallback or other bogus values. 4290 * 4291 * @param path the given xpath 4292 * @return the string value, or null 4293 */ 4294 private String getStringValueWithBaileyNotConstructed(String path) { 4295 Output<String> pathWhereFound = new Output<>(); 4296 final String value = getStringValueWithBailey(path, pathWhereFound, null); 4297 if (value == null || GlossonymConstructor.PSEUDO_PATH.equals(pathWhereFound.toString())) { 4298 return null; 4299 } 4300 return value; 4301 } 4302 4303 /** 4304 * A set of paths to be added to getRawExtraPaths(). These are constant across locales, and 4305 * don't have good fallback values in root. NOTE: if this is changed, you'll need to modify 4306 * TestPaths.extraPathAllowsNullValue 4307 */ 4308 static final Set<String> CONST_EXTRA_PATHS = 4309 CharUtilities.internImmutableSet( 4310 Set.of( 4311 // Individual zone overrides — were in getRawExtraPaths 4312 "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/generic", 4313 "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/standard", 4314 "//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/daylight", 4315 "//ldml/dates/timeZoneNames/zone[@type=\"Europe/Dublin\"]/long/daylight", 4316 "//ldml/dates/timeZoneNames/zone[@type=\"Europe/London\"]/long/daylight", 4317 "//ldml/dates/timeZoneNames/zone[@type=\"Etc/UTC\"]/long/standard", 4318 "//ldml/dates/timeZoneNames/zone[@type=\"Etc/UTC\"]/short/standard", 4319 // Person name paths 4320 "//ldml/personNames/sampleName[@item=\"nativeG\"]/nameField[@type=\"given\"]", 4321 "//ldml/personNames/sampleName[@item=\"nativeGS\"]/nameField[@type=\"given\"]", 4322 "//ldml/personNames/sampleName[@item=\"nativeGS\"]/nameField[@type=\"surname\"]", 4323 "//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"given\"]", 4324 "//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"given2\"]", 4325 "//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"surname\"]", 4326 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"title\"]", 4327 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given\"]", 4328 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given-informal\"]", 4329 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given2\"]", 4330 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname-prefix\"]", 4331 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname-core\"]", 4332 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname2\"]", 4333 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"generation\"]", 4334 "//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"credentials\"]", 4335 "//ldml/personNames/sampleName[@item=\"foreignG\"]/nameField[@type=\"given\"]", 4336 "//ldml/personNames/sampleName[@item=\"foreignGS\"]/nameField[@type=\"given\"]", 4337 "//ldml/personNames/sampleName[@item=\"foreignGS\"]/nameField[@type=\"surname\"]", 4338 "//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"given\"]", 4339 "//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"given2\"]", 4340 "//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"surname\"]", 4341 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"title\"]", 4342 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given\"]", 4343 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given-informal\"]", 4344 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given2\"]", 4345 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname-prefix\"]", 4346 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname-core\"]", 4347 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname2\"]", 4348 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"generation\"]", 4349 "//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"credentials\"]")); 4350 } 4351