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