1 package org.unicode.cldr.util; 2 3 import java.io.File; 4 import java.io.IOException; 5 import java.io.PrintWriter; 6 import java.io.StringWriter; 7 import java.io.Writer; 8 import java.util.ArrayList; 9 import java.util.Arrays; 10 import java.util.Collections; 11 import java.util.EnumSet; 12 import java.util.HashMap; 13 import java.util.HashSet; 14 import java.util.LinkedHashSet; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.Map.Entry; 18 import java.util.Objects; 19 import java.util.Set; 20 import java.util.TreeMap; 21 import java.util.TreeSet; 22 import java.util.regex.Matcher; 23 import java.util.regex.Pattern; 24 25 import org.unicode.cldr.draft.FileUtilities; 26 import org.unicode.cldr.test.CheckCLDR; 27 import org.unicode.cldr.test.CheckCLDR.CheckStatus; 28 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype; 29 import org.unicode.cldr.test.CheckCoverage; 30 import org.unicode.cldr.test.CheckNew; 31 import org.unicode.cldr.test.CoverageLevel2; 32 import org.unicode.cldr.test.OutdatedPaths; 33 import org.unicode.cldr.test.SubmissionLocales; 34 import org.unicode.cldr.tool.Option; 35 import org.unicode.cldr.tool.Option.Options; 36 import org.unicode.cldr.util.CLDRFile.Status; 37 import org.unicode.cldr.util.PathHeader.PageId; 38 import org.unicode.cldr.util.PathHeader.SectionId; 39 import org.unicode.cldr.util.StandardCodes.LocaleCoverageType; 40 41 import com.ibm.icu.impl.Relation; 42 import com.ibm.icu.impl.Row; 43 import com.ibm.icu.impl.Row.R2; 44 import com.ibm.icu.text.Collator; 45 import com.ibm.icu.text.NumberFormat; 46 import com.ibm.icu.text.UnicodeSet; 47 import com.ibm.icu.util.ICUUncheckedIOException; 48 import com.ibm.icu.util.Output; 49 import com.ibm.icu.util.ULocale; 50 51 /** 52 * Provides a HTML tables showing the important issues for vetters to review for 53 * a given locale. See the main for an example. Most elements have CSS styles, 54 * allowing for customization of the display. 55 * 56 * @author markdavis 57 */ 58 public class VettingViewer<T> { 59 60 private static boolean SHOW_SUBTYPES = true; // CldrUtility.getProperty("SHOW_SUBTYPES", "false").equals("true"); 61 62 private static final String CONNECT_PREFIX = "₍_"; 63 private static final String CONNECT_SUFFIX = "₎"; 64 65 private static final String TH_AND_STYLES = "<th class='tv-th' style='text-align:left'>"; 66 67 private static final String SPLIT_CHAR = "\uFFFE"; 68 69 private static final String TEST_PATH = "//ldml/localeDisplayNames/territories/territory[@type=\"SX\"]"; 70 private static final double NANOSECS = 1000000000.0; 71 private static final boolean TESTING = CldrUtility.getProperty("TEST", false); 72 73 public static final Pattern ALT_PROPOSED = PatternCache.get("\\[@alt=\"[^\"]*proposed"); 74 75 public static Set<CheckCLDR.CheckStatus.Subtype> OK_IF_VOTED = EnumSet.of(Subtype.sameAsEnglish); 76 77 public enum Choice { 78 /** 79 * There is a console-check error 80 */ 81 error('E', "Error", "The Survey Tool detected an error in the winning value.", 1), 82 /** 83 * My choice is not the winning item 84 */ 85 weLost( 86 'L', 87 "Losing", 88 "The value that your organization chose (overall) is either not the winning value, or doesn’t have enough votes to be approved. " 89 + "This might be due to a dispute between members of your organization.", 90 2), 91 /** 92 * There is a dispute. 93 */ 94 notApproved('P', "Provisional", "There are not enough votes for this item to be approved (and used).", 3), 95 /** 96 * There is a dispute. 97 */ 98 hasDispute('D', "Disputed", "Different organizations are choosing different values. " 99 + "Please review to approve or reach consensus.", 4), 100 /** 101 * There is a console-check warning 102 */ 103 warning('W', "Warning", "The Survey Tool detected a warning about the winning value.", 5), 104 /** 105 * The English value for the path changed AFTER the current value for 106 * the locale. 107 */ 108 englishChanged('U', "English Changed", 109 "The English value has changed in CLDR, but the corresponding value for your language has not. Check if any changes are needed in your language.", 110 6), 111 /** 112 * The value changed from the baseline 113 */ 114 changedOldValue('C', "Changed", "The winning value was altered from the baseline value. (Informational)", 7), 115 /** 116 * Given the users' coverage, some items are missing. 117 */ 118 missingCoverage( 119 'M', 120 "Missing", 121 "Your current coverage level requires the item to be present. (During the vetting phase, this is informational: you can’t add new values.)", 8), 122 // /** 123 // * There is a console-check error 124 // */ 125 // other('O', "Other", "Everything else."), 126 ; 127 128 public final char abbreviation; 129 public final String buttonLabel; 130 public final String description; 131 public final int order; 132 Choice(char abbreviation, String buttonLabel, String description, int order)133 Choice(char abbreviation, String buttonLabel, String description, int order) { 134 this.abbreviation = abbreviation; 135 this.buttonLabel = TransliteratorUtilities.toHTML.transform(buttonLabel); 136 this.description = TransliteratorUtilities.toHTML.transform(description); 137 this.order = order; 138 139 } 140 appendDisplay(Set<Choice> choices, String htmlMessage, T target)141 public static <T extends Appendable> T appendDisplay(Set<Choice> choices, String htmlMessage, T target) { 142 try { 143 boolean first = true; 144 for (Choice item : choices) { 145 if (first) { 146 first = false; 147 } else { 148 target.append(", "); 149 } 150 item.appendDisplay(htmlMessage, target); 151 } 152 return target; 153 } catch (IOException e) { 154 throw new ICUUncheckedIOException(e); // damn'd checked 155 // exceptions 156 } 157 } 158 appendDisplay(String htmlMessage, T target)159 private <T extends Appendable> void appendDisplay(String htmlMessage, T target) throws IOException { 160 target.append("<span title='") 161 .append(description); 162 if (!htmlMessage.isEmpty()) { 163 target.append(": ") 164 .append(htmlMessage); 165 } 166 target.append("'>") 167 .append(buttonLabel) 168 .append("*</span>"); 169 } 170 fromString(String i)171 public static Choice fromString(String i) { 172 try { 173 return valueOf(i); 174 } catch (NullPointerException e) { 175 throw e; 176 } catch (RuntimeException e) { 177 if (i.isEmpty()) { 178 throw e; 179 } 180 int cp = i.codePointAt(0); 181 for (Choice choice : Choice.values()) { 182 if (cp == choice.abbreviation) { 183 return choice; 184 } 185 } 186 throw e; 187 } 188 } 189 appendRowStyles(Set<Choice> choices, Appendable target)190 public static Appendable appendRowStyles(Set<Choice> choices, Appendable target) { 191 try { 192 target.append("hide"); 193 for (Choice item : choices) { 194 target.append(' ').append("vv").append(Character.toLowerCase(item.abbreviation)); 195 } 196 return target; 197 } catch (IOException e) { 198 throw new ICUUncheckedIOException(e); // damn'd checked 199 // exceptions 200 } 201 } 202 } 203 getOutdatedPaths()204 public static OutdatedPaths getOutdatedPaths() { 205 return outdatedPaths; 206 } 207 208 static private PathHeader.Factory pathTransform; 209 static final Pattern breaks = PatternCache.get("\\|"); 210 static final OutdatedPaths outdatedPaths = new OutdatedPaths(); 211 212 /** 213 * See VoteResolver getStatusForOrganization to see how this is computed. 214 */ 215 public enum VoteStatus { 216 /** 217 * The value for the path is either contributed or approved, and 218 * the user's organization didn't vote. (see class def for null user) 219 */ 220 ok_novotes, 221 222 /** 223 * The value for the path is either contributed or approved, and 224 * the user's organization chose the winning value. (see class def for null user) 225 */ 226 ok, 227 228 /** 229 * The user's organization chose the winning value for the path, but 230 * that value is neither contributed nor approved. (see class def for null user) 231 */ 232 provisionalOrWorse, 233 234 /** 235 * The user's organization's choice is not winning. There may be 236 * insufficient votes to overcome a previously approved value, or other 237 * organizations may be voting against it. (see class def for null user) 238 */ 239 losing, 240 241 /** 242 * There is a dispute, meaning more than one item with votes, or the item with votes didn't win. 243 */ 244 disputed 245 } 246 247 /** 248 * @author markdavis 249 * 250 * @param <T> 251 */ 252 public static interface UsersChoice<T> { 253 /** 254 * Return the value that the user's organization (as a whole) voted for, 255 * or null if none of the users in the organization voted for the path. <br> 256 * NOTE: Would be easier if this were a method on CLDRFile. 257 * NOTE: if user = null, then it must return the absolute winning value. 258 * 259 * @param locale 260 */ getWinningValueForUsersOrganization(CLDRFile cldrFile, String path, T user)261 public String getWinningValueForUsersOrganization(CLDRFile cldrFile, String path, T user); 262 263 /** 264 * 265 * Return the vote status 266 * NOTE: if user = null, then it must disregard the user and never return losing. See VoteStatus. 267 * 268 * @param locale 269 */ getStatusForUsersOrganization(CLDRFile cldrFile, String path, T user)270 public VoteStatus getStatusForUsersOrganization(CLDRFile cldrFile, String path, T user); 271 } 272 273 public static interface ErrorChecker { 274 enum Status { 275 ok, error, warning 276 } 277 278 /** 279 * Initialize an error checker with a cldrFile. MUST be called before 280 * any getErrorStatus. 281 */ initErrorStatus(CLDRFile cldrFile)282 public Status initErrorStatus(CLDRFile cldrFile); 283 284 /** 285 * Return the detailed CheckStatus information. 286 */ getErrorCheckStatus(String path, String value)287 public List<CheckStatus> getErrorCheckStatus(String path, String value); 288 289 /** 290 * Return the status, and append the error message to the status 291 * message. If there are any errors, then the warnings are not included. 292 */ getErrorStatus(String path, String value, StringBuilder statusMessage)293 public Status getErrorStatus(String path, String value, StringBuilder statusMessage); 294 295 /** 296 * Return the status, and append the error message to the status 297 * message, and get the subtypes. If there are any errors, then the warnings are not included. 298 */ getErrorStatus(String path, String value, StringBuilder statusMessage, EnumSet<Subtype> outputSubtypes)299 public Status getErrorStatus(String path, String value, StringBuilder statusMessage, 300 EnumSet<Subtype> outputSubtypes); 301 } 302 303 public static class NoErrorStatus implements ErrorChecker { 304 @SuppressWarnings("unused") 305 @Override initErrorStatus(CLDRFile cldrFile)306 public Status initErrorStatus(CLDRFile cldrFile) { 307 return Status.ok; 308 } 309 310 @SuppressWarnings("unused") 311 @Override getErrorCheckStatus(String path, String value)312 public List<CheckStatus> getErrorCheckStatus(String path, String value) { 313 return Collections.emptyList(); 314 } 315 316 @SuppressWarnings("unused") 317 @Override getErrorStatus(String path, String value, StringBuilder statusMessage)318 public Status getErrorStatus(String path, String value, StringBuilder statusMessage) { 319 return Status.ok; 320 } 321 322 @SuppressWarnings("unused") 323 @Override getErrorStatus(String path, String value, StringBuilder statusMessage, EnumSet<Subtype> outputSubtypes)324 public Status getErrorStatus(String path, String value, StringBuilder statusMessage, 325 EnumSet<Subtype> outputSubtypes) { 326 return Status.ok; 327 } 328 329 } 330 331 public static class DefaultErrorStatus implements ErrorChecker { 332 333 private CheckCLDR checkCldr; 334 private HashMap<String, String> options = new HashMap<>(); 335 private ArrayList<CheckStatus> result = new ArrayList<>(); 336 private CLDRFile cldrFile; 337 private Factory factory; 338 DefaultErrorStatus(Factory cldrFactory)339 public DefaultErrorStatus(Factory cldrFactory) { 340 this.factory = cldrFactory; 341 } 342 343 @Override initErrorStatus(CLDRFile cldrFile)344 public Status initErrorStatus(CLDRFile cldrFile) { 345 this.cldrFile = cldrFile; 346 options = new HashMap<>(); 347 result = new ArrayList<>(); 348 checkCldr = CheckCLDR.getCheckAll(factory, ".*"); 349 checkCldr.setCldrFileToCheck(cldrFile, options, result); 350 return Status.ok; 351 } 352 353 @Override getErrorCheckStatus(String path, String value)354 public List<CheckStatus> getErrorCheckStatus(String path, String value) { 355 String fullPath = cldrFile.getFullXPath(path); 356 ArrayList<CheckStatus> result2 = new ArrayList<>(); 357 checkCldr.check(path, fullPath, value, new CheckCLDR.Options(options), result2); 358 return result2; 359 } 360 361 @Override getErrorStatus(String path, String value, StringBuilder statusMessage)362 public Status getErrorStatus(String path, String value, StringBuilder statusMessage) { 363 return getErrorStatus(path, value, statusMessage, null); 364 } 365 366 @Override getErrorStatus(String path, String value, StringBuilder statusMessage, EnumSet<Subtype> outputSubtypes)367 public Status getErrorStatus(String path, String value, StringBuilder statusMessage, 368 EnumSet<Subtype> outputSubtypes) { 369 Status result0 = Status.ok; 370 StringBuilder errorMessage = new StringBuilder(); 371 String fullPath = cldrFile.getFullXPath(path); 372 checkCldr.check(path, fullPath, value, new CheckCLDR.Options(options), result); 373 for (CheckStatus checkStatus : result) { 374 final CheckCLDR cause = checkStatus.getCause(); 375 /* 376 * CheckCoverage will be shown under Missing, not under Warnings; and 377 * CheckNew will be shown under New, not under Warnings; so skip them here. 378 */ 379 if (cause instanceof CheckCoverage || cause instanceof CheckNew) { 380 continue; 381 } 382 CheckStatus.Type statusType = checkStatus.getType(); 383 if (statusType.equals(CheckStatus.errorType)) { 384 // throw away any accumulated warning messages 385 if (result0 == Status.warning) { 386 errorMessage.setLength(0); 387 if (outputSubtypes != null) { 388 outputSubtypes.clear(); 389 } 390 } 391 result0 = Status.error; 392 if (outputSubtypes != null) { 393 outputSubtypes.add(checkStatus.getSubtype()); 394 } 395 appendToMessage(checkStatus.getMessage(), checkStatus.getSubtype(), errorMessage); 396 } else if (result0 != Status.error && statusType.equals(CheckStatus.warningType)) { 397 result0 = Status.warning; 398 // accumulate all the warning messages 399 if (outputSubtypes != null) { 400 outputSubtypes.add(checkStatus.getSubtype()); 401 } 402 appendToMessage(checkStatus.getMessage(), checkStatus.getSubtype(), errorMessage); 403 } 404 } 405 if (result0 != Status.ok) { 406 appendToMessage(errorMessage, statusMessage); 407 } 408 return result0; 409 } 410 } 411 412 private final Factory cldrFactory; 413 private final CLDRFile englishFile; 414 private final UsersChoice<T> userVoteStatus; 415 private final SupplementalDataInfo supplementalDataInfo; 416 private final String baselineTitle = "Baseline"; 417 private final String currentWinningTitle; 418 private ErrorChecker errorChecker; 419 420 private final Set<String> defaultContentLocales; 421 422 /** 423 * Create the Vetting Viewer. 424 * 425 * @param supplementalDataInfo 426 * @param cldrFactory 427 * @param userVoteStatus 428 * @param currentWinningTitle the title of the next version of CLDR to be released 429 */ VettingViewer(SupplementalDataInfo supplementalDataInfo, Factory cldrFactory, UsersChoice<T> userVoteStatus, String currentWinningTitle)430 public VettingViewer(SupplementalDataInfo supplementalDataInfo, Factory cldrFactory, 431 UsersChoice<T> userVoteStatus, String currentWinningTitle) { 432 433 super(); 434 this.cldrFactory = cldrFactory; 435 englishFile = cldrFactory.make("en", true); 436 if (pathTransform == null) { 437 pathTransform = PathHeader.getFactory(englishFile); 438 } 439 this.userVoteStatus = userVoteStatus; 440 this.supplementalDataInfo = supplementalDataInfo; 441 this.defaultContentLocales = supplementalDataInfo.getDefaultContentLocales(); 442 443 this.currentWinningTitle = currentWinningTitle; 444 reasonsToPaths = Relation.of(new HashMap<String, Set<String>>(), HashSet.class); 445 errorChecker = new DefaultErrorStatus(cldrFactory); 446 } 447 448 public class WritingInfo implements Comparable<WritingInfo> { 449 public final PathHeader codeOutput; 450 public final Set<Choice> problems; 451 public final String htmlMessage; 452 WritingInfo(PathHeader pretty, EnumSet<Choice> problems, CharSequence htmlMessage)453 public WritingInfo(PathHeader pretty, EnumSet<Choice> problems, CharSequence htmlMessage) { 454 super(); 455 this.codeOutput = pretty; 456 this.problems = Collections.unmodifiableSet(problems.clone()); 457 this.htmlMessage = htmlMessage.toString(); 458 } 459 460 @Override compareTo(WritingInfo other)461 public int compareTo(WritingInfo other) { 462 return codeOutput.compareTo(other.codeOutput); 463 } 464 getUrl(CLDRLocale locale)465 public String getUrl(CLDRLocale locale) { 466 return urls.forPathHeader(locale, codeOutput); 467 } 468 } 469 470 /** 471 * Show a table of values, filtering according to the choices here and in 472 * the constructor. 473 * 474 * @param output 475 * @param choices 476 * See the class description for more information. 477 * @param localeId 478 * @param user 479 * @param usersLevel 480 */ generateHtmlErrorTables(Appendable output, EnumSet<Choice> choices, String localeID, T user, Level usersLevel, boolean quick)481 public void generateHtmlErrorTables(Appendable output, EnumSet<Choice> choices, String localeID, T user, 482 Level usersLevel, boolean quick) { 483 484 // Gather the relevant paths 485 // each one will be marked with the choice that it triggered. 486 Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of( 487 new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class); 488 489 CLDRFile sourceFile = cldrFactory.make(localeID, true); 490 491 // Initialize 492 CLDRFile baselineFile = null; 493 if (!quick) { 494 try { 495 Factory baselineFactory = CLDRConfig.getInstance().getCommonAndSeedAndMainAndAnnotationsFactory(); 496 baselineFile = baselineFactory.make(localeID, true); 497 } catch (Exception e) { 498 } 499 } 500 501 FileInfo fileInfo = new FileInfo().getFileInfo(sourceFile, baselineFile, sorted, choices, localeID, user, 502 usersLevel, quick); 503 504 // now write the results out 505 writeTables(output, sourceFile, baselineFile, sorted, choices, fileInfo, quick); 506 } 507 508 /** 509 * Give the list of errors 510 * 511 * @param output 512 * @param choices 513 * See the class description for more information. 514 * @param localeId 515 * @param user 516 * @param usersLevel 517 * @param nonVettingPhase 518 * 519 * Called only by writeVettingViewerOutput 520 */ generateFileInfoReview(EnumSet<Choice> choices, String localeID, T user, Level usersLevel, boolean quick, CLDRFile sourceFile, CLDRFile baselineFile)521 public Relation<R2<SectionId, PageId>, WritingInfo> generateFileInfoReview(EnumSet<Choice> choices, String localeID, T user, 522 Level usersLevel, boolean quick, CLDRFile sourceFile, CLDRFile baselineFile) { 523 524 // Gather the relevant paths 525 // each one will be marked with the choice that it triggered. 526 Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of( 527 new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class); 528 529 new FileInfo().getFileInfo(sourceFile, baselineFile, sorted, choices, localeID, user, 530 usersLevel, quick); 531 532 // now write the results out 533 534 return sorted; 535 } 536 537 class FileInfo { 538 Counter<Choice> problemCounter = new Counter<>(); 539 Counter<Subtype> errorSubtypeCounter = new Counter<>(); 540 Counter<Subtype> warningSubtypeCounter = new Counter<>(); 541 EnumSet<Choice> problems = EnumSet.noneOf(Choice.class); 542 addAll(FileInfo other)543 public void addAll(FileInfo other) { 544 problemCounter.addAll(other.problemCounter); 545 errorSubtypeCounter.addAll(other.errorSubtypeCounter); 546 warningSubtypeCounter.addAll(other.warningSubtypeCounter); 547 } 548 getFileInfo(CLDRFile sourceFile, CLDRFile baselineFile, Relation<R2<SectionId, PageId>, WritingInfo> sorted, EnumSet<Choice> choices, String localeID, T user, Level usersLevel, boolean quick)549 private FileInfo getFileInfo(CLDRFile sourceFile, CLDRFile baselineFile, 550 Relation<R2<SectionId, PageId>, WritingInfo> sorted, 551 EnumSet<Choice> choices, String localeID, 552 T user, Level usersLevel, boolean quick) { 553 return this.getFileInfo(sourceFile, baselineFile, sorted, 554 choices, localeID, 555 user, usersLevel, quick, null); 556 } 557 558 /** 559 * Loop through paths for the Dashboard or the Priority Items Summary 560 * 561 * @param sourceFile 562 * @param baselineFile 563 * @param sorted 564 * @param choices 565 * @param localeID 566 * @param user 567 * @param usersLevel 568 * @param quick 569 * @param xpath 570 * @return 571 */ getFileInfo(CLDRFile sourceFile, CLDRFile baselineFile, Relation<R2<SectionId, PageId>, WritingInfo> sorted, EnumSet<Choice> choices, String localeID, T user, Level usersLevel, boolean quick, String xpath)572 private FileInfo getFileInfo(CLDRFile sourceFile, CLDRFile baselineFile, 573 Relation<R2<SectionId, PageId>, WritingInfo> sorted, 574 EnumSet<Choice> choices, String localeID, 575 T user, Level usersLevel, boolean quick, String xpath) { 576 577 errorChecker.initErrorStatus(sourceFile); 578 Matcher altProposed = ALT_PROPOSED.matcher(""); 579 problems = EnumSet.noneOf(Choice.class); 580 581 // now look through the paths 582 583 StringBuilder htmlMessage = new StringBuilder(); 584 StringBuilder statusMessage = new StringBuilder(); 585 EnumSet<Subtype> subtypes = EnumSet.noneOf(Subtype.class); 586 Set<String> seenSoFar = new HashSet<>(); 587 boolean latin = VettingViewer.isLatinScriptLocale(sourceFile); 588 CLDRFile baselineFileUnresolved = (baselineFile == null) ? null : baselineFile.getUnresolved(); 589 for (String path : sourceFile.fullIterable()) { 590 if (xpath != null && !xpath.equals(path)) 591 continue; 592 String value = sourceFile.getWinningValueForVettingViewer(path); 593 statusMessage.setLength(0); 594 subtypes.clear(); 595 ErrorChecker.Status errorStatus = errorChecker.getErrorStatus(path, value, statusMessage, subtypes); 596 597 if (quick && errorStatus != ErrorChecker.Status.error && errorStatus != ErrorChecker.Status.warning) { //skip all values but errors and warnings if in "quick" mode 598 continue; 599 } 600 601 if (seenSoFar.contains(path)) { 602 continue; 603 } 604 seenSoFar.add(path); 605 progressCallback.nudge(); // Let the user know we're moving along. 606 607 PathHeader pretty = pathTransform.fromPath(path); 608 if (pretty.getSurveyToolStatus() == PathHeader.SurveyToolStatus.HIDE) { 609 continue; 610 } 611 612 // note that the value might be missing! 613 614 // make sure we only look at the real values 615 if (altProposed.reset(path).find()) { 616 continue; 617 } 618 619 if (path.contains("/references")) { 620 continue; 621 } 622 623 Level level = supplementalDataInfo.getCoverageLevel(path, sourceFile.getLocaleID()); 624 625 // skip all but errors above the requested level 626 boolean onlyRecordErrors = false; 627 if (level.compareTo(usersLevel) > 0) { 628 onlyRecordErrors = true; 629 } 630 631 problems.clear(); 632 htmlMessage.setLength(0); 633 634 final String oldValue = (baselineFileUnresolved == null) ? null : baselineFileUnresolved.getWinningValue(path); 635 636 if (CheckCLDR.LIMITED_SUBMISSION) { 637 boolean isError = (errorStatus == ErrorChecker.Status.error); 638 boolean isMissing = (oldValue == null); 639 if (!SubmissionLocales.allowEvenIfLimited(localeID, path, isError, isMissing)) { 640 continue; 641 } 642 } 643 644 if (!onlyRecordErrors && choices.contains(Choice.changedOldValue)) { 645 if (oldValue != null && !oldValue.equals(value)) { 646 problems.add(Choice.changedOldValue); 647 problemCounter.increment(Choice.changedOldValue); 648 } 649 } 650 VoteStatus voteStatus = userVoteStatus.getStatusForUsersOrganization(sourceFile, path, user); 651 boolean itemsOkIfVoted = (voteStatus == VoteStatus.ok); 652 MissingStatus missingStatus = null; 653 654 if (!onlyRecordErrors) { 655 missingStatus = getMissingStatus(sourceFile, path, latin); 656 if (choices.contains(Choice.missingCoverage) && missingStatus == MissingStatus.ABSENT) { 657 problems.add(Choice.missingCoverage); 658 problemCounter.increment(Choice.missingCoverage); 659 } 660 if (SubmissionLocales.pathAllowedInLimitedSubmission(path)) { 661 problems.add(Choice.englishChanged); 662 problemCounter.increment(Choice.englishChanged); 663 } 664 if (!CheckCLDR.LIMITED_SUBMISSION && !itemsOkIfVoted && outdatedPaths.isOutdated(localeID, path)) { 665 if (Objects.equals(value, oldValue) && choices.contains(Choice.englishChanged)) { 666 // check to see if we voted 667 problems.add(Choice.englishChanged); 668 problemCounter.increment(Choice.englishChanged); 669 } 670 } 671 } 672 Choice choice = errorStatus == ErrorChecker.Status.error ? Choice.error 673 : errorStatus == ErrorChecker.Status.warning ? Choice.warning 674 : null; 675 676 if (choice == Choice.error && choices.contains(Choice.error) 677 && (!itemsOkIfVoted 678 || !OK_IF_VOTED.containsAll(subtypes))) { 679 problems.add(choice); 680 appendToMessage(statusMessage, htmlMessage); 681 problemCounter.increment(choice); 682 for (Subtype subtype : subtypes) { 683 errorSubtypeCounter.increment(subtype); 684 } 685 } else if (!onlyRecordErrors && choice == Choice.warning && choices.contains(Choice.warning) 686 && (!itemsOkIfVoted 687 || !OK_IF_VOTED.containsAll(subtypes))) { 688 problems.add(choice); 689 appendToMessage(statusMessage, htmlMessage); 690 problemCounter.increment(choice); 691 for (Subtype subtype : subtypes) { 692 warningSubtypeCounter.increment(subtype); 693 } 694 } 695 if (!onlyRecordErrors) { 696 switch (voteStatus) { 697 case losing: 698 if (choices.contains(Choice.weLost)) { 699 problems.add(Choice.weLost); 700 problemCounter.increment(Choice.weLost); 701 } 702 String usersValue = userVoteStatus.getWinningValueForUsersOrganization(sourceFile, path, user); 703 if (usersValue != null) { 704 usersValue = "Losing value: <" + TransliteratorUtilities.toHTML.transform(usersValue) + ">"; 705 appendToMessage(usersValue, htmlMessage); 706 } 707 break; 708 case disputed: 709 if (choices.contains(Choice.hasDispute)) { 710 problems.add(Choice.hasDispute); 711 problemCounter.increment(Choice.hasDispute); 712 } 713 break; 714 case provisionalOrWorse: 715 if (missingStatus == MissingStatus.PRESENT && choices.contains(Choice.notApproved)) { 716 problems.add(Choice.notApproved); 717 problemCounter.increment(Choice.notApproved); 718 } 719 break; 720 default: 721 } 722 } 723 724 if (xpath != null) 725 return this; 726 727 if (!problems.isEmpty()) { 728 if (sorted != null) { 729 reasonsToPaths.clear(); 730 R2<SectionId, PageId> group = Row.of(pretty.getSectionId(), pretty.getPageId()); 731 732 sorted.put(group, new WritingInfo(pretty, problems, htmlMessage)); 733 } 734 } 735 736 } 737 return this; 738 } 739 } 740 741 public static final class LocalesWithExplicitLevel implements Predicate<String> { 742 private final Organization org; 743 private final Level desiredLevel; 744 LocalesWithExplicitLevel(Organization org, Level level)745 public LocalesWithExplicitLevel(Organization org, Level level) { 746 this.org = org; 747 this.desiredLevel = level; 748 } 749 750 @Override is(String localeId)751 public boolean is(String localeId) { 752 Output<LocaleCoverageType> output = new Output<>(); 753 // For admin - return true if SOME organization has explicit coverage for the locale 754 // TODO: Make admin pick up any locale that has a vote 755 if (org.equals(Organization.surveytool)) { 756 for (Organization checkorg : Organization.values()) { 757 StandardCodes.make().getLocaleCoverageLevel(checkorg, localeId, output); 758 if (output.value == StandardCodes.LocaleCoverageType.explicit) { 759 return true; 760 } 761 } 762 return false; 763 } else { 764 Level level = StandardCodes.make().getLocaleCoverageLevel(org, localeId, output); 765 return desiredLevel == level && output.value == StandardCodes.LocaleCoverageType.explicit; 766 } 767 } 768 } 769 generateSummaryHtmlErrorTables(Appendable output, EnumSet<Choice> choices, T organization)770 public void generateSummaryHtmlErrorTables(Appendable output, EnumSet<Choice> choices, T organization) { 771 String helpUrl = "http://cldr.unicode.org/translation/getting-started/vetting-view#TOC-Priority-Items"; 772 try { 773 output 774 .append("<p>The following summarizes the Priority Items across locales, " + 775 "using the default coverage levels for your organization for each locale. " + 776 "Before using, please read the instructions at " + 777 "<a target='CLDR_ST_DOCS' href='" + helpUrl + "'>Priority " + 778 "Items Summary</a>.</p>\n"); 779 780 StringBuilder headerRow = new StringBuilder(); 781 headerRow 782 .append("<tr class='tvs-tr'>") 783 .append(TH_AND_STYLES) 784 .append("Locale</th>") 785 .append(TH_AND_STYLES) 786 .append("Codes</th>"); 787 for (Choice choice : choices) { 788 headerRow.append("<th class='tv-th'>"); 789 choice.appendDisplay("", headerRow); 790 headerRow.append("</th>"); 791 } 792 headerRow.append("</tr>\n"); 793 String header = headerRow.toString(); 794 795 if (organization.equals(Organization.surveytool)) { 796 writeSummaryTable(output, header, Level.COMPREHENSIVE, choices, organization); 797 } else { 798 for (Level level : Level.values()) { 799 writeSummaryTable(output, header, level, choices, organization); 800 } 801 } 802 } catch (IOException e) { 803 throw new ICUUncheckedIOException(e); // dang'ed checked exceptions 804 } 805 806 } 807 808 /** 809 * 810 * @param output 811 * @param header 812 * @param desiredLevel 813 * @param choices 814 * @param organization 815 * @throws IOException 816 * 817 * Called only by generateSummaryHtmlErrorTables 818 */ writeSummaryTable(Appendable output, String header, Level desiredLevel, EnumSet<Choice> choices, T organization)819 private void writeSummaryTable(Appendable output, String header, Level desiredLevel, 820 EnumSet<Choice> choices, T organization) throws IOException { 821 822 Map<String, String> sortedNames = new TreeMap<>(Collator.getInstance()); 823 824 // Gather the relevant paths 825 // Each one will be marked with the choice that it triggered. 826 827 // TODO Fix HACK 828 // We are going to ignore the predicate for now, just using the locales that have explicit coverage. 829 // in that locale, or allow all locales for admin@ 830 LocalesWithExplicitLevel includeLocale = new LocalesWithExplicitLevel((Organization) organization, desiredLevel); 831 832 for (String localeID : cldrFactory.getAvailable()) { 833 if (defaultContentLocales.contains(localeID) 834 || localeID.equals("en") 835 || !includeLocale.is(localeID)) { 836 continue; 837 } 838 839 sortedNames.put(getName(localeID), localeID); 840 } 841 if (sortedNames.isEmpty()) { 842 return; 843 } 844 845 EnumSet<Choice> thingsThatRequireOldFile = EnumSet.of(Choice.englishChanged, Choice.missingCoverage, Choice.changedOldValue); 846 EnumSet<Choice> ourChoicesThatRequireOldFile = choices.clone(); 847 ourChoicesThatRequireOldFile.retainAll(thingsThatRequireOldFile); 848 output.append("<h2>Level: ").append(desiredLevel.toString()).append("</h2>"); 849 output.append("<table class='tvs-table'>\n"); 850 char lastChar = ' '; 851 Map<String, FileInfo> localeNameToFileInfo = new TreeMap<>(); 852 FileInfo totals = new FileInfo(); 853 854 for (Entry<String, String> entry : sortedNames.entrySet()) { 855 String name = entry.getKey(); 856 String localeID = entry.getValue(); 857 // Initialize 858 859 CLDRFile sourceFile = cldrFactory.make(localeID, true); 860 861 CLDRFile baselineFile = null; 862 if (!ourChoicesThatRequireOldFile.isEmpty()) { 863 try { 864 Factory baselineFactory = CLDRConfig.getInstance().getCommonAndSeedAndMainAndAnnotationsFactory(); 865 baselineFile = baselineFactory.make(localeID, true); 866 } catch (Exception e) { 867 } 868 } 869 Level level = Level.MODERN; 870 if (organization != null) { 871 level = StandardCodes.make().getLocaleCoverageLevel(organization.toString(), localeID); 872 } 873 FileInfo fileInfo = new FileInfo().getFileInfo(sourceFile, baselineFile, null, choices, localeID, organization, level, false); 874 localeNameToFileInfo.put(name, fileInfo); 875 totals.addAll(fileInfo); 876 877 char nextChar = name.charAt(0); 878 if (lastChar != nextChar) { 879 output.append(header); 880 lastChar = nextChar; 881 } 882 883 writeSummaryRow(output, choices, fileInfo.problemCounter, name, localeID); 884 885 if (output instanceof Writer) { 886 ((Writer) output).flush(); 887 } 888 } 889 output.append(header); 890 writeSummaryRow(output, choices, totals.problemCounter, "Total", null); 891 output.append("</table>"); 892 if (SHOW_SUBTYPES) { 893 showSubtypes(output, sortedNames, localeNameToFileInfo, totals, true); 894 showSubtypes(output, sortedNames, localeNameToFileInfo, totals, false); 895 } 896 } 897 showSubtypes(Appendable output, Map<String, String> sortedNames, Map<String, FileInfo> localeNameToFileInfo, FileInfo totals, boolean errors)898 private void showSubtypes(Appendable output, Map<String, String> sortedNames, 899 Map<String, FileInfo> localeNameToFileInfo, 900 FileInfo totals, 901 boolean errors) throws IOException { 902 output.append("<h3>Details: ").append(errors ? "Error Types" : "Warning Types").append("</h3>"); 903 output.append("<table class='tvs-table'>"); 904 Counter<Subtype> subtypeCounterTotals = errors ? totals.errorSubtypeCounter : totals.warningSubtypeCounter; 905 Set<Subtype> sortedBySize = subtypeCounterTotals.getKeysetSortedByCount(false); 906 907 // header 908 writeDetailHeader(sortedBySize, output); 909 910 // items 911 for (Entry<String, FileInfo> entry : localeNameToFileInfo.entrySet()) { 912 Counter<Subtype> counter = errors ? entry.getValue().errorSubtypeCounter : entry.getValue().warningSubtypeCounter; 913 if (counter.getTotal() == 0) { 914 continue; 915 } 916 String name = entry.getKey(); 917 String localeID = sortedNames.get(name); 918 output.append("<tr>").append(TH_AND_STYLES); 919 appendNameAndCode(name, localeID, output); 920 output.append("</th>"); 921 for (Subtype subtype : sortedBySize) { 922 long count = counter.get(subtype); 923 output.append("<td class='tvs-count'>"); 924 if (count != 0) { 925 output.append(nf.format(count)); 926 } 927 output.append("</td>"); 928 } 929 } 930 931 // subtotals 932 writeDetailHeader(sortedBySize, output); 933 output.append("<tr>").append(TH_AND_STYLES).append("<i>Total</i>").append("</th>").append(TH_AND_STYLES).append("</th>"); 934 for (Subtype subtype : sortedBySize) { 935 long count = subtypeCounterTotals.get(subtype); 936 output.append("<td class='tvs-count'>"); 937 if (count != 0) { 938 output.append("<b>").append(nf.format(count)).append("</b>"); 939 } 940 output.append("</td>"); 941 } 942 output.append("</table>"); 943 } 944 writeDetailHeader(Set<Subtype> sortedBySize, Appendable output)945 private void writeDetailHeader(Set<Subtype> sortedBySize, Appendable output) throws IOException { 946 output.append("<tr>") 947 .append(TH_AND_STYLES).append("Name").append("</th>") 948 .append(TH_AND_STYLES).append("ID").append("</th>"); 949 for (Subtype subtype : sortedBySize) { 950 output.append(TH_AND_STYLES).append(subtype.toString()).append("</th>"); 951 } 952 } 953 writeSummaryRow(Appendable output, EnumSet<Choice> choices, Counter<Choice> problemCounter, String name, String localeID)954 private void writeSummaryRow(Appendable output, EnumSet<Choice> choices, Counter<Choice> problemCounter, 955 String name, String localeID) throws IOException { 956 output 957 .append("<tr>") 958 .append(TH_AND_STYLES); 959 if (localeID == null) { 960 output 961 .append("<i>") 962 .append(name) 963 .append("</i>") 964 .append("</th>") 965 .append(TH_AND_STYLES); 966 } else { 967 appendNameAndCode(name, localeID, output); 968 } 969 output.append("</th>\n"); 970 for (Choice choice : choices) { 971 long count = problemCounter.get(choice); 972 output.append("<td class='tvs-count'>"); 973 if (localeID == null) { 974 output.append("<b>"); 975 } 976 output.append(nf.format(count)); 977 if (localeID == null) { 978 output.append("</b>"); 979 } 980 output.append("</td>\n"); 981 } 982 output.append("</tr>\n"); 983 } 984 appendNameAndCode(String name, String localeID, Appendable output)985 private void appendNameAndCode(String name, String localeID, Appendable output) throws IOException { 986 String[] names = name.split(SPLIT_CHAR); 987 output 988 .append("<a href='" + urls.forSpecial(CLDRURLS.Special.Vetting, CLDRLocale.getInstance(localeID))) 989 .append("'>") 990 .append(TransliteratorUtilities.toHTML.transform(names[0])) 991 .append("</a>") 992 .append("</th>") 993 .append(TH_AND_STYLES) 994 .append("<code>") 995 .append(names[1]) 996 .append("</code>"); 997 } 998 999 LanguageTagParser ltp = new LanguageTagParser(); 1000 getName(String localeID)1001 private String getName(String localeID) { 1002 Set<String> contents = supplementalDataInfo.getEquivalentsForLocale(localeID); 1003 // put in special character that can be split on later 1004 String name = englishFile.getName(localeID, true, CLDRFile.SHORT_ALTS) + SPLIT_CHAR + gatherCodes(contents); 1005 return name; 1006 } 1007 1008 /** 1009 * Collapse the names 1010 {en_Cyrl, en_Cyrl_US} => en_Cyrl(_US) 1011 {en_GB, en_Latn_GB} => en(_Latn)_GB 1012 {en, en_US, en_Latn, en_Latn_US} => en(_Latn)(_US) 1013 {az_IR, az_Arab, az_Arab_IR} => az_IR, az_Arab(_IR) 1014 */ gatherCodes(Set<String> contents)1015 public static String gatherCodes(Set<String> contents) { 1016 Set<Set<String>> source = new LinkedHashSet<>(); 1017 for (String s : contents) { 1018 source.add(new LinkedHashSet<>(Arrays.asList(s.split("_")))); 1019 } 1020 Set<Set<String>> oldSource = new LinkedHashSet<>(); 1021 1022 do { 1023 // exchange source/target 1024 oldSource.clear(); 1025 oldSource.addAll(source); 1026 source.clear(); 1027 Set<String> last = null; 1028 for (Set<String> ss : oldSource) { 1029 if (last == null) { 1030 last = ss; 1031 } else { 1032 if (ss.containsAll(last)) { 1033 last = combine(last, ss); 1034 } else { 1035 source.add(last); 1036 last = ss; 1037 } 1038 } 1039 } 1040 source.add(last); 1041 } while (oldSource.size() != source.size()); 1042 1043 StringBuilder b = new StringBuilder(); 1044 for (Set<String> stringSet : source) { 1045 if (b.length() != 0) { 1046 b.append(", "); 1047 } 1048 String sep = ""; 1049 for (String string : stringSet) { 1050 if (string.startsWith(CONNECT_PREFIX)) { 1051 b.append(string + CONNECT_SUFFIX); 1052 } else { 1053 b.append(sep + string); 1054 } 1055 sep = "_"; 1056 } 1057 } 1058 return b.toString(); 1059 } 1060 combine(Set<String> last, Set<String> ss)1061 private static Set<String> combine(Set<String> last, Set<String> ss) { 1062 LinkedHashSet<String> result = new LinkedHashSet<>(); 1063 for (String s : ss) { 1064 if (last.contains(s)) { 1065 result.add(s); 1066 } else { 1067 result.add(CONNECT_PREFIX + s); 1068 } 1069 } 1070 return result; 1071 } 1072 1073 public enum MissingStatus { 1074 PRESENT, ALIASED, MISSING_OK, ROOT_OK, ABSENT 1075 } 1076 1077 /** 1078 * Get the MissingStatus 1079 * 1080 * @param sourceFile the CLDRFile 1081 * @param path the path 1082 * @param latin boolean from isLatinScriptLocale, passed to isMissingOk 1083 * @return the MissingStatus 1084 */ getMissingStatus(CLDRFile sourceFile, String path, boolean latin)1085 public static MissingStatus getMissingStatus(CLDRFile sourceFile, String path, boolean latin) { 1086 if (sourceFile == null) { 1087 return MissingStatus.ABSENT; 1088 } 1089 if ("root".equals(sourceFile.getLocaleID()) || path.startsWith("//ldml/layout/orientation/")) { 1090 return MissingStatus.MISSING_OK; 1091 } 1092 if (path.equals(TEST_PATH)) { 1093 int debug = 1; 1094 } 1095 MissingStatus result; 1096 1097 String value = sourceFile.getStringValue(path); 1098 Status status = new Status(); 1099 sourceFile.getSourceLocaleID(path, status); 1100 boolean isAliased = !path.equals(status.pathWhereFound); // this was path.equals, which would be incorrect! 1101 1102 if (value == null) { 1103 result = ValuePathStatus.isMissingOk(sourceFile, path, latin, isAliased) ? MissingStatus.MISSING_OK : MissingStatus.ABSENT; 1104 } else { 1105 /* 1106 * skipInheritanceMarker must be false for getSourceLocaleIdExtended here, since INHERITANCE_MARKER 1107 * may be found if there are votes for inheritance, in which case we must not skip up to "root" and 1108 * treat the item as missing. Reference: https://unicode.org/cldr/trac/ticket/11765 1109 */ 1110 String localeFound = sourceFile.getSourceLocaleIdExtended(path, status, false /* skipInheritanceMarker */); 1111 /* 1112 * Only count it as missing IF the (localeFound is root or codeFallback) 1113 * AND the aliasing didn't change the path 1114 */ 1115 if (localeFound.equals("root") || localeFound.equals(XMLSource.CODE_FALLBACK_ID)) { 1116 result = ValuePathStatus.isMissingOk(sourceFile, path, latin, isAliased) 1117 || sourceFile.getLocaleID().equals("en") 1118 ? MissingStatus.ROOT_OK 1119 : MissingStatus.ABSENT; 1120 } else if (isAliased) { 1121 result = MissingStatus.ALIASED; 1122 } else { 1123 result = MissingStatus.PRESENT; 1124 } 1125 } 1126 return result; 1127 } 1128 1129 public static final UnicodeSet LATIN = ValuePathStatus.LATIN; 1130 isLatinScriptLocale(CLDRFile sourceFile)1131 public static boolean isLatinScriptLocale(CLDRFile sourceFile) { 1132 return ValuePathStatus.isLatinScriptLocale(sourceFile); 1133 } 1134 appendToMessage(CharSequence usersValue, Subtype subtype, StringBuilder testMessage)1135 private static StringBuilder appendToMessage(CharSequence usersValue, Subtype subtype, StringBuilder testMessage) { 1136 if (subtype != null) { 1137 usersValue = "<" + subtype + "> " + usersValue; 1138 } 1139 return appendToMessage(usersValue, testMessage); 1140 } 1141 appendToMessage(CharSequence usersValue, StringBuilder testMessage)1142 private static StringBuilder appendToMessage(CharSequence usersValue, StringBuilder testMessage) { 1143 if (usersValue.length() == 0) { 1144 return testMessage; 1145 } 1146 if (testMessage.length() != 0) { 1147 testMessage.append("<br>"); 1148 } 1149 return testMessage.append(usersValue); 1150 } 1151 1152 static final NumberFormat nf = NumberFormat.getIntegerInstance(ULocale.ENGLISH); 1153 private Relation<String, String> reasonsToPaths; 1154 private CLDRURLS urls = CLDRConfig.getInstance().urls(); 1155 1156 static { 1157 nf.setGroupingUsed(true); 1158 } 1159 1160 /** 1161 * Class that allows the relaying of progress information 1162 * 1163 * @author srl 1164 * 1165 */ 1166 public static class ProgressCallback { 1167 /** 1168 * Note any progress. This will be called before any output is printed. 1169 * It will be called approximately once per xpath. 1170 */ nudge()1171 public void nudge() { 1172 } 1173 1174 /** 1175 * Called when all operations are complete. 1176 */ done()1177 public void done() { 1178 } 1179 } 1180 1181 /* 1182 * null instance by default 1183 */ 1184 private ProgressCallback progressCallback = new ProgressCallback(); 1185 1186 /** 1187 * Select a new callback. Must be set before running. 1188 * 1189 * @return 1190 * 1191 */ setProgressCallback(ProgressCallback newCallback)1192 public VettingViewer<T> setProgressCallback(ProgressCallback newCallback) { 1193 progressCallback = newCallback; 1194 return this; 1195 } 1196 getErrorChecker()1197 public ErrorChecker getErrorChecker() { 1198 return errorChecker; 1199 } 1200 1201 /** 1202 * Select a new error checker. Must be set before running. 1203 * 1204 * @return 1205 * 1206 */ setErrorChecker(ErrorChecker errorChecker)1207 public VettingViewer<T> setErrorChecker(ErrorChecker errorChecker) { 1208 this.errorChecker = errorChecker; 1209 return this; 1210 } 1211 1212 /** 1213 * Provide the styles for inclusion into the ST <head> element. 1214 * 1215 * @return 1216 */ getHeaderStyles()1217 public static String getHeaderStyles() { 1218 return "<style>\n" 1219 + ".hide {display:none}\n" 1220 + ".vve {}\n" 1221 + ".vvn {}\n" 1222 + ".vvp {}\n" 1223 + ".vvl {}\n" 1224 + ".vvm {}\n" 1225 + ".vvu {}\n" 1226 + ".vvw {}\n" 1227 + ".vvd {}\n" 1228 + ".vvo {}\n" 1229 + "</style>"; 1230 } 1231 writeTables(Appendable output, CLDRFile sourceFile, CLDRFile baselineFile, Relation<R2<SectionId, PageId>, WritingInfo> sorted, EnumSet<Choice> choices, FileInfo outputFileInfo, boolean quick)1232 private void writeTables(Appendable output, CLDRFile sourceFile, CLDRFile baselineFile, 1233 Relation<R2<SectionId, PageId>, WritingInfo> sorted, 1234 EnumSet<Choice> choices, 1235 FileInfo outputFileInfo, 1236 boolean quick) { 1237 try { 1238 1239 boolean latin = VettingViewer.isLatinScriptLocale(sourceFile); 1240 1241 output.append("<h2>Summary</h2>\n") 1242 .append("<p><i>It is important that you read " + 1243 "<a target='CLDR-ST-DOCS' href='http://cldr.unicode.org/translation/vetting-view'>" + 1244 "Priority Items</a> before starting!</i></p>") 1245 .append("<form name='checkboxes' action='#'>\n") 1246 .append("<table class='tvs-table'>\n") 1247 .append("<tr class='tvs-tr'>" + 1248 "<th class='tv-th'>Count</th>" + 1249 "<th class='tv-th'>Issue</th>" + 1250 "<th class='tv-th'>Description</th>" + 1251 "</tr>\n"); 1252 1253 // find the choice to check 1254 // OLD if !vetting and missing != 0, use missing. Otherwise pick first. 1255 Choice checkedItem = null; 1256 // if (nonVettingPhase && problemCounter.get(Choice.missingCoverage) != 0) { 1257 // checkedItem = Choice.missingCoverage; 1258 // } 1259 1260 for (Choice choice : choices) { 1261 if (quick && choice != Choice.error && choice != Choice.warning) { //if "quick" mode, only show errors and warnings 1262 continue; 1263 } 1264 long count = outputFileInfo.problemCounter.get(choice); 1265 output.append("<tr><td class='tvs-count'>") 1266 .append(nf.format(count)) 1267 .append("</td>\n\t<td nowrap class='tvs-abb'>") 1268 .append("<input type='checkbox' name='") 1269 .append(Character.toLowerCase(choice.abbreviation)) 1270 .append("' onclick='setStyles()'"); 1271 if (checkedItem == choice || checkedItem == null && count != 0) { 1272 output.append(" checked"); 1273 checkedItem = choice; 1274 } 1275 output.append(">"); 1276 choice.appendDisplay("", output); 1277 output.append("</td>\n\t<td class='tvs-desc'>") 1278 .append(choice.description) 1279 .append("</td></tr>\n"); 1280 } 1281 output.append("</table>\n</form>\n" 1282 + "<script>\n" + 1283 "<!-- \n" + 1284 "setStyles()\n" + 1285 "-->\n" 1286 + "</script>"); 1287 1288 // gather information on choices on each page 1289 1290 Relation<Row.R3<SectionId, PageId, String>, Choice> choicesForHeader = Relation.of( 1291 new HashMap<Row.R3<SectionId, PageId, String>, Set<Choice>>(), HashSet.class); 1292 1293 Relation<Row.R2<SectionId, PageId>, Choice> choicesForSection = Relation.of( 1294 new HashMap<R2<SectionId, PageId>, Set<Choice>>(), HashSet.class); 1295 1296 for (Entry<R2<SectionId, PageId>, Set<WritingInfo>> entry0 : sorted.keyValuesSet()) { 1297 SectionId section = entry0.getKey().get0(); 1298 PageId subsection = entry0.getKey().get1(); 1299 final Set<WritingInfo> rows = entry0.getValue(); 1300 for (WritingInfo pathInfo : rows) { 1301 String header = pathInfo.codeOutput.getHeader(); 1302 Set<Choice> choicesForPath = pathInfo.problems; 1303 choicesForSection.putAll(Row.of(section, subsection), choicesForPath); 1304 choicesForHeader.putAll(Row.of(section, subsection, header), choicesForPath); 1305 } 1306 } 1307 1308 final String localeId = sourceFile.getLocaleID(); 1309 final CLDRLocale locale = CLDRLocale.getInstance(localeId); 1310 int count = 0; 1311 for (Entry<R2<SectionId, PageId>, Set<WritingInfo>> entry0 : sorted.keyValuesSet()) { 1312 SectionId section = entry0.getKey().get0(); 1313 PageId subsection = entry0.getKey().get1(); 1314 final Set<WritingInfo> rows = entry0.getValue(); 1315 1316 rows.iterator().next(); // getUrl(localeId); (no side effect?) 1317 // http://kwanyin.unicode.org/cldr-apps/survey?_=ur&x=scripts 1318 // http://unicode.org/cldr-apps/survey?_=ur&x=scripts 1319 1320 output.append("\n<h2 class='tv-s'>Section: ") 1321 .append(section.toString()) 1322 .append(" — <i><a " + /*target='CLDR_ST-SECTION' */"href='") 1323 .append(urls.forPage(locale, subsection)) 1324 .append("'>Page: ") 1325 .append(subsection.toString()) 1326 .append("</a></i> (" + rows.size() + ")</h2>\n"); 1327 startTable(choicesForSection.get(Row.of(section, subsection)), output); 1328 1329 String oldHeader = ""; 1330 for (WritingInfo pathInfo : rows) { 1331 String header = pathInfo.codeOutput.getHeader(); 1332 String code = pathInfo.codeOutput.getCode(); 1333 String path = pathInfo.codeOutput.getOriginalPath(); 1334 Set<Choice> choicesForPath = pathInfo.problems; 1335 1336 if (!header.equals(oldHeader)) { 1337 Set<Choice> headerChoices = choicesForHeader.get(Row.of(section, subsection, header)); 1338 output.append("<tr class='"); 1339 Choice.appendRowStyles(headerChoices, output); 1340 output.append("'>\n"); 1341 output.append(" <th class='partsection' colSpan='6'>"); 1342 output.append(header); 1343 output.append("</th>\n</tr>\n"); 1344 oldHeader = header; 1345 } 1346 1347 output.append("<tr class='"); 1348 Choice.appendRowStyles(choicesForPath, output); 1349 output.append("'>\n"); 1350 addCell(output, nf.format(++count), null, "tv-num", HTMLType.plain); 1351 // path 1352 addCell(output, code, null, "tv-code", HTMLType.plain); 1353 // English value 1354 if (choicesForPath.contains(Choice.englishChanged)) { 1355 String winning = englishFile.getWinningValue(path); 1356 String cellValue = winning == null ? "<i>missing</i>" : TransliteratorUtilities.toHTML 1357 .transform(winning); 1358 String previous = outdatedPaths.getPreviousEnglish(path); 1359 if (previous != null) { 1360 cellValue += "<br><span style='color:#900'><b>OLD: </b>" 1361 + TransliteratorUtilities.toHTML.transform(previous) + "</span>"; 1362 } else { 1363 cellValue += "<br><b><i>missing</i></b>"; 1364 } 1365 addCell(output, cellValue, null, "tv-eng", HTMLType.markup); 1366 } else { 1367 addCell(output, englishFile.getWinningValue(path), null, "tv-eng", HTMLType.plain); 1368 } 1369 // baseline value 1370 // TODO: should this be baselineFile.getUnresolved()? Compare how getFileInfo calls getMissingStatus 1371 final String oldStringValue = baselineFile == null ? null : baselineFile.getWinningValue(path); 1372 MissingStatus oldValueMissing = getMissingStatus(baselineFile, path, latin); 1373 1374 addCell(output, oldStringValue, null, oldValueMissing != MissingStatus.PRESENT ? "tv-miss" 1375 : "tv-last", HTMLType.plain); 1376 // current winning value 1377 String newWinningValue = sourceFile.getWinningValue(path); 1378 if (Objects.equals(newWinningValue, oldStringValue)) { 1379 newWinningValue = "="; 1380 } 1381 addCell(output, newWinningValue, null, choicesForPath.contains(Choice.missingCoverage) ? "tv-miss" 1382 : "tv-win", HTMLType.plain); 1383 // Fix? 1384 // http://unicode.org/cldr/apps/survey?_=az&xpath=%2F%2Fldml%2FlocaleDisplayNames%2Flanguages%2Flanguage%5B%40type%3D%22az%22%5D 1385 output.append(" <td class='tv-fix'><a target='_blank' href='") 1386 .append(pathInfo.getUrl(locale)) // .append(c)baseUrl + "?_=") 1387 // .append(localeID) 1388 // .append("&xpath=") 1389 // .append(percentEscape.transform(path)) 1390 .append("'>"); 1391 Choice.appendDisplay(choicesForPath, "", output); 1392 // String otherUrl = pathInfo.getUrl(sourceFile.getLocaleID()); 1393 output.append("</a></td>"); 1394 // if (!otherUrl.equals(url)) { 1395 // output.append("<td class='tv-test'><a "+/*target='CLDR_ST-SECTION' */"href='") 1396 // .append(otherUrl) 1397 // .append("'><i>Section*</i></a></td>"); 1398 // } 1399 if (!pathInfo.htmlMessage.isEmpty()) { 1400 addCell(output, pathInfo.htmlMessage, null, "tv-test", HTMLType.markup); 1401 } 1402 output.append("</tr>\n"); 1403 } 1404 output.append("</table>\n"); 1405 } 1406 } catch (IOException e) { 1407 throw new ICUUncheckedIOException(e); // damn'ed checked exceptions 1408 } 1409 } 1410 1411 /** 1412 * 1413 * @param output 1414 * @param choices 1415 * See the class description for more information. 1416 * @param localeId 1417 * @param user 1418 * @param usersLevel 1419 */ getErrorOnPath(EnumSet<Choice> choices, String localeID, T user, Level usersLevel, String path)1420 public ArrayList<String> getErrorOnPath(EnumSet<Choice> choices, String localeID, T user, 1421 Level usersLevel, String path) { 1422 1423 // Gather the relevant paths 1424 // each one will be marked with the choice that it triggered. 1425 Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of( 1426 new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class); 1427 1428 CLDRFile sourceFile = cldrFactory.make(localeID, true); 1429 1430 // Initialize 1431 CLDRFile baselineFile = null; 1432 try { 1433 Factory baselineFactory = CLDRConfig.getInstance().getCommonAndSeedAndMainAndAnnotationsFactory(); 1434 baselineFile = baselineFactory.make(localeID, true); 1435 } catch (Exception e) { 1436 } 1437 1438 EnumSet<Choice> errors = new FileInfo().getFileInfo(sourceFile, baselineFile, sorted, choices, localeID, user, usersLevel, 1439 false, path).problems; 1440 1441 ArrayList<String> out = new ArrayList<>(); 1442 for (Object error : errors.toArray()) { 1443 out.add(((Choice) error).buttonLabel); 1444 } 1445 1446 return out; 1447 } 1448 startTable(Set<Choice> choices, Appendable output)1449 private void startTable(Set<Choice> choices, Appendable output) throws IOException { 1450 output.append("<table class='tv-table'>\n"); 1451 output.append("<tr class='"); 1452 Choice.appendRowStyles(choices, output); 1453 output.append("'>" + 1454 "<th class='tv-th'>No.</th>" + 1455 "<th class='tv-th'>Code</th>" + 1456 "<th class='tv-th'>English</th>" + 1457 "<th class='tv-th'>" + baselineTitle + "</th>" + 1458 "<th class='tv-th'>" + currentWinningTitle + "</th>" + 1459 "<th class='tv-th'>Fix?</th>" + 1460 "<th class='tv-th'>Comment</th>" + 1461 "</tr>\n"); 1462 } 1463 1464 enum HTMLType { 1465 plain, markup 1466 } 1467 addCell(Appendable output, String value, String title, String classValue, HTMLType htmlType)1468 private void addCell(Appendable output, String value, String title, String classValue, HTMLType htmlType) 1469 throws IOException { 1470 output.append(" <td class='") 1471 .append(classValue); 1472 if (value == null) { 1473 output.append(" tv-null'><i>missing</i></td>"); 1474 } else { 1475 if (title != null && !title.equals(value)) { 1476 output.append("title='").append(TransliteratorUtilities.toHTML.transform(title)).append('\''); 1477 } 1478 output 1479 .append("'>") 1480 .append(htmlType == HTMLType.markup ? value : TransliteratorUtilities.toHTML.transform(value)) 1481 .append("</td>\n"); 1482 } 1483 } 1484 1485 /** 1486 * Find the status of the items in the file. 1487 * @param file the source. Must be a resolved file, made with minimalDraftStatus = unconfirmed 1488 * @param pathHeaderFactory PathHeaderFactory. 1489 * @param foundCounter output counter of the number of paths with values having contributed or approved status 1490 * @param unconfirmedCounter output counter of the number of paths with values, but neither contributed nor approved status 1491 * @param missingCounter output counter of the number of paths without values 1492 * @param missingPaths output if not null, the specific paths that are missing. 1493 * @param unconfirmedPaths TODO 1494 */ getStatus(CLDRFile file, PathHeader.Factory pathHeaderFactory, Counter<Level> foundCounter, Counter<Level> unconfirmedCounter, Counter<Level> missingCounter, Relation<MissingStatus, String> missingPaths, Set<String> unconfirmedPaths)1495 public static void getStatus(CLDRFile file, PathHeader.Factory pathHeaderFactory, 1496 Counter<Level> foundCounter, Counter<Level> unconfirmedCounter, 1497 Counter<Level> missingCounter, 1498 Relation<MissingStatus, String> missingPaths, 1499 Set<String> unconfirmedPaths) { 1500 getStatus(file.fullIterable(), file, pathHeaderFactory, foundCounter, unconfirmedCounter, missingCounter, missingPaths, unconfirmedPaths); 1501 } 1502 1503 /** 1504 * Find the status of the items in the file. 1505 * @param allPaths manual list of paths 1506 * @param file the source. Must be a resolved file, made with minimalDraftStatus = unconfirmed 1507 * @param pathHeaderFactory PathHeaderFactory. 1508 * @param foundCounter output counter of the number of paths with values having contributed or approved status 1509 * @param unconfirmedCounter output counter of the number of paths with values, but neither contributed nor approved status 1510 * @param missingCounter output counter of the number of paths without values 1511 * @param missingPaths output if not null, the specific paths that are missing. 1512 * @param unconfirmedPaths TODO 1513 */ getStatus(Iterable<String> allPaths, CLDRFile file, PathHeader.Factory pathHeaderFactory, Counter<Level> foundCounter, Counter<Level> unconfirmedCounter, Counter<Level> missingCounter, Relation<MissingStatus, String> missingPaths, Set<String> unconfirmedPaths)1514 public static void getStatus(Iterable<String> allPaths, CLDRFile file, 1515 PathHeader.Factory pathHeaderFactory, Counter<Level> foundCounter, 1516 Counter<Level> unconfirmedCounter, 1517 Counter<Level> missingCounter, 1518 Relation<MissingStatus, String> missingPaths, Set<String> unconfirmedPaths) { 1519 1520 if (!file.isResolved()) { 1521 throw new IllegalArgumentException("File must be resolved, no minimal draft status"); 1522 } 1523 foundCounter.clear(); 1524 unconfirmedCounter.clear(); 1525 missingCounter.clear(); 1526 1527 boolean latin = VettingViewer.isLatinScriptLocale(file); 1528 CoverageLevel2 coverageLevel2 = CoverageLevel2.getInstance(SupplementalDataInfo.getInstance(), file.getLocaleID()); 1529 1530 for (String path : allPaths) { 1531 1532 PathHeader ph = pathHeaderFactory.fromPath(path); 1533 if (ph.getSectionId() == SectionId.Special) { 1534 continue; 1535 } 1536 1537 Level level = coverageLevel2.getLevel(path); 1538 // String localeFound = file.getSourceLocaleID(path, status); 1539 // String value = file.getSourceLocaleID(path, status); 1540 MissingStatus missingStatus = VettingViewer.getMissingStatus(file, path, latin); 1541 1542 switch (missingStatus) { 1543 case ABSENT: 1544 missingCounter.add(level, 1); 1545 if (missingPaths != null && level.compareTo(Level.MODERN) <= 0) { 1546 missingPaths.put(missingStatus, path); 1547 } 1548 break; 1549 case ALIASED: 1550 case PRESENT: 1551 String fullPath = file.getFullXPath(path); 1552 if (fullPath.contains("unconfirmed") 1553 || fullPath.contains("provisional")) { 1554 unconfirmedCounter.add(level, 1); 1555 if (unconfirmedPaths != null && level.compareTo(Level.MODERN) <= 0) { 1556 unconfirmedPaths.add(path); 1557 } 1558 } else { 1559 foundCounter.add(level, 1); 1560 } 1561 break; 1562 case MISSING_OK: 1563 case ROOT_OK: 1564 break; 1565 default: 1566 throw new IllegalArgumentException(); 1567 } 1568 } 1569 } 1570 1571 /** 1572 * Simple example of usage 1573 * 1574 * @param args 1575 * @throws IOException 1576 */ 1577 final static Options myOptions = new Options(); 1578 1579 enum MyOptions { 1580 repeat(null, null, "Repeat indefinitely"), 1581 filter(".*", ".*", "Filter files"), 1582 locale(".*", "af", "Single locale for testing"), 1583 source(".*", CLDRPaths.MAIN_DIRECTORY + "," + CLDRPaths.ANNOTATIONS_DIRECTORY, // CldrUtility.TMP2_DIRECTORY + "/vxml/common/main" 1584 "if summary, creates filtered version (eg -d main): does a find in the name, which is of the form dir/file"), 1585 verbose(null, null, "verbose debugging messages"), 1586 output(".*", CLDRPaths.GEN_DIRECTORY + "vetting/", "filter the raw files (non-summary, mostly for debugging)"),; 1587 // boilerplate 1588 final Option option; 1589 MyOptions(String argumentPattern, String defaultArgument, String helpText)1590 MyOptions(String argumentPattern, String defaultArgument, String helpText) { 1591 option = myOptions.add(this, argumentPattern, defaultArgument, helpText); 1592 } 1593 } 1594 main(String[] args)1595 public static void main(String[] args) throws IOException { 1596 SHOW_SUBTYPES = true; 1597 myOptions.parse(MyOptions.source, args, true); 1598 boolean repeat = MyOptions.repeat.option.doesOccur(); 1599 String fileFilter = MyOptions.filter.option.getValue(); 1600 String myOutputDir = repeat ? null : MyOptions.output.option.getValue(); 1601 String LOCALE = MyOptions.locale.option.getValue(); 1602 1603 String[] DIRECTORIES = MyOptions.source.option.getValue().split(",\\s*"); 1604 final File[] fileDirectories = new File[DIRECTORIES.length]; 1605 int i = 0; 1606 for (String s : DIRECTORIES) { 1607 fileDirectories[i++] = new File(s); 1608 } 1609 1610 do { 1611 Timer timer = new Timer(); 1612 timer.start(); 1613 1614 Factory cldrFactory = SimpleFactory.make(fileDirectories, fileFilter); 1615 cldrFactory.setSupplementalDirectory(new File(CLDRPaths.SUPPLEMENTAL_DIRECTORY)); 1616 SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo 1617 .getInstance(CLDRPaths.SUPPLEMENTAL_DIRECTORY); 1618 CheckCLDR.setDisplayInformation(cldrFactory.make("en", true)); 1619 1620 // FAKE this, because we don't have access to ST data 1621 1622 UsersChoice<Organization> usersChoice = new UsersChoice<Organization>() { 1623 // Fake values for now 1624 @Override 1625 @SuppressWarnings("unused") 1626 public String getWinningValueForUsersOrganization(CLDRFile cldrFile, String path, Organization user) { 1627 if (path.contains("USD")) { 1628 return "&dummy ‘losing’ value"; 1629 } 1630 return null; // assume we didn't vote on anything else. 1631 } 1632 1633 // Fake values for now 1634 @Override 1635 public VoteStatus getStatusForUsersOrganization(CLDRFile cldrFile, String path, Organization user) { 1636 String usersValue = getWinningValueForUsersOrganization(cldrFile, path, user); 1637 String winningValue = cldrFile.getWinningValue(path); 1638 if (usersValue != null && !Objects.equals(usersValue, winningValue)) { 1639 return VoteStatus.losing; 1640 } 1641 String fullPath = cldrFile.getFullXPath(path); 1642 if (fullPath.contains("AMD") || fullPath.contains("unconfirmed") || fullPath.contains("provisional")) { 1643 return VoteStatus.provisionalOrWorse; 1644 } else if (fullPath.contains("AED")) { 1645 return VoteStatus.disputed; 1646 } else if (fullPath.contains("AED")) { 1647 return VoteStatus.ok_novotes; 1648 } 1649 return VoteStatus.ok; 1650 } 1651 }; 1652 1653 // create the tableView and set the options desired. 1654 // The Options should come from a GUI; from each you can get a long 1655 // description and a button label. 1656 // Assuming user can be identified by an int 1657 VettingViewer<Organization> tableView = new VettingViewer<>(supplementalDataInfo, cldrFactory, 1658 usersChoice, "Winning Proposed"); 1659 1660 // here are per-view parameters 1661 1662 final EnumSet<Choice> choiceSet = EnumSet.allOf(Choice.class); 1663 String localeStringID = LOCALE; 1664 int userNumericID = 666; 1665 Level usersLevel = Level.MODERN; 1666 // http: // unicode.org/cldr-apps/survey?_=ur 1667 1668 if (!repeat) { 1669 FileCopier.ensureDirectoryExists(myOutputDir); 1670 FileCopier.copy(VettingViewer.class, "vettingView.css", myOutputDir); 1671 FileCopier.copy(VettingViewer.class, "vettingView.js", myOutputDir); 1672 } 1673 System.out.println("Creation: " + timer.getDuration() / NANOSECS + " secs"); 1674 1675 timer.start(); 1676 writeFile(myOutputDir, tableView, choiceSet, "", localeStringID, userNumericID, usersLevel, CodeChoice.newCode, null); 1677 System.out.println("Code: " + timer.getDuration() / NANOSECS + " secs"); 1678 1679 timer.start(); 1680 writeFile(myOutputDir, tableView, choiceSet, "", localeStringID, userNumericID, usersLevel, CodeChoice.summary, 1681 Organization.google); 1682 System.out.println("Summary: " + timer.getDuration() / NANOSECS + " secs"); 1683 } while (repeat); 1684 } 1685 1686 private enum CodeChoice { 1687 /** For the normal (locale) view of data **/ 1688 newCode, 1689 1690 /** For a summary view of data **/ 1691 summary 1692 } 1693 1694 /** 1695 * 1696 * @param myOutputDir 1697 * @param tableView 1698 * @param choiceSet 1699 * @param name 1700 * @param localeStringID 1701 * @param userNumericID 1702 * @param usersLevel 1703 * @param newCode 1704 * @param organization 1705 * @throws IOException 1706 * 1707 * Called only by VettingViewer.main 1708 */ writeFile(String myOutputDir, VettingViewer<Organization> tableView, final EnumSet<Choice> choiceSet, String name, String localeStringID, int userNumericID, Level usersLevel, CodeChoice newCode, Organization organization)1709 public static void writeFile(String myOutputDir, VettingViewer<Organization> tableView, final EnumSet<Choice> choiceSet, 1710 String name, String localeStringID, int userNumericID, 1711 Level usersLevel, 1712 CodeChoice newCode, Organization organization) 1713 throws IOException { 1714 // open up a file, and output some of the styles to control the table 1715 // appearance 1716 PrintWriter out = myOutputDir == null ? new PrintWriter(new StringWriter()) 1717 : FileUtilities.openUTF8Writer(myOutputDir, "vettingView" 1718 + name 1719 + (newCode == CodeChoice.newCode ? "" : newCode == CodeChoice.summary ? "-summary" : "") 1720 + (organization == null ? "" : "-" + organization.toString()) 1721 + ".html"); 1722 // FileUtilities.appendFile(VettingViewer.class, "vettingViewerHead.txt", out); 1723 FileCopier.copy(VettingViewer.class, "vettingViewerHead.txt", out); 1724 out.append(getHeaderStyles()); 1725 out.append("</head><body>\n"); 1726 1727 out.println( 1728 "<p>Note: this is just a sample run. The user, locale, user's coverage level, and choices of tests will change the output. In a real ST page using these, the first three would " 1729 + "come from context, and the choices of tests would be set with radio buttons. Demo settings are: </p>\n<ol>" 1730 + "<li>choices: " 1731 + choiceSet 1732 + "</li><li>localeStringID: " 1733 + localeStringID 1734 + "</li><li>userNumericID: " 1735 + userNumericID 1736 + "</li><li>usersLevel: " 1737 + usersLevel 1738 + "</ol>" 1739 + "<p>Notes: This is a static version, using old values and faked values (L) just for testing." 1740 + (TESTING ? "Also, the white cell after the Fix column is just for testing." : "") 1741 + "</p><hr>\n"); 1742 1743 // now generate the table with the desired options 1744 // The options should come from a GUI; from each you can get a long 1745 // description and a button label. 1746 // Assuming user can be identified by an int 1747 1748 switch (newCode) { 1749 case newCode: 1750 tableView.generateHtmlErrorTables(out, choiceSet, localeStringID, organization, usersLevel, false); 1751 break; 1752 case summary: 1753 //System.out.println(tableView.getName("zh_Hant_HK")); 1754 tableView.generateSummaryHtmlErrorTables(out, choiceSet, organization); 1755 break; 1756 } 1757 out.println("</body>\n</html>\n"); 1758 out.close(); 1759 } 1760 } 1761