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