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