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