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