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