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