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