• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = "&lt;" + subtype + "&gt; " + 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