1 /* 2 ****************************************************************************** 3 * Copyright (C) 2005-2014, International Business Machines Corporation and * 4 * others. All Rights Reserved. * 5 ****************************************************************************** 6 */ 7 8 package org.unicode.cldr.test; 9 10 import com.google.common.base.Supplier; 11 import com.google.common.base.Suppliers; 12 import com.google.common.collect.ComparisonChain; 13 import com.google.common.collect.ImmutableList; 14 import com.google.common.collect.ImmutableSet; 15 import com.ibm.icu.dev.util.ElapsedTimer; 16 import com.ibm.icu.impl.Row.R3; 17 import com.ibm.icu.text.ListFormatter; 18 import com.ibm.icu.text.MessageFormat; 19 import java.text.ParsePosition; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.HashMap; 23 import java.util.Iterator; 24 import java.util.List; 25 import java.util.Locale; 26 import java.util.Map; 27 import java.util.Set; 28 import java.util.TreeSet; 29 import java.util.function.Function; 30 import java.util.logging.Logger; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype; 34 import org.unicode.cldr.util.CLDRFile; 35 import org.unicode.cldr.util.CLDRInfo.CandidateInfo; 36 import org.unicode.cldr.util.CLDRInfo.PathValueInfo; 37 import org.unicode.cldr.util.CLDRInfo.UserInfo; 38 import org.unicode.cldr.util.CLDRLocale; 39 import org.unicode.cldr.util.CldrUtility; 40 import org.unicode.cldr.util.Factory; 41 import org.unicode.cldr.util.InternalCldrException; 42 import org.unicode.cldr.util.Level; 43 import org.unicode.cldr.util.PathHeader; 44 import org.unicode.cldr.util.PathHeader.SurveyToolStatus; 45 import org.unicode.cldr.util.PatternCache; 46 import org.unicode.cldr.util.RegexFileParser; 47 import org.unicode.cldr.util.RegexFileParser.RegexLineParser; 48 import org.unicode.cldr.util.StandardCodes; 49 import org.unicode.cldr.util.TransliteratorUtilities; 50 import org.unicode.cldr.util.VoteResolver; 51 import org.unicode.cldr.util.VoteResolver.Status; 52 53 /** 54 * This class provides a foundation for both console-driven CLDR tests, and Survey Tool Tests. 55 * 56 * <p>To add a test, subclass CLDRFile and override handleCheck and possibly setCldrFileToCheck. 57 * Then put the test into getCheckAll. 58 * 59 * <p>To use the test, take a look at the main in ConsoleCheckCLDR. Note that you need to call 60 * setDisplayInformation with the CLDRFile for the locale that you want the display information (eg 61 * names for codes) to be in.<br> 62 * Some options are passed in the Map options. Examples: boolean SHOW_TIMES = 63 * options.containsKey("SHOW_TIMES"); // for printing times for doing setCldrFileToCheck. 64 * 65 * <p>Some errors/warnings will be explicitly filtered out when calling CheckCLDR's check() method. 66 * The full list of filters can be found in org/unicode/cldr/util/data/CheckCLDR-exceptions.txt. 67 * 68 * @author davis 69 */ 70 public abstract class CheckCLDR implements CheckAccessor { 71 72 /** protected so subclasses can use it */ 73 protected static Logger logger = Logger.getLogger(CheckCLDR.class.getSimpleName()); 74 75 /** 76 * set the internal logger level. For ConsoleCheck. 77 * 78 * @returns the previous level 79 */ setLoggerLevel(java.util.logging.Level newLevel)80 public static java.util.logging.Level setLoggerLevel(java.util.logging.Level newLevel) { 81 // NB: we use the full package name here, to avoid conflict with other CLDR classes named 82 // Level 83 java.util.logging.Level oldLevel = logger.getLevel(); 84 logger.setLevel(newLevel); 85 return oldLevel; 86 } 87 88 /** serialize CheckCLDR as just its class name */ toString()89 public String toString() { 90 return getClass().getSimpleName(); 91 } 92 93 public static final boolean LIMITED_SUBMISSION = 94 false; // TODO: CLDR-13337: represent differently 95 96 private static CLDRFile displayInformation; 97 98 private CLDRFile cldrFileToCheck; 99 private CLDRFile englishFile = null; 100 101 private boolean skipTest = false; 102 private Phase phase; 103 private Map<Subtype, List<Pattern>> filtersForLocale = new HashMap<>(); 104 105 @Override getStringValue(String path)106 public String getStringValue(String path) { 107 return getCldrFileToCheck().getStringValue(path); 108 } 109 110 @Override getUnresolvedStringValue(String path)111 public String getUnresolvedStringValue(String path) { 112 return getCldrFileToCheck().getUnresolved().getStringValue(path); 113 } 114 115 @Override getLocaleID()116 public String getLocaleID() { 117 return getCldrFileToCheck().getLocaleID(); 118 } 119 120 @Override getCause()121 public CheckCLDR getCause() { 122 return this; 123 } 124 125 public enum InputMethod { 126 DIRECT, 127 BULK 128 } 129 130 public enum StatusAction { 131 /** Allow voting and add new values (in Change column). */ 132 ALLOW, 133 /** Allow voting and ticket (in Change column). */ 134 ALLOW_VOTING_AND_TICKET, 135 /** Allow voting but no add new values (in Change column). */ 136 ALLOW_VOTING_BUT_NO_ADD, 137 /** Only allow filing a ticket. */ 138 ALLOW_TICKET_ONLY, 139 /** Disallow (for various reasons) */ 140 FORBID_ERRORS(true), 141 FORBID_READONLY(true), 142 FORBID_UNLESS_DATA_SUBMISSION(true), 143 FORBID_NULL(true), 144 FORBID_ROOT(true), 145 FORBID_CODE(true), 146 FORBID_PERMANENT_WITHOUT_FORUM(true); 147 148 private final boolean isForbidden; 149 StatusAction()150 private StatusAction() { 151 isForbidden = false; 152 } 153 StatusAction(boolean isForbidden)154 private StatusAction(boolean isForbidden) { 155 this.isForbidden = isForbidden; 156 } 157 isForbidden()158 public boolean isForbidden() { 159 return isForbidden; 160 } 161 canShow()162 public boolean canShow() { 163 return !isForbidden; 164 } 165 canVote()166 public boolean canVote() { 167 // the one non-voting case 168 if (this == ALLOW_TICKET_ONLY) return false; 169 return !isForbidden(); 170 } 171 canSubmit()172 public boolean canSubmit() { 173 return (this == ALLOW); 174 } 175 } 176 177 private static final HashMap<String, Phase> PHASE_NAMES = new HashMap<>(); 178 179 public enum Phase { 180 BUILD, 181 SUBMISSION, 182 VETTING, 183 FINAL_TESTING("RESOLUTION"); 184 Phase(String... alternateName)185 Phase(String... alternateName) { 186 for (String name : alternateName) { 187 PHASE_NAMES.put(name.toUpperCase(Locale.ENGLISH), this); 188 } 189 } 190 forString(String value)191 public static Phase forString(String value) { 192 if (value == null) { 193 return org.unicode.cldr.util.CLDRConfig.getInstance().getPhase(); 194 } 195 value = value.toUpperCase(Locale.ENGLISH); 196 Phase result = PHASE_NAMES.get(value); 197 return result != null ? result : Phase.valueOf(value); 198 } 199 200 /** true if it's a 'unit test' phase. */ isUnitTest()201 public boolean isUnitTest() { 202 return this == BUILD || this == FINAL_TESTING; 203 } 204 205 /** 206 * Return whether or not to show a row, and if so, how. 207 * 208 * @param pathValueInfo - may be null for a non-path entry. 209 * @param inputMethod 210 * @param ph the path header - may be null if it is a non-path entry 211 * @param userInfo null if there is no userInfo (nobody logged in). 212 * @return 213 */ getShowRowAction( PathValueInfo pathValueInfo, InputMethod inputMethod, PathHeader ph, UserInfo userInfo )214 public StatusAction getShowRowAction( 215 PathValueInfo pathValueInfo, 216 InputMethod inputMethod, 217 PathHeader ph, 218 UserInfo userInfo // can get voterInfo from this. 219 ) { 220 221 // default to read/write 222 PathHeader.SurveyToolStatus status = PathHeader.SurveyToolStatus.READ_WRITE; 223 boolean canReadAndWrite = true; 224 225 if (ph != null) { 226 status = ph.getSurveyToolStatus(); 227 canReadAndWrite = ph.canReadAndWrite(); 228 } 229 /* 230 * Always forbid DEPRECATED items - don't show. 231 * 232 * Currently, bulk submission and TC voting are allowed even for SurveyToolStatus.HIDE, 233 * but not for SurveyToolStatus.DEPRECATED. If we ever want to treat HIDE and DEPRECATED 234 * the same here, then it would be simpler to call ph.shouldHide which is true for both. 235 */ 236 if (status == SurveyToolStatus.DEPRECATED) { 237 return StatusAction.FORBID_READONLY; 238 } 239 240 if (status == SurveyToolStatus.READ_ONLY) { 241 return StatusAction.ALLOW_TICKET_ONLY; 242 } 243 244 // if TC+, allow anything else, even suppressed items and errors 245 if (userInfo != null 246 && userInfo.getVoterInfo().getLevel().compareTo(VoteResolver.Level.tc) >= 0) { 247 return StatusAction.ALLOW; 248 } 249 250 // always forbid bulk import except in data submission. 251 if (inputMethod == InputMethod.BULK && (this != Phase.SUBMISSION && isUnitTest())) { 252 return StatusAction.FORBID_UNLESS_DATA_SUBMISSION; 253 } 254 255 if (status == SurveyToolStatus.HIDE) { 256 return StatusAction.FORBID_READONLY; 257 } 258 259 ValueStatus valueStatus = ValueStatus.NONE; 260 if (pathValueInfo != null) { 261 CandidateInfo winner = pathValueInfo.getCurrentItem(); 262 valueStatus = getValueStatus(winner, ValueStatus.NONE, null); 263 264 // if limited submission, and winner doesn't have an error, limit the values 265 266 if (LIMITED_SUBMISSION) { 267 if (!SubmissionLocales.allowEvenIfLimited( 268 pathValueInfo.getLocale().toString(), 269 pathValueInfo.getXpath(), 270 valueStatus == ValueStatus.ERROR, 271 pathValueInfo.getBaselineStatus() == Status.missing)) { 272 return StatusAction.FORBID_READONLY; 273 } 274 } 275 } 276 277 if (this == Phase.SUBMISSION || isUnitTest()) { 278 return (canReadAndWrite) 279 ? StatusAction.ALLOW 280 : StatusAction.ALLOW_VOTING_AND_TICKET; 281 } 282 283 // We are in vetting, not in submission 284 285 // Only allow ADD if we have an error or warning 286 // Only check winning value for errors/warnings per ticket #8677 287 if (valueStatus != ValueStatus.NONE) { 288 return (canReadAndWrite) 289 ? StatusAction.ALLOW 290 : StatusAction.ALLOW_VOTING_AND_TICKET; 291 } 292 293 // No warnings, so allow just voting. 294 return StatusAction.ALLOW_VOTING_BUT_NO_ADD; 295 } 296 297 /** 298 * getAcceptNewItemAction. MUST only be called if getShowRowAction(...).canShow() TODO 299 * Consider moving Phase, StatusAction, etc into CLDRInfo. 300 * 301 * @param enteredValue If null, means an abstention. If voting for an existing value, 302 * pathValueInfo.getValues().contains(enteredValue) MUST be true 303 * @param pathValueInfo 304 * @param inputMethod 305 * @param status 306 * @param userInfo 307 * @return 308 */ getAcceptNewItemAction( CandidateInfo enteredValue, PathValueInfo pathValueInfo, InputMethod inputMethod, PathHeader ph, UserInfo userInfo )309 public StatusAction getAcceptNewItemAction( 310 CandidateInfo enteredValue, 311 PathValueInfo pathValueInfo, 312 InputMethod inputMethod, 313 PathHeader ph, 314 UserInfo userInfo // can get voterInfo from this. 315 ) { 316 if (!ph.canReadAndWrite()) { 317 return StatusAction.FORBID_READONLY; 318 } 319 320 // only logged in users can add items. 321 if (userInfo == null) { 322 return StatusAction.FORBID_ERRORS; 323 } 324 325 // we can always abstain 326 if (enteredValue == null) { 327 return StatusAction.ALLOW; 328 } 329 330 // if TC+, allow anything else, even suppressed items and errors 331 if (userInfo.getVoterInfo().getLevel().compareTo(VoteResolver.Level.tc) >= 0) { 332 return StatusAction.ALLOW; 333 } 334 335 // Disallow errors. 336 ValueStatus valueStatus = 337 getValueStatus(enteredValue, ValueStatus.NONE, CheckStatus.crossCheckSubtypes); 338 if (valueStatus == ValueStatus.ERROR) { 339 return StatusAction.FORBID_ERRORS; 340 } 341 342 // Allow items if submission 343 if (this == Phase.SUBMISSION || isUnitTest()) { 344 return StatusAction.ALLOW; 345 } 346 347 // Voting for an existing value is ok 348 valueStatus = ValueStatus.NONE; 349 for (CandidateInfo value : pathValueInfo.getValues()) { 350 if (value == enteredValue) { 351 return StatusAction.ALLOW; 352 } 353 valueStatus = getValueStatus(value, valueStatus, CheckStatus.crossCheckSubtypes); 354 } 355 356 // If there were any errors/warnings on other values, allow 357 if (valueStatus != ValueStatus.NONE) { 358 return StatusAction.ALLOW; 359 } 360 361 // Otherwise (we are vetting, but with no errors or warnings) 362 // DISALLOW NEW STUFF 363 364 return StatusAction.FORBID_UNLESS_DATA_SUBMISSION; 365 } 366 367 public enum ValueStatus { 368 ERROR, 369 WARNING, 370 NONE 371 } 372 getValueStatus( CandidateInfo value, ValueStatus previous, Set<Subtype> changeErrorToWarning)373 public ValueStatus getValueStatus( 374 CandidateInfo value, ValueStatus previous, Set<Subtype> changeErrorToWarning) { 375 if (previous == ValueStatus.ERROR || value == null) { 376 return previous; 377 } 378 379 for (CheckStatus item : value.getCheckStatusList()) { 380 CheckStatus.Type type = item.getType(); 381 if (type.equals(CheckStatus.Type.Error)) { 382 if (changeErrorToWarning != null 383 && changeErrorToWarning.contains(item.getSubtype())) { 384 return ValueStatus.WARNING; 385 } else { 386 return ValueStatus.ERROR; 387 } 388 } else if (type.equals(CheckStatus.Type.Warning)) { 389 previous = ValueStatus.WARNING; 390 } 391 } 392 return previous; 393 } 394 } 395 396 public static final class Options implements Comparable<Options> { 397 398 public enum Option { 399 locale, 400 CoverageLevel_requiredLevel("CoverageLevel.requiredLevel"), 401 CoverageLevel_localeType("CoverageLevel.localeType"), 402 SHOW_TIMES, 403 phase, 404 lgWarningCheck, 405 CheckCoverage_skip("CheckCoverage.skip"), 406 exemplarErrors; 407 408 private String key; 409 getKey()410 public String getKey() { 411 return key; 412 } 413 Option(String key)414 Option(String key) { 415 this.key = key; 416 } 417 Option()418 Option() { 419 this.key = name(); 420 } 421 } 422 423 private static StandardCodes sc = StandardCodes.make(); 424 425 private final boolean DEBUG_OPTS = false; 426 427 String options[] = new String[Option.values().length]; 428 CLDRLocale locale = null; 429 430 private final String key; // for fast compare 431 432 /** 433 * Adopt some other map 434 * 435 * @param fromOptions 436 */ Options(Map<String, String> fromOptions)437 public Options(Map<String, String> fromOptions) { 438 clear(); 439 setAll(fromOptions); 440 key = null; // no key = slow compare 441 } 442 setAll(Map<String, String> fromOptions)443 private void setAll(Map<String, String> fromOptions) { 444 for (Map.Entry<String, String> e : fromOptions.entrySet()) { 445 set(e.getKey(), e.getValue()); 446 } 447 } 448 449 /** 450 * @param key 451 * @param value 452 */ set(String key, String value)453 public void set(String key, String value) { 454 // TODO- cache the map 455 for (Option o : Option.values()) { 456 if (o.getKey().equals(key)) { 457 set(o, value); 458 return; 459 } 460 } 461 throw new IllegalArgumentException( 462 "Unknown CLDR option: '" 463 + key 464 + "' - valid keys are: " 465 + Options.getValidKeys()); 466 } 467 getValidKeys()468 private static String getValidKeys() { 469 Set<String> allkeys = new TreeSet<>(); 470 for (Option o : Option.values()) { 471 allkeys.add(o.getKey()); 472 } 473 return ListFormatter.getInstance().format(allkeys); 474 } 475 Options()476 public Options() { 477 clear(); 478 key = "".intern(); // null Options. 479 } 480 481 /** 482 * Deep clone 483 * 484 * @param options2 485 */ Options(Options options2)486 public Options(Options options2) { 487 this.options = Arrays.copyOf(options2.options, options2.options.length); 488 this.key = options2.key; 489 this.locale = options2.locale; 490 } 491 Options(CLDRLocale locale)492 public Options(CLDRLocale locale) { 493 this.locale = locale; 494 options = new String[Option.values().length]; 495 set(Option.locale, locale.getBaseName()); 496 StringBuilder sb = new StringBuilder(); 497 sb.append(locale.getBaseName()).append('/'); 498 key = sb.toString().intern(); 499 } 500 Options( CLDRLocale locale, CheckCLDR.Phase testPhase, String requiredLevel, String localeType)501 public Options( 502 CLDRLocale locale, 503 CheckCLDR.Phase testPhase, 504 String requiredLevel, 505 String localeType) { 506 this.locale = locale; 507 options = new String[Option.values().length]; 508 StringBuilder sb = new StringBuilder(); 509 set(Option.locale, locale.getBaseName()); 510 sb.append(locale.getBaseName()).append('/'); 511 set(Option.CoverageLevel_requiredLevel, requiredLevel); 512 sb.append(requiredLevel).append('/'); 513 set(Option.CoverageLevel_localeType, localeType); 514 sb.append(localeType).append('/'); 515 set(Option.phase, testPhase.name().toLowerCase()); 516 sb.append(localeType).append('/'); 517 key = sb.toString().intern(); 518 } 519 520 @Override clone()521 public Options clone() { 522 return new Options(this); 523 } 524 525 @Override equals(Object other)526 public boolean equals(Object other) { 527 if (this == other) return true; 528 if (!(other instanceof Options)) return false; 529 if (this.key != null && ((Options) other).key != null) { 530 return (this.key == ((Options) other).key); 531 } else { 532 return this.compareTo((Options) other) == 0; 533 } 534 } 535 clear()536 private Options clear() { 537 for (int i = 0; i < options.length; i++) { 538 options[i] = null; 539 } 540 return this; 541 } 542 set(Option o, String v)543 private Options set(Option o, String v) { 544 options[o.ordinal()] = v; 545 if (DEBUG_OPTS) System.err.println("Setting " + o + " = " + v); 546 return this; 547 } 548 get(Option o)549 public String get(Option o) { 550 final String v = options[o.ordinal()]; 551 if (DEBUG_OPTS) System.err.println("Getting " + o + " = " + v); 552 return v; 553 } 554 getLocale()555 public CLDRLocale getLocale() { 556 if (locale != null) return locale; 557 return CLDRLocale.getInstance(get(Option.locale)); 558 } 559 560 /** 561 * Get the required coverage level for the specified locale, for this CheckCLDR object. 562 * 563 * @param localeID 564 * @return the Level 565 * <p>Called by CheckCoverage.setCldrFileToCheck and CheckDates.setCldrFileToCheck 566 */ getRequiredLevel(String localeID)567 public Level getRequiredLevel(String localeID) { 568 Level result; 569 // see if there is an explicit level 570 String localeType = get(Option.CoverageLevel_requiredLevel); 571 if (localeType != null) { 572 result = Level.get(localeType); 573 if (result != Level.UNDETERMINED) { 574 return result; 575 } 576 } 577 // otherwise, see if there is an organization level for the "Cldr" organization. 578 // This is not user-specific. 579 return sc.getLocaleCoverageLevel("Cldr", localeID); 580 } 581 contains(Option o)582 public boolean contains(Option o) { 583 String s = get(o); 584 return (s != null && !s.isEmpty()); 585 } 586 587 @Override compareTo(Options other)588 public int compareTo(Options other) { 589 if (other == this) return 0; 590 if (key != null && other.key != null) { 591 if (key == other.key) return 0; 592 return key.compareTo(other.key); 593 } 594 for (int i = 0; i < options.length; i++) { 595 final String s1 = options[i]; 596 final String s2 = other.options[i]; 597 if (s1 == null && s2 == null) { 598 // no difference 599 } else if (s1 == null) { 600 return -1; 601 } else if (s2 == null) { 602 return 1; 603 } else { 604 int rv = s1.compareTo(s2); 605 if (rv != 0) { 606 return rv; 607 } 608 } 609 } 610 return 0; 611 } 612 613 @Override hashCode()614 public int hashCode() { 615 if (key != null) return key.hashCode(); 616 617 int h = 1; 618 for (int i = 0; i < options.length; i++) { 619 if (options[i] == null) { 620 h *= 11; 621 } else { 622 h = (h * 11) + options[i].hashCode(); 623 } 624 } 625 return h; 626 } 627 628 @Override toString()629 public String toString() { 630 if (key != null) return "Options:" + key; 631 StringBuilder sb = new StringBuilder(); 632 for (Option o : Option.values()) { 633 if (options[o.ordinal()] != null) { 634 sb.append(o).append('=').append(options[o.ordinal()]).append(' '); 635 } 636 } 637 return sb.toString(); 638 } 639 } 640 isSkipTest()641 public boolean isSkipTest() { 642 return skipTest; 643 } 644 645 // this should only be set for the test in setCldrFileToCheck setSkipTest(boolean skipTest)646 public void setSkipTest(boolean skipTest) { 647 this.skipTest = skipTest; 648 } 649 650 /** 651 * Here is where the list of all checks is found. 652 * 653 * @param nameMatcher Regex pattern that determines which checks are run, based on their class 654 * name (such as .* for all checks, .*Collisions.* for CheckDisplayCollisions, etc.) 655 * @return 656 */ getCheckAll(Factory factory, String nameMatcher)657 public static CompoundCheckCLDR getCheckAll(Factory factory, String nameMatcher) { 658 return new CompoundCheckCLDR() 659 .setFilter(Pattern.compile(nameMatcher, Pattern.CASE_INSENSITIVE).matcher("")) 660 .add(new CheckAnnotations()) 661 // .add(new CheckAttributeValues(factory)) 662 .add(new CheckChildren(factory)) 663 .add(new CheckCoverage(factory)) 664 .add(new CheckDates(factory)) 665 .add(new CheckForCopy(factory)) 666 .add(new CheckDisplayCollisions(factory)) 667 .add(new CheckExemplars(factory)) 668 .add(new CheckForExemplars(factory)) 669 .add(new CheckForInheritanceMarkers()) 670 .add(new CheckNames()) 671 .add(new CheckNumbers(factory)) 672 // .add(new CheckZones()) // this doesn't work; many spurious errors that user can't 673 // correct 674 .add(new CheckMetazones()) 675 .add(new CheckLogicalGroupings(factory)) 676 .add(new CheckAlt()) 677 .add(new CheckAltOnly(factory)) 678 .add(new CheckCurrencies()) 679 .add(new CheckCasing()) 680 .add( 681 new CheckConsistentCasing( 682 factory)) // this doesn't work; many spurious errors that user can't 683 // correct 684 .add(new CheckQuotes()) 685 .add(new CheckUnits()) 686 .add(new CheckWidths()) 687 .add(new CheckPlaceHolders()) 688 .add(new CheckPersonNames()) 689 .add(new CheckNew(factory)) // this is at the end; it will check for other certain 690 // other errors and warnings and 691 // not add a message if there are any. 692 ; 693 } 694 695 /** These determine what language is used to display information. Must be set before use. */ getDisplayInformation()696 public static synchronized CLDRFile getDisplayInformation() { 697 return displayInformation; 698 } 699 setDisplayInformation(CLDRFile inputDisplayInformation)700 public static synchronized void setDisplayInformation(CLDRFile inputDisplayInformation) { 701 displayInformation = inputDisplayInformation; 702 } 703 704 /** Get the CLDRFile. */ getCldrFileToCheck()705 public final CLDRFile getCldrFileToCheck() { 706 return cldrFileToCheck; 707 } 708 709 /** 710 * Often subclassed for initializing. If so, make the first 2 lines: if (cldrFileToCheck == 711 * null) return this; super.handleSetCldrFileToCheck(cldrFileToCheck); do stuff 712 * 713 * <p>Called late via accept(). 714 * 715 * @param cldrFileToCheck 716 * @param options 717 * @param possibleErrors any deferred possibleErrors can be set here. They will be appended to 718 * every handleCheck() call. 719 * @return 720 */ handleSetCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)721 public CheckCLDR handleSetCldrFileToCheck( 722 CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) { 723 724 // nothing by default 725 return this; 726 } 727 728 /** 729 * Set the CLDRFile. Must be done before calling check. 730 * 731 * @param cldrFileToCheck 732 * @param options (not currently used) 733 * @param possibleErrors 734 */ setCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)735 public CheckCLDR setCldrFileToCheck( 736 CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) { 737 this.cldrFileToCheck = cldrFileToCheck; 738 reset(); 739 // clear the *cached* possible Errors. Not counting any set immediately by subclasses. 740 cachedPossibleErrors.clear(); 741 cachedOptions = new Options(options); 742 // we must load filters here, as they are used by check() 743 744 // Shortlist error filters for this locale. 745 String locale = cldrFileToCheck.getLocaleID(); 746 filtersForLocale.clear(); 747 for (R3<Pattern, Subtype, Pattern> filter : getAllFilters()) { 748 if (filter.get0() == null || !filter.get0().matcher(locale).matches()) continue; 749 Subtype subtype = filter.get1(); 750 List<Pattern> xpaths = filtersForLocale.get(subtype); 751 if (xpaths == null) { 752 filtersForLocale.put(subtype, xpaths = new ArrayList<>()); 753 } 754 xpaths.add(filter.get2()); 755 } 756 757 // hook for checks that want to set possibleErrors early 758 handleCheckPossibleErrors(cldrFileToCheck, options, possibleErrors); 759 760 return this; 761 } 762 763 /** override this if you want to return errors immediately when setCldrFileToCheck is called */ handleCheckPossibleErrors( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)764 protected void handleCheckPossibleErrors( 765 CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) { 766 // nothing by default. 767 } 768 769 /** override this if you want to reset state immediately when setCldrFileToCheck is called */ reset()770 protected void reset() { 771 initted = false; 772 } 773 774 /** 775 * Subclasses must call this, after any skip calculation to indicate that an xpath is relevant 776 * to them. 777 * 778 * @param result out-parameter to contain any deferred errors 779 * @return false if test is skipped and should exit 780 */ accept(List<CheckStatus> result)781 protected boolean accept(List<CheckStatus> result) { 782 if (!initted) { 783 if (this.cldrFileToCheck == null) { 784 throw new NullPointerException("accept() was called before setCldrFileToCheck()"); 785 } 786 // clear this again. 787 cachedPossibleErrors.clear(); 788 // call into the subclass 789 handleSetCldrFileToCheck(this.cldrFileToCheck, cachedOptions, cachedPossibleErrors); 790 // all of these are entireLocale 791 cachedPossibleErrors.forEach(e -> e.setEntireLocale()); 792 initted = true; 793 } 794 // unconditionally append all cached possible errors 795 result.addAll(cachedPossibleErrors); 796 if (isSkipTest()) { 797 return false; 798 } 799 return true; 800 } 801 802 /** has accept() been called since setCldrFileToCheck() was called? */ 803 boolean initted = false; 804 805 /** cache of possible errors, for handleSetCldrFileToCheck */ 806 List<CheckStatus> cachedPossibleErrors = new ArrayList<>(); 807 808 Options cachedOptions = null; 809 810 /** 811 * abstract interface for mapping from a Subtype to a "more details" URL. see 812 * org.unicode.cldr.web.SubtypeToURLMap 813 */ 814 public interface SubtypeToURLProvider extends Function<Subtype, String> {} 815 816 /** Status value returned from check */ 817 public static class CheckStatus implements Comparable<CheckStatus> { 818 public static final Type alertType = Type.Comment, 819 warningType = Type.Warning, 820 errorType = Type.Error, 821 exampleType = Type.Example, 822 demoType = Type.Demo; 823 824 public enum Type { 825 Comment, 826 Warning, 827 Error, 828 Example, 829 Demo 830 } 831 832 public enum Subtype { 833 none, 834 noUnproposedVariant, 835 deprecatedAttribute, 836 illegalPlural, 837 invalidLocale, 838 incorrectCasing, 839 valueMustBeOverridden, 840 valueAlwaysOverridden, 841 nullChildFile, 842 internalError, 843 coverageLevel, 844 missingPluralInfo, 845 currencySymbolTooWide, 846 incorrectDatePattern, 847 abbreviatedDateFieldTooWide, 848 displayCollision, 849 illegalExemplarSet, 850 missingAuxiliaryExemplars, 851 extraPlaceholders, 852 missingPlaceholders, 853 shouldntHavePlaceholders, 854 couldNotAccessExemplars, 855 noExemplarCharacters, 856 modifiedEnglishValue, 857 invalidCurrencyMatchSet, 858 multipleMetazoneMappings, 859 noMetazoneMapping, 860 noMetazoneMappingAfter1970, 861 noMetazoneMappingBeforeNow, 862 cannotCreateZoneFormatter, 863 insufficientCoverage, 864 missingLanguageTerritoryInfo, 865 missingEuroCountryInfo, 866 deprecatedAttributeWithReplacement, 867 missingOrExtraDateField, 868 internalUnicodeSetFormattingError, 869 auxiliaryExemplarsOverlap, 870 missingPunctuationCharacters, 871 872 charactersNotInCurrencyExemplars, 873 asciiCharactersNotInCurrencyExemplars, 874 charactersNotInMainOrAuxiliaryExemplars, 875 asciiCharactersNotInMainOrAuxiliaryExemplars, 876 877 narrowDateFieldTooWide, 878 illegalCharactersInExemplars, 879 orientationDisagreesWithExemplars, 880 inconsistentDatePattern, 881 inconsistentTimePattern, 882 missingDatePattern, 883 illegalDatePattern, 884 missingMainExemplars, 885 mustNotStartOrEndWithSpace, 886 illegalCharactersInNumberPattern, 887 numberPatternNotCanonical, 888 currencyPatternMissingCurrencySymbol, 889 currencyPatternUnexpectedCurrencySymbol, 890 missingMinusSign, 891 badNumericType, 892 percentPatternMissingPercentSymbol, 893 illegalNumberFormat, 894 unexpectedAttributeValue, 895 metazoneContainsDigit, 896 tooManyGroupingSeparators, 897 inconsistentPluralFormat, 898 missingZeros, 899 sameAsEnglish, 900 sameAsCode, 901 dateSymbolCollision, 902 incompleteLogicalGroup, 903 extraMetazoneString, 904 inconsistentDraftStatus, 905 errorOrWarningInLogicalGroup, 906 valueTooWide, 907 valueTooNarrow, 908 nameContainsYear, 909 patternCannotContainDigits, 910 patternContainsInvalidCharacters, 911 parenthesesNotAllowed, 912 illegalNumberingSystem, 913 unexpectedOrderOfEraYear, 914 invalidPlaceHolder, 915 asciiQuotesNotAllowed, 916 badMinimumGroupingDigits, 917 inconsistentPeriods, 918 inheritanceMarkerNotAllowed, 919 invalidDurationUnitPattern, 920 invalidDelimiter, 921 illegalCharactersInPattern, 922 badParseLenient, 923 tooManyValues, 924 invalidSymbol, 925 invalidGenderCode, 926 mismatchedUnitComponent, 927 longPowerWithSubscripts, 928 gapsInPlaceholderNumbers, 929 duplicatePlaceholders, 930 largerDifferences, 931 missingNonAltPath, 932 badSamplePersonName, 933 missingLanguage, 934 namePlaceholderProblem, 935 missingSpaceBetweenNameFields, 936 shortDateFieldInconsistentLength, 937 illegalParameterValue, 938 illegalAnnotationCode, 939 illegalCharacter; 940 941 @Override toString()942 public String toString() { 943 // converts "thisThisThis" to "this this this" 944 return TO_STRING.matcher(name()).replaceAll(" $1").toLowerCase(); 945 } 946 947 static Pattern TO_STRING = PatternCache.get("([A-Z])"); 948 } 949 950 /** 951 * These error don't prevent entry during submission, since they become valid if a different 952 * row is changed. 953 */ 954 public static Set<Subtype> crossCheckSubtypes = 955 ImmutableSet.of( 956 Subtype.dateSymbolCollision, 957 Subtype.displayCollision, 958 Subtype.inconsistentDraftStatus, 959 Subtype.incompleteLogicalGroup, 960 Subtype.inconsistentPeriods, 961 Subtype.abbreviatedDateFieldTooWide, 962 Subtype.narrowDateFieldTooWide, 963 Subtype.shortDateFieldInconsistentLength, 964 Subtype.coverageLevel); 965 966 public static Set<Subtype> errorCodesPath = 967 ImmutableSet.of( 968 Subtype.duplicatePlaceholders, 969 Subtype.extraPlaceholders, 970 Subtype.gapsInPlaceholderNumbers, 971 Subtype.invalidPlaceHolder, 972 Subtype.missingPlaceholders, 973 Subtype.shouldntHavePlaceholders); 974 975 private Type type; 976 private Subtype subtype = Subtype.none; 977 private String messageFormat; 978 private Object[] parameters; 979 private CheckAccessor cause; 980 private boolean checkOnSubmit = true; 981 CheckStatus()982 public CheckStatus() {} 983 isCheckOnSubmit()984 public boolean isCheckOnSubmit() { 985 return checkOnSubmit; 986 } 987 setCheckOnSubmit(boolean dependent)988 public CheckStatus setCheckOnSubmit(boolean dependent) { 989 this.checkOnSubmit = dependent; 990 return this; 991 } 992 getType()993 public Type getType() { 994 return type; 995 } 996 setMainType(CheckStatus.Type type)997 public CheckStatus setMainType(CheckStatus.Type type) { 998 this.type = type; 999 return this; 1000 } 1001 getMessage()1002 public String getMessage() { 1003 String message = messageFormat; 1004 if (messageFormat != null && parameters != null) { 1005 try { 1006 String fixedApos = MessageFormat.autoQuoteApostrophe(messageFormat); 1007 MessageFormat format = new MessageFormat(fixedApos); 1008 message = format.format(parameters); 1009 if (errorCodesPath.contains(subtype)) { 1010 message += 1011 "; see <a href='http://cldr.unicode.org/translation/error-codes#" 1012 + subtype.name() 1013 + "' target='cldr_error_codes'>" 1014 + subtype 1015 + "</a>."; 1016 } 1017 } catch (Exception e) { 1018 message = messageFormat; 1019 final String failMsg = 1020 "MessageFormat Failure: " 1021 + subtype 1022 + "; " 1023 + messageFormat 1024 + "; " 1025 + (parameters == null ? null : Arrays.asList(parameters)); 1026 logger.log(java.util.logging.Level.SEVERE, e, () -> failMsg); 1027 System.err.println(failMsg); 1028 // throw new IllegalArgumentException(subtype + "; " + messageFormat + "; " 1029 // + (parameters == null ? null : Arrays.asList(parameters)), e); 1030 } 1031 } 1032 Exception[] exceptionParameters = getExceptionParameters(); 1033 if (exceptionParameters != null) { 1034 for (Exception exception : exceptionParameters) { 1035 message += "; " + exception.getMessage(); // + " \t(" + 1036 // exception.getClass().getName() + ")"; 1037 // for (StackTraceElement item : exception.getStackTrace()) { 1038 // message += "\n\t" + item; 1039 // } 1040 } 1041 } 1042 return message.replace('\t', ' '); 1043 } 1044 setMessage(String message)1045 public CheckStatus setMessage(String message) { 1046 if (cause == null) { 1047 throw new IllegalArgumentException("Must have cause set."); 1048 } 1049 if (message == null) { 1050 throw new IllegalArgumentException("Message cannot be null."); 1051 } 1052 this.messageFormat = message; 1053 this.parameters = null; 1054 return this; 1055 } 1056 setMessage(String message, Object... messageArguments)1057 public CheckStatus setMessage(String message, Object... messageArguments) { 1058 if (cause == null) { 1059 throw new IllegalArgumentException("Must have cause set."); 1060 } 1061 this.messageFormat = message; 1062 this.parameters = messageArguments; 1063 return this; 1064 } 1065 1066 @Override toString()1067 public String toString() { 1068 return getType() + ": " + getMessage(); 1069 } 1070 1071 /** Warning: don't change the contents of the parameters after retrieving. */ getParameters()1072 public Object[] getParameters() { 1073 return parameters; 1074 } 1075 1076 /** 1077 * Returns any Exception parameters in the status, or null if there are none. 1078 * 1079 * @return 1080 */ getExceptionParameters()1081 public Exception[] getExceptionParameters() { 1082 if (parameters == null) { 1083 return null; 1084 } 1085 1086 List<Exception> errors = new ArrayList<>(); 1087 for (Object o : parameters) { 1088 if (o instanceof Exception) { 1089 errors.add((Exception) o); 1090 } 1091 } 1092 if (errors.size() == 0) { 1093 return null; 1094 } 1095 return errors.toArray(new Exception[errors.size()]); 1096 } 1097 1098 /** Warning: don't change the contents of the parameters after passing in. */ setParameters(Object[] parameters)1099 public CheckStatus setParameters(Object[] parameters) { 1100 if (cause == null) { 1101 throw new IllegalArgumentException("Must have cause set."); 1102 } 1103 this.parameters = parameters; 1104 return this; 1105 } 1106 getDemo()1107 public SimpleDemo getDemo() { 1108 return null; 1109 } 1110 getCause()1111 public CheckCLDR getCause() { 1112 return cause instanceof CheckCLDR ? (CheckCLDR) cause : null; 1113 } 1114 setCause(CheckAccessor cause)1115 public CheckStatus setCause(CheckAccessor cause) { 1116 this.cause = cause; 1117 return this; 1118 } 1119 getSubtype()1120 public Subtype getSubtype() { 1121 return subtype; 1122 } 1123 setSubtype(Subtype subtype)1124 public CheckStatus setSubtype(Subtype subtype) { 1125 this.subtype = subtype; 1126 return this; 1127 } 1128 1129 /** 1130 * Convenience function: return true if any items in this list are of errorType 1131 * 1132 * @param result the list to check (could be null for empty) 1133 * @return true if any items in result are of errorType 1134 */ hasError(List<CheckStatus> result)1135 public static final boolean hasError(List<CheckStatus> result) { 1136 return hasType(result, errorType); 1137 } 1138 1139 /** 1140 * Convenience function: return true if any items in this list are of errorType 1141 * 1142 * @param result the list to check (could be null for empty) 1143 * @return true if any items in result are of errorType 1144 */ hasType(List<CheckStatus> result, Type type)1145 public static boolean hasType(List<CheckStatus> result, Type type) { 1146 if (result == null) return false; 1147 for (CheckStatus s : result) { 1148 if (s.getType().equals(type)) { 1149 return true; 1150 } 1151 } 1152 return false; 1153 } 1154 1155 /** 1156 * @returns true if this status applies to the entire locale, not a single path 1157 */ getEntireLocale()1158 public boolean getEntireLocale() { 1159 return entireLocale; 1160 } 1161 1162 /** Mark this CheckStatus as isEntireLocale */ setEntireLocale()1163 CheckStatus setEntireLocale() { 1164 entireLocale = true; 1165 return this; 1166 } 1167 1168 private boolean entireLocale = false; 1169 1170 @Override compareTo(CheckStatus o)1171 public int compareTo(CheckStatus o) { 1172 if (this == o) return 0; 1173 return ComparisonChain.start() 1174 .compare(getType(), o.getType()) 1175 .compare(getSubtype(), o.getSubtype()) 1176 .compare(getMessage(), o.getMessage()) 1177 .result(); 1178 } 1179 1180 @Override equals(Object o)1181 public boolean equals(Object o) { 1182 if (this == o) return true; 1183 if (o instanceof CheckStatus) return false; 1184 return compareTo((CheckStatus) o) == 0; 1185 } 1186 } 1187 1188 public abstract static class SimpleDemo { 1189 Map<String, String> internalPostArguments = new HashMap<>(); 1190 1191 /** 1192 * @param postArguments A read-write map containing post-style arguments. eg TEXTBOX=abcd, 1193 * etc. <br> 1194 * The first time this is called, the Map should be empty. 1195 * @return true if the map has been changed 1196 */ getHTML(Map<String, String> postArguments)1197 public abstract String getHTML(Map<String, String> postArguments) throws Exception; 1198 1199 /** Only here for compatibility. Use the other getHTML instead */ getHTML(String path, String fullPath, String value)1200 public final String getHTML(String path, String fullPath, String value) throws Exception { 1201 return getHTML(internalPostArguments); 1202 } 1203 1204 /** 1205 * THIS IS ONLY FOR COMPATIBILITY: you can call this, then the non-postArguments form of 1206 * getHTML; or better, call getHTML with the postArguments. 1207 * 1208 * @param postArguments A read-write map containing post-style arguments. eg TEXTBOX=abcd, 1209 * etc. 1210 * @return true if the map has been changed 1211 */ processPost(Map<String, String> postArguments)1212 public final boolean processPost(Map<String, String> postArguments) { 1213 internalPostArguments.clear(); 1214 internalPostArguments.putAll(postArguments); 1215 return true; 1216 } 1217 } 1218 1219 public abstract static class FormatDemo extends SimpleDemo { 1220 protected String currentPattern, currentInput, currentFormatted, currentReparsed; 1221 protected ParsePosition parsePosition = new ParsePosition(0); 1222 getPattern()1223 protected abstract String getPattern(); 1224 getSampleInput()1225 protected abstract String getSampleInput(); 1226 getArguments(Map<String, String> postArguments)1227 protected abstract void getArguments(Map<String, String> postArguments); 1228 1229 @Override getHTML(Map<String, String> postArguments)1230 public String getHTML(Map<String, String> postArguments) throws Exception { 1231 getArguments(postArguments); 1232 StringBuffer htmlMessage = new StringBuffer(); 1233 FormatDemo.appendTitle(htmlMessage); 1234 FormatDemo.appendLine( 1235 htmlMessage, currentPattern, currentInput, currentFormatted, currentReparsed); 1236 htmlMessage.append("</table>"); 1237 return htmlMessage.toString(); 1238 } 1239 getPlainText(Map<String, String> postArguments)1240 public String getPlainText(Map<String, String> postArguments) { 1241 getArguments(postArguments); 1242 return MessageFormat.format( 1243 "<\"\u200E{0}\u200E\", \"{1}\"> \u2192 \"\u200E{2}\u200E\" \u2192 \"{3}\"", 1244 (Object[]) 1245 new String[] { 1246 currentPattern, currentInput, currentFormatted, currentReparsed 1247 }); 1248 } 1249 1250 /** 1251 * @param htmlMessage 1252 * @param pattern 1253 * @param input 1254 * @param formatted 1255 * @param reparsed 1256 */ appendLine( StringBuffer htmlMessage, String pattern, String input, String formatted, String reparsed)1257 public static void appendLine( 1258 StringBuffer htmlMessage, 1259 String pattern, 1260 String input, 1261 String formatted, 1262 String reparsed) { 1263 htmlMessage 1264 .append("<tr><td><input type='text' name='pattern' value='") 1265 .append(TransliteratorUtilities.toXML.transliterate(pattern)) 1266 .append("'></td><td><input type='text' name='input' value='") 1267 .append(TransliteratorUtilities.toXML.transliterate(input)) 1268 .append("'></td><td>") 1269 .append("<input type='submit' value='Test' name='Test'>") 1270 .append("</td><td>" + "<input type='text' name='formatted' value='") 1271 .append(TransliteratorUtilities.toXML.transliterate(formatted)) 1272 .append("'></td><td>" + "<input type='text' name='reparsed' value='") 1273 .append(TransliteratorUtilities.toXML.transliterate(reparsed)) 1274 .append("'></td></tr>"); 1275 } 1276 1277 /** 1278 * @param htmlMessage 1279 */ appendTitle(StringBuffer htmlMessage)1280 public static void appendTitle(StringBuffer htmlMessage) { 1281 htmlMessage.append( 1282 "<table border='1' cellspacing='0' cellpadding='2'" 1283 + 1284 // " style='border-collapse: collapse' style='width: 100%'" + 1285 "><tr>" 1286 + "<th>Pattern</th>" 1287 + "<th>Unlocalized Input</th>" 1288 + "<th></th>" 1289 + "<th>Localized Format</th>" 1290 + "<th>Re-Parsed</th>" 1291 + "</tr>"); 1292 } 1293 } 1294 1295 /** 1296 * Checks the path/value in the cldrFileToCheck for correctness, according to some criterion. If 1297 * the path is relevant to the check, there is an alert or warning, then a CheckStatus is added 1298 * to List. 1299 * 1300 * @param path Must be a distinguished path, such as what comes out of CLDRFile.iterator() 1301 * @param fullPath Must be the full path 1302 * @param value the value associated with the path 1303 * @param result 1304 */ check( String path, String fullPath, String value, Options options, List<CheckStatus> result)1305 public final CheckCLDR check( 1306 String path, String fullPath, String value, Options options, List<CheckStatus> result) { 1307 if (cldrFileToCheck == null) { 1308 throw new InternalCldrException("CheckCLDR problem: cldrFileToCheck must not be null"); 1309 } 1310 if (path == null) { 1311 throw new InternalCldrException("CheckCLDR problem: path must not be null"); 1312 } 1313 // if (fullPath == null) { 1314 // throw new InternalError("CheckCLDR problem: fullPath must not be null"); 1315 // } 1316 // if (value == null) { 1317 // throw new InternalError("CheckCLDR problem: value must not be null"); 1318 // } 1319 result.clear(); 1320 1321 /* 1322 * If the item is non-winning, and either inherited or it is code-fallback, then don't run 1323 * any tests on this item. See http://unicode.org/cldr/trac/ticket/7574 1324 * 1325 * The following conditional formerly used "value == ..." and "value != ...", which in Java doesn't 1326 * mean what it does in some other languages. The condition has been changed to use the equals() method. 1327 * Since value can be null, check for that first. 1328 */ 1329 // if (value == cldrFileToCheck.getBaileyValue(path, null, null) && value != 1330 // cldrFileToCheck.getWinningValue(path)) { 1331 if (value != null 1332 && !value.equals(cldrFileToCheck.getWinningValue(path)) 1333 && cldrFileToCheck.getUnresolved().getStringValue(path) == null) { 1334 return this; 1335 } 1336 1337 // If we're being asked to run tests for an inheritance marker, then we need to change it 1338 // to the "real" value first before running tests. Testing the value 1339 // CldrUtility.INHERITANCE_MARKER ("↑↑↑") doesn't make sense. 1340 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 1341 value = cldrFileToCheck.getBaileyValue(path, null, null); 1342 // If it hasn't changed, then don't run any tests. 1343 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 1344 return this; 1345 } 1346 } 1347 CheckCLDR instance = handleCheck(path, fullPath, value, options, result); 1348 Iterator<CheckStatus> iterator = result.iterator(); 1349 // Filter out any errors/warnings that match the filter list in CheckCLDR-exceptions.txt. 1350 while (iterator.hasNext()) { 1351 CheckStatus status = iterator.next(); 1352 if (shouldExcludeStatus(fullPath, status)) { 1353 iterator.remove(); 1354 } 1355 } 1356 return instance; 1357 } 1358 1359 /** 1360 * Returns any examples in the result parameter. Both examples and demos can be returned. A demo 1361 * will have getType() == CheckStatus.demoType. In that case, there will be no getMessage 1362 * available; instead, call getDemo() to get the demo, then call getHTML() to get the initial 1363 * HTML. 1364 */ getExamples( String path, String fullPath, String value, Options options, List<CheckStatus> result)1365 public final CheckCLDR getExamples( 1366 String path, String fullPath, String value, Options options, List<CheckStatus> result) { 1367 result.clear(); 1368 return handleGetExamples(path, fullPath, value, options, result); 1369 } 1370 1371 @SuppressWarnings("unused") handleGetExamples( String path, String fullPath, String value, Options options2, List<CheckStatus> result)1372 protected CheckCLDR handleGetExamples( 1373 String path, 1374 String fullPath, 1375 String value, 1376 Options options2, 1377 List<CheckStatus> result) { 1378 return this; // NOOP unless overridden 1379 } 1380 1381 /** 1382 * This is what the subclasses override. 1383 * 1384 * <p>If a path is not applicable, exit early with <code>return this;</code> Once a path is 1385 * applicable, call <code>accept(result);</code> to add deferred possible problems. 1386 * 1387 * <p>If something is found, a CheckStatus is added to result. This can be done multiple times 1388 * in one call, if multiple errors or warnings are found. The CheckStatus may return warnings, 1389 * errors, examples, or demos. We may expand that in the future. 1390 * 1391 * <p>The code to add the CheckStatus will look something like:: 1392 * 1393 * <pre> 1394 * result.add(new CheckStatus() 1395 * .setType(CheckStatus.errorType) 1396 * .setMessage("Value should be {0}", new Object[] { pattern })); 1397 * </pre> 1398 */ handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)1399 public abstract CheckCLDR handleCheck( 1400 String path, String fullPath, String value, Options options, List<CheckStatus> result); 1401 1402 /** Only for use in ConsoleCheck, for debugging */ handleFinish()1403 public void handleFinish() {} 1404 1405 /** 1406 * Internal class used to bundle up a number of Checks. 1407 * 1408 * @author davis 1409 */ 1410 static class CompoundCheckCLDR extends CheckCLDR { 1411 private Matcher filter; 1412 private List<CheckCLDR> checkList = new ArrayList<>(); 1413 private List<CheckCLDR> filteredCheckList = new ArrayList<>(); 1414 add(CheckCLDR item)1415 public CompoundCheckCLDR add(CheckCLDR item) { 1416 checkList.add(item); 1417 if (filter == null) { 1418 filteredCheckList.add(item); 1419 } else { 1420 final String className = item.getClass().getName(); 1421 if (filter.reset(className).find()) { 1422 filteredCheckList.add(item); 1423 } 1424 } 1425 return this; 1426 } 1427 1428 @Override handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)1429 public CheckCLDR handleCheck( 1430 String path, 1431 String fullPath, 1432 String value, 1433 Options options, 1434 List<CheckStatus> result) { 1435 result.clear(); 1436 1437 if (!accept(result)) return this; 1438 1439 // If we're being asked to run tests for an inheritance marker, then we need to change 1440 // it 1441 // to the "real" value first before running tests. Testing the value 1442 // CldrUtility.INHERITANCE_MARKER ("↑↑↑") doesn't make sense. 1443 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 1444 value = getCldrFileToCheck().getBaileyValue(path, null, null); 1445 } 1446 for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) { 1447 CheckCLDR item = it.next(); 1448 // skip proposed items in final testing. 1449 if (Phase.FINAL_TESTING == item.getPhase()) { 1450 if (path.contains("proposed") && path.contains("[@alt=")) { 1451 continue; 1452 } 1453 } 1454 try { 1455 if (!item.isSkipTest()) { 1456 item.handleCheck(path, fullPath, value, options, result); 1457 } 1458 } catch (Exception e) { 1459 addError(result, item, e); 1460 return this; 1461 } 1462 } 1463 return this; 1464 } 1465 1466 @Override handleFinish()1467 public void handleFinish() { 1468 for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) { 1469 CheckCLDR item = it.next(); 1470 item.handleFinish(); 1471 } 1472 } 1473 1474 @Override handleGetExamples( String path, String fullPath, String value, Options options, List<CheckStatus> result)1475 protected CheckCLDR handleGetExamples( 1476 String path, 1477 String fullPath, 1478 String value, 1479 Options options, 1480 List<CheckStatus> result) { 1481 result.clear(); 1482 for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) { 1483 CheckCLDR item = it.next(); 1484 try { 1485 item.handleGetExamples(path, fullPath, value, options, result); 1486 } catch (Exception e) { 1487 addError(result, item, e); 1488 return this; 1489 } 1490 } 1491 return this; 1492 } 1493 addError(List<CheckStatus> result, CheckCLDR item, Exception e)1494 private void addError(List<CheckStatus> result, CheckCLDR item, Exception e) { 1495 // send to java.util.logging, useful for servers 1496 logger.log( 1497 java.util.logging.Level.SEVERE, 1498 e, 1499 () -> { 1500 String locale = "(unknown)"; 1501 if (item.cldrFileToCheck != null) { 1502 locale = item.cldrFileToCheck.getLocaleID(); 1503 } 1504 return String.format( 1505 "Internal error: %s in %s", item.getClass().getName(), locale); 1506 }); 1507 // also add as a check 1508 result.add( 1509 new CheckStatus() 1510 .setCause(this) 1511 .setMainType(CheckStatus.errorType) 1512 .setSubtype(Subtype.internalError) 1513 .setMessage( 1514 "Internal error in {0}. Exception: {1}, Message: {2}, Trace: {3}", 1515 new Object[] { 1516 item.getClass().getName(), 1517 e.getClass().getName(), 1518 e, 1519 Arrays.asList(e.getStackTrace()) 1520 })); 1521 } 1522 1523 @Override handleCheckPossibleErrors( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)1524 public void handleCheckPossibleErrors( 1525 CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) { 1526 ElapsedTimer testTime = null, testOverallTime = null; 1527 if (cldrFileToCheck == null) return; 1528 boolean SHOW_TIMES = options.contains(Options.Option.SHOW_TIMES); 1529 setPhase(Phase.forString(options.get(Options.Option.phase))); 1530 if (SHOW_TIMES) 1531 testOverallTime = new ElapsedTimer("Test setup time for setCldrFileToCheck: {0}"); 1532 super.handleCheckPossibleErrors(cldrFileToCheck, options, possibleErrors); 1533 possibleErrors.clear(); 1534 1535 for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) { 1536 CheckCLDR item = it.next(); 1537 if (SHOW_TIMES) 1538 testTime = 1539 new ElapsedTimer( 1540 "Test setup time for " + item.getClass().toString() + ": {0}"); 1541 try { 1542 item.setPhase(getPhase()); 1543 item.setCldrFileToCheck(cldrFileToCheck, options, possibleErrors); 1544 if (SHOW_TIMES) { 1545 if (item.isSkipTest()) { 1546 System.out.println("Disabled : " + testTime); 1547 } else { 1548 System.out.println("OK : " + testTime); 1549 } 1550 } 1551 } catch (RuntimeException e) { 1552 addError(possibleErrors, item, e); 1553 if (SHOW_TIMES) System.out.println("ERR: " + testTime + " - " + e.toString()); 1554 } 1555 } 1556 if (SHOW_TIMES) System.out.println("Overall: " + testOverallTime + ": {0}"); 1557 // all of these are entire locale 1558 possibleErrors.forEach(e -> e.setEntireLocale()); 1559 } 1560 getFilter()1561 public Matcher getFilter() { 1562 return filter; 1563 } 1564 setFilter(Matcher filter)1565 public CompoundCheckCLDR setFilter(Matcher filter) { 1566 this.filter = filter; 1567 filteredCheckList.clear(); 1568 for (Iterator<CheckCLDR> it = checkList.iterator(); it.hasNext(); ) { 1569 CheckCLDR item = it.next(); 1570 if (filter == null || filter.reset(item.getClass().getName()).matches()) { 1571 filteredCheckList.add(item); 1572 item.handleSetCldrFileToCheck(getCldrFileToCheck(), (Options) null, null); 1573 } 1574 } 1575 return this; 1576 } 1577 getFilteredTests()1578 public String getFilteredTests() { 1579 return filteredCheckList.toString(); 1580 } 1581 getFilteredTestList()1582 public List<CheckCLDR> getFilteredTestList() { 1583 return filteredCheckList; 1584 } 1585 } 1586 1587 @Override getPhase()1588 public Phase getPhase() { 1589 return phase; 1590 } 1591 setPhase(Phase phase)1592 public void setPhase(Phase phase) { 1593 this.phase = phase; 1594 } 1595 1596 /** A map of error/warning types to their filters. */ 1597 private static Supplier<List<R3<Pattern, Subtype, Pattern>>> filterSupplier = 1598 Suppliers.memoize( 1599 () -> { 1600 final List<R3<Pattern, Subtype, Pattern>> newFilters = new ArrayList<>(); 1601 RegexFileParser fileParser = new RegexFileParser(); 1602 fileParser.setLineParser( 1603 new RegexLineParser() { 1604 @Override 1605 public void parse(String line) { 1606 String[] fields = line.split("\\s*;\\s*"); 1607 Subtype subtype = Subtype.valueOf(fields[0]); 1608 Pattern locale = PatternCache.get(fields[1]); 1609 Pattern xpathRegex = 1610 PatternCache.get( 1611 fields[2].replaceAll("\\[@", "\\\\[@")); 1612 newFilters.add(new R3<>(locale, subtype, xpathRegex)); 1613 } 1614 }); 1615 fileParser.parse( 1616 CheckCLDR.class, 1617 "/org/unicode/cldr/util/data/CheckCLDR-exceptions.txt"); 1618 return ImmutableList.copyOf(newFilters); 1619 }); 1620 getAllFilters()1621 private static final List<R3<Pattern, Subtype, Pattern>> getAllFilters() { 1622 return filterSupplier.get(); 1623 } 1624 1625 /** 1626 * Checks if a status should be excluded from the list of results returned from CheckCLDR. 1627 * 1628 * @param xpath the xpath that the status belongs to 1629 * @param status the status 1630 * @return true if the status should be included 1631 */ shouldExcludeStatus(String xpath, CheckStatus status)1632 private boolean shouldExcludeStatus(String xpath, CheckStatus status) { 1633 List<Pattern> xpathPatterns = filtersForLocale.get(status.getSubtype()); 1634 if (xpathPatterns == null) { 1635 return false; 1636 } 1637 for (Pattern xpathPattern : xpathPatterns) { 1638 if (xpathPattern.matcher(xpath).matches()) { 1639 return true; 1640 } 1641 } 1642 return false; 1643 } 1644 getEnglishFile()1645 public CLDRFile getEnglishFile() { 1646 return englishFile; 1647 } 1648 setEnglishFile(CLDRFile englishFile)1649 public void setEnglishFile(CLDRFile englishFile) { 1650 this.englishFile = englishFile; 1651 } 1652 fixedValueIfInherited(String value, String path)1653 public CharSequence fixedValueIfInherited(String value, String path) { 1654 return !CldrUtility.INHERITANCE_MARKER.equals(value) 1655 ? value 1656 : getCldrFileToCheck().getStringValueWithBailey(path); 1657 } 1658 } 1659