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