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