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