blob: acbbbca90bd5cf6970bb775b0aa66ca92709915b [file] [log] [blame]
package org.unicode.cldr.util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import org.unicode.cldr.test.CheckCLDR;
import org.unicode.cldr.test.CheckCLDR.CheckStatus;
import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
import org.unicode.cldr.test.CheckCLDR.Options;
import org.unicode.cldr.test.CheckCoverage;
import org.unicode.cldr.test.CheckNew;
import org.unicode.cldr.test.CoverageLevel2;
import org.unicode.cldr.test.OutdatedPaths;
import org.unicode.cldr.test.SubmissionLocales;
import org.unicode.cldr.util.CLDRFile.Status;
import org.unicode.cldr.util.PathHeader.PageId;
import org.unicode.cldr.util.PathHeader.SectionId;
import org.unicode.cldr.util.StandardCodes.LocaleCoverageType;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R2;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;
/**
* Provides data for the Dashboard, showing the important issues for vetters to review for
* a given locale.
*
* Also provides the Priority Items Summary, which is like a Dashboard that combines multiple locales.
*
* @author markdavis
*/
public class VettingViewer<T> {
private static final boolean DEBUG = false;
private static final boolean SHOW_SUBTYPES = false;
private static final String CONNECT_PREFIX = "₍_";
private static final String CONNECT_SUFFIX = "₎";
private static final String TH_AND_STYLES = "<th class='tv-th' style='text-align:left'>";
private static final String SPLIT_CHAR = "\uFFFE";
private static final boolean DEBUG_THREADS = false;
private static final Set<CheckCLDR.CheckStatus.Subtype> OK_IF_VOTED = EnumSet.of(Subtype.sameAsEnglish);
public static Organization getNeutralOrgForSummary() {
return Organization.surveytool;
}
private static boolean orgIsNeutralForSummary(Organization org) {
return org.equals(getNeutralOrgForSummary());
}
private LocaleBaselineCount localeBaselineCount = null;
public void setLocaleBaselineCount(LocaleBaselineCount localeBaselineCount) {
this.localeBaselineCount = localeBaselineCount;
}
public static OutdatedPaths getOutdatedPaths() {
return outdatedPaths;
}
private static PathHeader.Factory pathTransform;
private static final OutdatedPaths outdatedPaths = new OutdatedPaths();
/**
* See VoteResolver getStatusForOrganization to see how this is computed.
*/
public enum VoteStatus {
/**
* The value for the path is either contributed or approved, and
* the user's organization didn't vote. (see class def for null user)
*/
ok_novotes,
/**
* The value for the path is either contributed or approved, and
* the user's organization chose the winning value. (see class def for null user)
*/
ok,
/**
* The user's organization chose the winning value for the path, but
* that value is neither contributed nor approved. (see class def for null user)
*/
provisionalOrWorse,
/**
* The user's organization's choice is not winning. There may be
* insufficient votes to overcome a previously approved value, or other
* organizations may be voting against it. (see class def for null user)
*/
losing,
/**
* There is a dispute, meaning more than one item with votes, or the item with votes didn't win.
*/
disputed
}
/**
* @author markdavis
*
* @param <T>
*/
public interface UsersChoice<T> {
/**
* Return the value that the user's organization (as a whole) voted for,
* or null if none of the users in the organization voted for the path. <br>
* NOTE: Would be easier if this were a method on CLDRFile.
* NOTE: if organization = null, then it must return the absolute winning value.
*/
String getWinningValueForUsersOrganization(CLDRFile cldrFile, String path, T organization);
/**
* Return the vote status
* NOTE: if organization = null, then it must disregard the organization and never return losing. See VoteStatus.
*/
VoteStatus getStatusForUsersOrganization(CLDRFile cldrFile, String path, T organization);
/**
* Has the given user voted for the given path and locale?
* @param userId
* @param loc
* @param path
* @return true if that user has voted, else false
*/
boolean userDidVote(int userId, CLDRLocale loc, String path);
VoteResolver<String> getVoteResolver(CLDRFile baselineFile, CLDRLocale loc, String path);
}
public interface ErrorChecker {
enum Status {
ok, error, warning
}
/**
* Initialize an error checker with a cldrFile. MUST be called before
* any getErrorStatus.
*/
Status initErrorStatus(CLDRFile cldrFile);
/**
* Return the detailed CheckStatus information.
*/
List<CheckStatus> getErrorCheckStatus(String path, String value);
/**
* Return the status, and append the error message to the status
* message. If there are any errors, then the warnings are not included.
*/
Status getErrorStatus(String path, String value, StringBuilder statusMessage);
/**
* Return the status, and append the error message to the status
* message, and get the subtypes. If there are any errors, then the warnings are not included.
*/
Status getErrorStatus(String path, String value, StringBuilder statusMessage,
EnumSet<Subtype> outputSubtypes);
}
private static class DefaultErrorStatus implements ErrorChecker {
private CheckCLDR checkCldr;
private HashMap<String, String> options = new HashMap<>();
private ArrayList<CheckStatus> result = new ArrayList<>();
private CLDRFile cldrFile;
private final Factory factory;
private DefaultErrorStatus(Factory cldrFactory) {
this.factory = cldrFactory;
}
@Override
public Status initErrorStatus(CLDRFile cldrFile) {
this.cldrFile = cldrFile;
options = new HashMap<>();
result = new ArrayList<>();
checkCldr = CheckCLDR.getCheckAll(factory, ".*");
checkCldr.setCldrFileToCheck(cldrFile, new Options(options), result);
return Status.ok;
}
@Override
public List<CheckStatus> getErrorCheckStatus(String path, String value) {
String fullPath = cldrFile.getFullXPath(path);
ArrayList<CheckStatus> result2 = new ArrayList<>();
checkCldr.check(path, fullPath, value, new CheckCLDR.Options(options), result2);
return result2;
}
@Override
public Status getErrorStatus(String path, String value, StringBuilder statusMessage) {
return getErrorStatus(path, value, statusMessage, null);
}
@Override
public Status getErrorStatus(String path, String value, StringBuilder statusMessage,
EnumSet<Subtype> outputSubtypes) {
Status result0 = Status.ok;
StringBuilder errorMessage = new StringBuilder();
String fullPath = cldrFile.getFullXPath(path);
checkCldr.check(path, fullPath, value, new CheckCLDR.Options(options), result);
for (CheckStatus checkStatus : result) {
final CheckCLDR cause = checkStatus.getCause();
/*
* CheckCoverage will be shown under Missing, not under Warnings; and
* CheckNew will be shown under New, not under Warnings; so skip them here.
*/
if (cause instanceof CheckCoverage || cause instanceof CheckNew) {
continue;
}
CheckStatus.Type statusType = checkStatus.getType();
if (statusType.equals(CheckStatus.errorType)) {
// throw away any accumulated warning messages
if (result0 == Status.warning) {
errorMessage.setLength(0);
if (outputSubtypes != null) {
outputSubtypes.clear();
}
}
result0 = Status.error;
if (outputSubtypes != null) {
outputSubtypes.add(checkStatus.getSubtype());
}
appendToMessage(checkStatus.getMessage(), checkStatus.getSubtype(), errorMessage);
} else if (result0 != Status.error && statusType.equals(CheckStatus.warningType)) {
result0 = Status.warning;
// accumulate all the warning messages
if (outputSubtypes != null) {
outputSubtypes.add(checkStatus.getSubtype());
}
appendToMessage(checkStatus.getMessage(), checkStatus.getSubtype(), errorMessage);
}
}
if (result0 != Status.ok) {
appendToMessage(errorMessage, statusMessage);
}
return result0;
}
}
private final Factory cldrFactory;
private final CLDRFile englishFile;
private final UsersChoice<T> userVoteStatus;
private final SupplementalDataInfo supplementalDataInfo;
private final Set<String> defaultContentLocales;
/**
* Create the Vetting Viewer.
*
* @param supplementalDataInfo
* @param cldrFactory
* @param userVoteStatus
*/
public VettingViewer(SupplementalDataInfo supplementalDataInfo, Factory cldrFactory,
UsersChoice<T> userVoteStatus) {
super();
this.cldrFactory = cldrFactory;
englishFile = cldrFactory.make("en", true);
if (pathTransform == null) {
pathTransform = PathHeader.getFactory(englishFile);
}
this.userVoteStatus = userVoteStatus;
this.supplementalDataInfo = supplementalDataInfo;
this.defaultContentLocales = supplementalDataInfo.getDefaultContentLocales();
reasonsToPaths = Relation.of(new HashMap<>(), HashSet.class);
}
public class WritingInfo implements Comparable<WritingInfo> {
public final PathHeader codeOutput;
public final Set<NotificationCategory> problems;
public final String htmlMessage;
public final Subtype subtype;
public WritingInfo(PathHeader ph, EnumSet<NotificationCategory> problems, CharSequence htmlMessage, Subtype subtype) {
super();
this.codeOutput = ph;
this.problems = Collections.unmodifiableSet(problems.clone());
this.htmlMessage = htmlMessage.toString();
this.subtype = subtype;
}
@Override
public int compareTo(WritingInfo other) {
return codeOutput.compareTo(other.codeOutput);
}
}
public class DashboardData {
public Relation<R2<SectionId, PageId>, WritingInfo> sorted = Relation.of(
new TreeMap<R2<SectionId, PageId>, Set<WritingInfo>>(), TreeSet.class);
public VoterProgress voterProgress = new VoterProgress();
}
/**
* Generate the Dashboard
*
* @param args the DashboardArgs
* @return the DashboardData
*/
public DashboardData generateDashboard(VettingParameters args) {
DashboardData dd = new DashboardData();
FileInfo fileInfo = new FileInfo(args.locale.getBaseName(), args.coverageLevel, args.choices, (T) args.organization);
if (args.specificSinglePath != null) {
fileInfo.setSinglePath(args.specificSinglePath);
}
fileInfo.setFiles(args.sourceFile, args.baselineFile);
fileInfo.setSorted(dd.sorted);
fileInfo.setVoterProgressAndId(dd.voterProgress, args.userId);
fileInfo.getFileInfo();
return dd;
}
public LocaleCompletionData generateLocaleCompletion(VettingParameters args) {
if (!args.sourceFile.isResolved()) {
throw new IllegalArgumentException("File must be resolved for locale completion");
}
FileInfo fileInfo = new FileInfo(args.locale.getBaseName(), args.coverageLevel, args.choices, (T) args.organization);
fileInfo.setFiles(args.sourceFile, args.baselineFile);
fileInfo.getFileInfo();
return new LocaleCompletionData(fileInfo.vc.problemCounter);
}
private class VettingCounters {
private final Counter<NotificationCategory> problemCounter = new Counter<>();
private final Counter<Subtype> errorSubtypeCounter = new Counter<>();
private final Counter<Subtype> warningSubtypeCounter = new Counter<>();
/**
* Combine some statistics into this VettingCounters from another VettingCounters
*
* This is used by Priority Items Summary to combine stats from multiple locales.
*
* @param other the other VettingCounters object (for a single locale)
*/
private void addAll(VettingCounters other) {
problemCounter.addAll(other.problemCounter);
errorSubtypeCounter.addAll(other.errorSubtypeCounter);
warningSubtypeCounter.addAll(other.warningSubtypeCounter);
}
}
/**
* A FileInfo contains parameters, results, and methods for gathering information about a locale
*/
private class FileInfo {
private final String localeId;
private final CLDRLocale cldrLocale;
private final Level usersLevel;
private final EnumSet<NotificationCategory> choices;
private final T organization;
private FileInfo(String localeId, Level level, EnumSet<NotificationCategory> choices, T organization) {
this.localeId = localeId;
this.cldrLocale = CLDRLocale.getInstance(localeId);
this.usersLevel = level;
this.choices = choices;
this.organization = organization;
}
private CLDRFile sourceFile = null;
private CLDRFile baselineFile = null;
private CLDRFile baselineFileUnresolved = null;
private boolean latin = false;
private void setFiles(CLDRFile sourceFile, CLDRFile baselineFile) {
this.sourceFile = sourceFile;
this.baselineFile = baselineFile;
this.baselineFileUnresolved = (baselineFile == null) ? null : baselineFile.getUnresolved();
this.latin = VettingViewer.isLatinScriptLocale(sourceFile);
}
/**
* If not null, this object gets filled in with additional information
*/
private Relation<R2<SectionId, PageId>, WritingInfo> sorted = null;
private void setSorted(Relation<R2<SectionId, PageId>, VettingViewer<T>.WritingInfo> sorted) {
this.sorted = sorted;
}
/**
* If voterId > 0, calculate voterProgress for the indicated user.
*/
private int voterId = 0;
private VoterProgress voterProgress = null;
private void setVoterProgressAndId(VoterProgress voterProgress, int userId) {
this.voterProgress = voterProgress;
this.voterId = userId;
}
private final VettingCounters vc = new VettingCounters();
private final EnumSet<NotificationCategory> problems = EnumSet.noneOf(NotificationCategory.class);
private final StringBuilder htmlMessage = new StringBuilder();
private final StringBuilder statusMessage = new StringBuilder();
private final EnumSet<Subtype> subtypes = EnumSet.noneOf(Subtype.class);
private final DefaultErrorStatus errorChecker = new DefaultErrorStatus(cldrFactory);
/**
* If not null, getFileInfo will skip all paths except this one
*/
private String specificSinglePath = null;
private void setSinglePath(String path) {
this.specificSinglePath = path;
}
/**
* Loop through paths for the Dashboard or the Priority Items Summary
*
* @return the FileInfo
*/
private void getFileInfo() {
if (progressCallback.isStopped()) {
throw new RuntimeException("Requested to stop");
}
errorChecker.initErrorStatus(sourceFile);
if (specificSinglePath != null) {
handleOnePath(specificSinglePath);
return;
}
Set<String> seenSoFar = new HashSet<>();
for (String path : sourceFile.fullIterable()) {
if (seenSoFar.contains(path)) {
continue;
}
seenSoFar.add(path);
progressCallback.nudge(); // Let the user know we're moving along
handleOnePath(path);
}
}
private void handleOnePath(String path) {
PathHeader ph = pathTransform.fromPath(path);
if (ph == null || ph.shouldHide()) {
return;
}
String value = sourceFile.getWinningValue(path);
statusMessage.setLength(0);
subtypes.clear();
ErrorChecker.Status errorStatus = errorChecker.getErrorStatus(path, value, statusMessage, subtypes);
// note that the value might be missing!
Level pathLevel = supplementalDataInfo.getCoverageLevel(path, localeId);
// skip all but errors above the requested level
boolean pathLevelIsTooHigh = pathLevel.compareTo(usersLevel) > 0;
boolean onlyRecordErrors = pathLevelIsTooHigh;
problems.clear();
htmlMessage.setLength(0);
final String oldValue = (baselineFileUnresolved == null) ? null : baselineFileUnresolved.getWinningValue(path);
if (skipForLimitedSubmission(path, errorStatus, oldValue)) {
return;
}
if (!onlyRecordErrors && choices.contains(NotificationCategory.changedOldValue)) {
if (oldValue != null && !oldValue.equals(value)) {
problems.add(NotificationCategory.changedOldValue);
vc.problemCounter.increment(NotificationCategory.changedOldValue);
}
}
VoteStatus voteStatus = userVoteStatus.getStatusForUsersOrganization(sourceFile, path, organization);
boolean itemsOkIfVoted = (voteStatus == VoteStatus.ok);
MissingStatus missingStatus = onlyRecordErrors ? null : recordMissingChangedEtc(path, itemsOkIfVoted, value, oldValue);
recordChoice(errorStatus, itemsOkIfVoted, onlyRecordErrors);
if (!onlyRecordErrors) {
recordLosingDisputedEtc(path, voteStatus, missingStatus);
}
if (pathLevelIsTooHigh && problems.isEmpty()) {
return;
}
updateVotedOrAbstained(path);
if (!problems.isEmpty() && sorted != null) {
reasonsToPaths.clear();
R2<SectionId, PageId> group = Row.of(ph.getSectionId(), ph.getPageId());
sorted.put(group, new WritingInfo(ph, problems, htmlMessage, firstSubtype()));
}
}
private Subtype firstSubtype() {
for (Subtype subtype : subtypes) {
if (subtype != Subtype.none) {
return subtype;
}
}
return Subtype.none;
}
private void updateVotedOrAbstained(String path) {
if (voterProgress == null || voterId == 0) {
return;
}
voterProgress.incrementVotablePathCount();
if (userVoteStatus.userDidVote(voterId, cldrLocale, path)) {
voterProgress.incrementVotedPathCount();
} else if (choices.contains(NotificationCategory.abstained)) {
problems.add(NotificationCategory.abstained);
vc.problemCounter.increment(NotificationCategory.abstained);
}
}
private boolean skipForLimitedSubmission(String path, ErrorChecker.Status errorStatus, String oldValue) {
if (CheckCLDR.LIMITED_SUBMISSION) {
boolean isError = (errorStatus == ErrorChecker.Status.error);
boolean isMissing = (oldValue == null);
if (!SubmissionLocales.allowEvenIfLimited(localeId, path, isError, isMissing)) {
return true;
}
}
return false;
}
private MissingStatus recordMissingChangedEtc(String path,
boolean itemsOkIfVoted, String value, String oldValue) {
VoteResolver<String> resolver = userVoteStatus.getVoteResolver(baselineFile, cldrLocale, path);
MissingStatus missingStatus;
if (resolver.getWinningStatus() == VoteResolver.Status.missing) {
missingStatus = getMissingStatus(sourceFile, path, latin);
} else {
missingStatus = MissingStatus.PRESENT;
}
if (choices.contains(NotificationCategory.missingCoverage) && missingStatus == MissingStatus.ABSENT) {
problems.add(NotificationCategory.missingCoverage);
vc.problemCounter.increment(NotificationCategory.missingCoverage);
}
if (!CheckCLDR.LIMITED_SUBMISSION
&& !itemsOkIfVoted && outdatedPaths.isOutdated(localeId, path)) {
recordEnglishChanged(path, value, oldValue);
}
return missingStatus;
}
private void recordEnglishChanged(String path, String value, String oldValue) {
if (Objects.equals(value, oldValue) && choices.contains(NotificationCategory.englishChanged)) {
String oldEnglishValue = outdatedPaths.getPreviousEnglish(path);
if (!OutdatedPaths.NO_VALUE.equals(oldEnglishValue)) {
// check to see if we voted
problems.add(NotificationCategory.englishChanged);
vc.problemCounter.increment(NotificationCategory.englishChanged);
}
}
}
private void recordChoice(ErrorChecker.Status errorStatus, boolean itemsOkIfVoted, boolean onlyRecordErrors) {
NotificationCategory choice = errorStatus == ErrorChecker.Status.error ? NotificationCategory.error
: errorStatus == ErrorChecker.Status.warning ? NotificationCategory.warning
: null;
if (choice == NotificationCategory.error && choices.contains(NotificationCategory.error)
&& (!itemsOkIfVoted
|| !OK_IF_VOTED.containsAll(subtypes))) {
problems.add(choice);
appendToMessage(statusMessage, htmlMessage);
vc.problemCounter.increment(choice);
for (Subtype subtype : subtypes) {
vc.errorSubtypeCounter.increment(subtype);
}
} else if (!onlyRecordErrors && choice == NotificationCategory.warning && choices.contains(NotificationCategory.warning)
&& (!itemsOkIfVoted
|| !OK_IF_VOTED.containsAll(subtypes))) {
problems.add(choice);
appendToMessage(statusMessage, htmlMessage);
vc.problemCounter.increment(choice);
for (Subtype subtype : subtypes) {
vc.warningSubtypeCounter.increment(subtype);
}
}
}
private void recordLosingDisputedEtc(String path, VoteStatus voteStatus, MissingStatus missingStatus) {
switch (voteStatus) {
case losing:
if (choices.contains(NotificationCategory.weLost)) {
problems.add(NotificationCategory.weLost);
vc.problemCounter.increment(NotificationCategory.weLost);
}
String usersValue = userVoteStatus.getWinningValueForUsersOrganization(sourceFile, path, organization);
if (usersValue != null) {
usersValue = "Losing value: <" + TransliteratorUtilities.toHTML.transform(usersValue) + ">";
appendToMessage(usersValue, htmlMessage);
}
break;
case disputed:
if (choices.contains(NotificationCategory.hasDispute)) {
problems.add(NotificationCategory.hasDispute);
vc.problemCounter.increment(NotificationCategory.hasDispute);
}
break;
case provisionalOrWorse:
if (missingStatus == MissingStatus.PRESENT && choices.contains(NotificationCategory.notApproved)) {
problems.add(NotificationCategory.notApproved);
vc.problemCounter.increment(NotificationCategory.notApproved);
}
break;
default:
}
}
}
public final class LocalesWithExplicitLevel implements Predicate<String> {
private final Organization org;
private final Level desiredLevel;
public LocalesWithExplicitLevel(Organization org, Level level) {
this.org = org;
this.desiredLevel = level;
}
@Override
public boolean is(String localeId) {
StandardCodes sc = StandardCodes.make();
if (orgIsNeutralForSummary(org)) {
if (!summarizeAllLocales && !SubmissionLocales.CLDR_LOCALES.contains(localeId)) {
return false;
}
return desiredLevel == sc.getTargetCoverageLevel(localeId);
} else {
Output<LocaleCoverageType> output = new Output<>();
Level level = sc.getLocaleCoverageLevel(org, localeId, output);
return desiredLevel == level && output.value == StandardCodes.LocaleCoverageType.explicit;
}
}
}
/**
* Get the number of locales to be summarized for the given organization
*
* @param org the organization
* @return the number of locales
*/
public int getLocaleCount(Organization org) {
int localeCount = 0;
for (Level lv : Level.values()) {
Map<String, String> sortedNames = getSortedNames(org, lv);
localeCount += sortedNames.size();
}
return localeCount;
}
/**
* Get the list of locales to be summarized for the given organization
*
* @param org the organization
* @return the list of locale id strings
*/
public ArrayList<String> getLocaleList(Organization org) {
final ArrayList<String> list = new ArrayList<>();
for (Level lv : Level.values()) {
final Map<String, String> sortedNames = getSortedNames(org, lv);
for (Map.Entry<String,String> entry : sortedNames.entrySet()) {
list.add(entry.getValue());
}
}
return list;
}
public void generatePriorityItemsSummary(Appendable output, EnumSet<NotificationCategory> choices, T organization) throws ExecutionException {
try {
StringBuilder headerRow = new StringBuilder();
headerRow
.append("<tr class='tvs-tr'>")
.append(TH_AND_STYLES)
.append("Level</th>")
.append(TH_AND_STYLES)
.append("Locale</th>")
.append(TH_AND_STYLES)
.append("Codes</th>")
.append(TH_AND_STYLES)
.append("Progress</th>");
for (NotificationCategory choice : choices) {
headerRow.append("<th class='tv-th'>");
appendDisplay(headerRow, choice);
headerRow.append("</th>");
}
headerRow.append("</tr>\n");
String header = headerRow.toString();
for (Level level : Level.values()) {
writeSummaryTable(output, header, level, choices, organization);
}
} catch (IOException e) {
throw new ICUUncheckedIOException(e); // dang'ed checked exceptions
}
}
private void appendDisplay(StringBuilder target, NotificationCategory category) throws IOException {
target.append("<span title='")
.append(category.description);
target.append("'>")
.append(category.buttonLabel)
.append("*</span>");
}
/**
* This is a context object for Vetting Viewer parallel writes.
* It keeps track of the input locales, other parameters, as well as the output
* streams.
*
* When done, appendTo() is called to append the output to the original requester.
* @author srl
*/
private class WriteContext {
private final List<String> localeNames = new ArrayList<>();
private final List<String> localeIds = new ArrayList<>();
private final StringBuffer[] outputs;
private final EnumSet<NotificationCategory> choices;
private final EnumSet<NotificationCategory> ourChoicesThatRequireOldFile;
private final T organization;
private final VettingViewer<T>.VettingCounters totals;
private final Map<String, VettingViewer<T>.FileInfo> localeNameToFileInfo;
private final String header;
private final int configChunkSize; // Number of locales to process at once, minimum 1
private WriteContext(Set<Entry<String, String>> entrySet, EnumSet<NotificationCategory> choices, T organization, VettingCounters totals,
Map<String, FileInfo> localeNameToFileInfo, String header) {
for (Entry<String, String> e : entrySet) {
localeNames.add(e.getKey());
localeIds.add(e.getValue());
}
int count = localeNames.size();
this.outputs = new StringBuffer[count];
for (int i = 0; i < count; i++) {
outputs[i] = new StringBuffer();
}
if (DEBUG_THREADS) {
System.out.println("Initted " + this.outputs.length + " outputs");
}
// other data
this.choices = choices;
EnumSet<NotificationCategory> thingsThatRequireOldFile = EnumSet.of(NotificationCategory.englishChanged, NotificationCategory.missingCoverage, NotificationCategory.changedOldValue);
ourChoicesThatRequireOldFile = choices.clone();
ourChoicesThatRequireOldFile.retainAll(thingsThatRequireOldFile);
this.organization = organization;
this.totals = totals;
this.localeNameToFileInfo = localeNameToFileInfo;
this.header = header;
if (DEBUG_THREADS) {
System.out.println("writeContext for " + organization.toString() + " booted with " + count + " locales");
}
// setup env
CLDRConfig config = CLDRConfig.getInstance();
// parallelism. 0 means "let Java decide"
int configParallel = Math.max(config.getProperty("CLDR_VETTINGVIEWER_PARALLEL", 0), 0);
if (configParallel < 1) {
configParallel = java.lang.Runtime.getRuntime().availableProcessors(); // matches ForkJoinPool() behavior
}
this.configChunkSize = Math.max(config.getProperty("CLDR_VETTINGVIEWER_CHUNKSIZE", 1), 1);
if (DEBUG) {
System.out.println("vv: CLDR_VETTINGVIEWER_PARALLEL=" + configParallel +
", CLDR_VETTINGVIEWER_CHUNKSIZE=" + configChunkSize);
}
}
/**
* Append all of the results (one stream per locale) to the output parameter.
* Insert the "header" as needed.
* @param output
* @throws IOException
*/
private void appendTo(Appendable output) throws IOException {
// all done, append all
char lastChar = ' ';
for (int n = 0; n < outputs.length; n++) {
final String name = localeNames.get(n);
if (DEBUG_THREADS) {
System.out.println("Appending " + name + " - " + outputs[n].length());
}
char nextChar = name.charAt(0);
if (lastChar != nextChar) {
output.append(this.header);
lastChar = nextChar;
}
output.append(outputs[n]);
}
}
/**
* How many locales are represented in this context?
* @return
*/
private int size() {
return localeNames.size();
}
}
/**
* Worker action to implement parallel Vetting Viewer writes.
* This takes a WriteContext as a parameter, as well as a subset of the locales
* to operate on.
*
* @author srl
*/
private class WriteAction extends RecursiveAction {
private final int length;
private final int start;
private final WriteContext context;
public WriteAction(WriteContext context) {
this(context, 0, context.size());
}
public WriteAction(WriteContext context, int start, int length) {
this.context = context;
this.start = start;
this.length = length;
if (DEBUG_THREADS) {
System.out.println("writeAction(…," + start + ", " + length + ") of " + context.size() +
" with outputCount:" + context.outputs.length);
}
}
private static final long serialVersionUID = 1L;
@Override
protected void compute() {
if (length == 0) {
return;
} else if (length <= context.configChunkSize) {
computeAll();
} else {
int split = length / 2;
// subdivide
invokeAll(new WriteAction(context, start, split),
new WriteAction(context, start + split, length - split));
}
}
/**
* Compute this entire task.
* Can call this to run this step as a single thread.
*/
private void computeAll() {
// do this many at once
for (int n = start; n < (start + length); n++) {
computeOne(n);
}
}
/**
* Calculate the Priority Items Summary output for one locale
* @param n
*/
private void computeOne(int n) {
if (progressCallback.isStopped()) {
throw new RuntimeException("Requested to stop");
}
if (DEBUG) {
MemoryHelper.availableMemory("VettingViewer.WriteAction.computeOne", true);
}
final String name = context.localeNames.get(n);
final String localeID = context.localeIds.get(n);
if (DEBUG_THREADS) {
System.out.println("writeAction.compute(" + n + ") - " + name + ": " + localeID);
}
EnumSet<NotificationCategory> choices = context.choices;
Appendable output = context.outputs[n];
if (output == null) {
throw new NullPointerException("output " + n + " null");
}
// Initialize
CLDRFile sourceFile = cldrFactory.make(localeID, true);
CLDRFile baselineFile = null;
if (!context.ourChoicesThatRequireOldFile.isEmpty()) {
try {
Factory baselineFactory = CLDRConfig.getInstance().getCommonAndSeedAndMainAndAnnotationsFactory();
baselineFile = baselineFactory.make(localeID, true);
} catch (Exception e) {
}
}
Level level = Level.MODERN;
if (context.organization != null) {
StandardCodes sc = StandardCodes.make();
if (orgIsNeutralForSummary((Organization) context.organization)) {
level = sc.getTargetCoverageLevel(localeID);
} else {
level = sc.getLocaleCoverageLevel(context.organization.toString(), localeID);
}
}
FileInfo fileInfo = new FileInfo(localeID, level, choices, context.organization);
fileInfo.setFiles(sourceFile, baselineFile);
fileInfo.getFileInfo();
context.localeNameToFileInfo.put(name, fileInfo);
context.totals.addAll(fileInfo.vc);
if (DEBUG_THREADS) {
System.out.println("writeAction.compute(" + n + ") - got fileinfo " + name + ": " + localeID);
}
try {
writeSummaryRow(output, choices, fileInfo.vc.problemCounter, name, localeID, level);
if (DEBUG_THREADS) {
System.out.println("writeAction.compute(" + n + ") - wrote " + name + ": " + localeID);
}
} catch (IOException | ExecutionException e) {
System.err.println("writeAction.compute(" + n + ") - writeexc " + name + ": " + localeID);
this.completeExceptionally(new RuntimeException("While writing " + localeID, e));
}
if (DEBUG) {
System.out.println("writeAction.compute(" + n + ") - DONE " + name + ": " + localeID);
}
}
}
/**
* Write the table for the Priority Items Summary
* @param output
* @param header
* @param desiredLevel
* @param choices
* @param organization
* @throws IOException
*/
private void writeSummaryTable(Appendable output, String header, Level desiredLevel,
EnumSet<NotificationCategory> choices, T organization) throws IOException, ExecutionException {
Map<String, String> sortedNames = getSortedNames((Organization) organization, desiredLevel);
if (sortedNames.isEmpty()) {
return;
}
output.append("<h2>Level: ").append(desiredLevel.toString()).append("</h2>");
output.append("<table class='tvs-table'>\n");
Map<String, FileInfo> localeNameToFileInfo = new TreeMap<>();
VettingCounters totals = new VettingCounters();
Set<Entry<String, String>> entrySet = sortedNames.entrySet();
WriteContext context = this.new WriteContext(entrySet, choices, organization, totals, localeNameToFileInfo, header);
WriteAction writeAction = this.new WriteAction(context);
if (USE_FORKJOIN) {
ForkJoinPool.commonPool().invoke(writeAction);
} else {
if (DEBUG) {
System.out.println("WARNING: calling writeAction.computeAll(), as the ForkJoinPool is disabled.");
}
writeAction.computeAll();
}
context.appendTo(output); // write all of the results together
output.append(header); // add one header at the bottom before the Total row
writeSummaryRow(output, choices, totals.problemCounter, "Total", null, desiredLevel);
output.append("</table>");
if (SHOW_SUBTYPES) {
showSubtypes(output, sortedNames, localeNameToFileInfo, totals, true);
showSubtypes(output, sortedNames, localeNameToFileInfo, totals, false);
}
}
private Map<String, String> getSortedNames(Organization org, Level desiredLevel) {
Map<String, String> sortedNames = new TreeMap<>(CLDRConfig.getInstance().getCollator());
// TODO Fix HACK
// We are going to ignore the predicate for now, just using the locales that have explicit coverage.
// in that locale, or allow all locales for admin@
LocalesWithExplicitLevel includeLocale = new LocalesWithExplicitLevel(org, desiredLevel);
for (String localeID : cldrFactory.getAvailable()) {
if (defaultContentLocales.contains(localeID)
|| localeID.equals("en")
|| !includeLocale.is(localeID)) {
continue;
}
sortedNames.put(getName(localeID), localeID);
}
return sortedNames;
}
private final boolean USE_FORKJOIN = false;
private void showSubtypes(Appendable output, Map<String, String> sortedNames,
Map<String, FileInfo> localeNameToFileInfo,
VettingCounters totals, boolean errors) throws IOException {
output.append("<h3>Details: ").append(errors ? "Error Types" : "Warning Types").append("</h3>");
output.append("<table class='tvs-table'>");
Counter<Subtype> subtypeCounterTotals = errors ? totals.errorSubtypeCounter : totals.warningSubtypeCounter;
Set<Subtype> sortedBySize = subtypeCounterTotals.getKeysetSortedByCount(false);
// header
writeDetailHeader(sortedBySize, output);
// items
for (Entry<String, FileInfo> entry : localeNameToFileInfo.entrySet()) {
Counter<Subtype> counter = errors ? entry.getValue().vc.errorSubtypeCounter : entry.getValue().vc.warningSubtypeCounter;
if (counter.getTotal() == 0) {
continue;
}
String name = entry.getKey();
String localeID = sortedNames.get(name);
output.append("<tr>").append(TH_AND_STYLES);
appendNameAndCode(name, localeID, output);
output.append("</th>");
for (Subtype subtype : sortedBySize) {
long count = counter.get(subtype);
output.append("<td class='tvs-count'>");
if (count != 0) {
output.append(nf.format(count));
}
output.append("</td>");
}
}
// subtotals
writeDetailHeader(sortedBySize, output);
output.append("<tr>").append(TH_AND_STYLES).append("<i>Total</i>").append("</th>").append(TH_AND_STYLES).append("</th>");
for (Subtype subtype : sortedBySize) {
long count = subtypeCounterTotals.get(subtype);
output.append("<td class='tvs-count'>");
if (count != 0) {
output.append("<b>").append(nf.format(count)).append("</b>");
}
output.append("</td>");
}
output.append("</table>");
}
private void writeDetailHeader(Set<Subtype> sortedBySize, Appendable output) throws IOException {
output.append("<tr>")
.append(TH_AND_STYLES).append("Name").append("</th>")
.append(TH_AND_STYLES).append("ID").append("</th>");
for (Subtype subtype : sortedBySize) {
output.append(TH_AND_STYLES).append(subtype.toString()).append("</th>");
}
}
/**
* Write one row of the Priority Items Summary
*
* @param output
* @param choices
* @param problemCounter
* @param name
* @param localeID if null, this is a "Total" row to be shown at the bottom of the table
* @param level
* @throws IOException
*
* CAUTION: this method not only uses "th" for "table header" in the usual sense, it also
* uses "th" for cells that contain data, including locale names like "Kashmiri (Devanagari)"
* and code values like "<code>ks_Deva₍_IN₎</code>". The same row may have both "th" and "td" cells.
*/
private void writeSummaryRow(Appendable output, EnumSet<NotificationCategory> choices, Counter<NotificationCategory> problemCounter,
String name, String localeID, Level level) throws IOException, ExecutionException {
output
.append("<tr>")
.append(TH_AND_STYLES)
.append(level.toString())
.append("</th>")
.append(TH_AND_STYLES);
if (localeID == null) {
output
.append("<i>")
.append(name) // here always name = "Total"
.append("</i>")
.append("</th>")
.append(TH_AND_STYLES); // empty cell for Codes
} else {
appendNameAndCode(name, localeID, output);
}
output.append("</th>\n");
final String progPerc = (localeID == null) ? "" : getLocaleProgressPercent(localeID, problemCounter);
output.append("<td class='tvs-count'>").append(progPerc).append("</td>\n");
for (NotificationCategory choice : choices) {
long count = problemCounter.get(choice);
output.append("<td class='tvs-count'>");
if (localeID == null) {
output.append("<b>");
}
output.append(nf.format(count));
if (localeID == null) {
output.append("</b>");
}
output.append("</td>\n");
}
output.append("</tr>\n");
}
private String getLocaleProgressPercent(String localeId, Counter<NotificationCategory> problemCounter) throws ExecutionException {
final LocaleCompletionData lcd = new LocaleCompletionData(problemCounter);
final int problemCount = lcd.problemCount();
final int total = localeBaselineCount.getBaselineProblemCount(CLDRLocale.getInstance(localeId));
final int done = (problemCount >= total) ? 0 : total - problemCount;
// return CompletionPercent.calculate(done, total) + "%";
// Adjust according to https://unicode-org.atlassian.net/browse/CLDR-15785
// This is NOT a logical long-term solution
int perc = CompletionPercent.calculate(done, total);
if (perc == 100 && problemCount > 0) {
perc = 99;
}
return perc + "%";
}
private void appendNameAndCode(String name, String localeID, Appendable output) throws IOException {
// See https://unicode-org.atlassian.net/browse/CLDR-15279
String url = "v#/" + localeID + "//";
String[] names = name.split(SPLIT_CHAR);
output
.append("<a href='" + url)
.append("'>")
.append(TransliteratorUtilities.toHTML.transform(names[0]))
.append("</a>")
.append("</th>")
.append(TH_AND_STYLES)
.append("<code>")
.append(names[1])
.append("</code>");
}
private String getName(String localeID) {
Set<String> contents = supplementalDataInfo.getEquivalentsForLocale(localeID);
// put in special character that can be split on later
return englishFile.getName(localeID, true, CLDRFile.SHORT_ALTS) + SPLIT_CHAR + gatherCodes(contents);
}
/**
* Collapse the names
{en_Cyrl, en_Cyrl_US} => en_Cyrl(_US)
{en_GB, en_Latn_GB} => en(_Latn)_GB
{en, en_US, en_Latn, en_Latn_US} => en(_Latn)(_US)
{az_IR, az_Arab, az_Arab_IR} => az_IR, az_Arab(_IR)
*/
private static String gatherCodes(Set<String> contents) {
Set<Set<String>> source = new LinkedHashSet<>();
for (String s : contents) {
source.add(new LinkedHashSet<>(Arrays.asList(s.split("_"))));
}
Set<Set<String>> oldSource = new LinkedHashSet<>();
do {
// exchange source/target
oldSource.clear();
oldSource.addAll(source);
source.clear();
Set<String> last = null;
for (Set<String> ss : oldSource) {
if (last == null) {
last = ss;
} else {
if (ss.containsAll(last)) {
last = combine(last, ss);
} else {
source.add(last);
last = ss;
}
}
}
source.add(last);
} while (oldSource.size() != source.size());
StringBuilder b = new StringBuilder();
for (Set<String> stringSet : source) {
if (b.length() != 0) {
b.append(", ");
}
String sep = "";
for (String string : stringSet) {
if (string.startsWith(CONNECT_PREFIX)) {
b.append(string + CONNECT_SUFFIX);
} else {
b.append(sep + string);
}
sep = "_";
}
}
return b.toString();
}
private static Set<String> combine(Set<String> last, Set<String> ss) {
LinkedHashSet<String> result = new LinkedHashSet<>();
for (String s : ss) {
if (last.contains(s)) {
result.add(s);
} else {
result.add(CONNECT_PREFIX + s);
}
}
return result;
}
/**
* Used to determine what the status of a particular path's value is in a given locale.
*/
public enum MissingStatus {
/**
* There is an explicit value for the path, including ↑↑↑,
* or there is an inherited value (but not including the ABSENT conditions, e.g. not from root).
*/
PRESENT,
/**
* The value is inherited from a different path. Only applies if the parent is not root.
* That path might be in the same locale or from a parent (but not root or CODE_FALLBACK).
*/
ALIASED,
/**
* See ABSENT
*/
MISSING_OK,
/**
* See ABSENT
*/
ROOT_OK,
/**
* The supplied CLDRFile is null, or the value is null, or the value is inherited from root or CODE_FALLBACK.
* A special ValuePathStatus.isMissingOk method allows for some exceptions, changing the result to MISSING_OK or ROOT_OK.
*/
ABSENT
}
/**
* Get the MissingStatus: for details see the javadoc for MissingStatus.
*
* @param sourceFile the CLDRFile
* @param path the path
* @param latin boolean from isLatinScriptLocale, passed to isMissingOk
* @return the MissingStatus
*/
public static MissingStatus getMissingStatus(CLDRFile sourceFile, String path, boolean latin) {
if (sourceFile == null) {
return MissingStatus.ABSENT;
}
final String sourceLocaleID = sourceFile.getLocaleID();
if ("root".equals(sourceLocaleID)) {
return MissingStatus.MISSING_OK;
}
MissingStatus result;
String value = sourceFile.getStringValue(path);
Status status = new Status();
String sourceLocale = sourceFile.getSourceLocaleIdExtended(path, status, false); // does not skip inheritance marker
boolean isAliased = !path.equals(status.pathWhereFound);
if (DEBUG) {
if (path.equals("//ldml/characterLabels/characterLabelPattern[@type=\"subscript\"]")) {
int debug = 0;
}
if (!isAliased && !sourceLocale.equals(sourceLocaleID)) {
int debug = 0;
}
}
if (value == null) {
result = ValuePathStatus.isMissingOk(sourceFile, path, latin, isAliased) ? MissingStatus.MISSING_OK
: MissingStatus.ABSENT;
} else {
/*
* skipInheritanceMarker must be false for getSourceLocaleIdExtended here, since INHERITANCE_MARKER
* may be found if there are votes for inheritance, in which case we must not skip up to "root" and
* treat the item as missing. Reference: https://unicode.org/cldr/trac/ticket/11765
*/
String localeFound = sourceFile.getSourceLocaleIdExtended(path, status, false /* skipInheritanceMarker */);
final boolean localeFoundIsRootOrCodeFallback = localeFound.equals("root")
|| localeFound.equals(XMLSource.CODE_FALLBACK_ID);
final boolean isParentRoot = CLDRLocale.getInstance(sourceFile.getLocaleID()).isParentRoot();
/*
* Only count it as missing IF the (localeFound is root or codeFallback)
* AND the aliasing didn't change the path.
* Note that localeFound will be where an item with ↑↑↑ was found even though
* the resolved value is actually inherited from somewhere else.
*/
if (localeFoundIsRootOrCodeFallback) {
result = ValuePathStatus.isMissingOk(sourceFile, path, latin, isAliased) ? MissingStatus.ROOT_OK
: isParentRoot ? MissingStatus.ABSENT
: MissingStatus.ALIASED;
} else if (!isAliased) {
result = MissingStatus.PRESENT;
} else if (isParentRoot) { // We handle ALIASED specially, depending on whether the parent is root or not.
result = ValuePathStatus.isMissingOk(sourceFile, path, latin, isAliased) ? MissingStatus.MISSING_OK
: MissingStatus.ABSENT;
} else {
result = MissingStatus.ALIASED;
}
}
return result;
}
public static final UnicodeSet LATIN = ValuePathStatus.LATIN;
public static boolean isLatinScriptLocale(CLDRFile sourceFile) {
return ValuePathStatus.isLatinScriptLocale(sourceFile);
}
private static void appendToMessage(CharSequence usersValue, Subtype subtype, StringBuilder testMessage) {
if (subtype != null) {
usersValue = "&lt;" + subtype + "&gt; " + usersValue;
}
appendToMessage(usersValue, testMessage);
}
private static void appendToMessage(CharSequence usersValue, StringBuilder testMessage) {
if (usersValue.length() == 0) {
return;
}
if (testMessage.length() != 0) {
testMessage.append("<br>");
}
testMessage.append(usersValue);
}
static final NumberFormat nf = NumberFormat.getIntegerInstance(ULocale.ENGLISH);
private final Relation<String, String> reasonsToPaths;
static {
nf.setGroupingUsed(true);
}
/**
* Class that allows the relaying of progress information
*
* @author srl
*
*/
public static class ProgressCallback {
/**
* Note any progress. This will be called before any output is printed.
* It will be called approximately once per xpath.
*/
public void nudge() {
}
/**
* Called when all operations are complete.
*/
public void done() {
}
/**
* Return true to cause an early stop.
* @return
*/
public boolean isStopped() {
return false;
}
}
/*
* null instance by default
*/
private ProgressCallback progressCallback = new ProgressCallback();
/**
* Select a new callback. Must be set before running.
*
* @return
*
*/
public VettingViewer<T> setProgressCallback(ProgressCallback newCallback) {
progressCallback = newCallback;
return this;
}
/**
* Provide the styles for inclusion into the ST &lt;head&gt; element.
*
* @return
*/
public static String getHeaderStyles() {
return "<style>\n"
+ ".hide {display:none}\n"
+ ".vve {}\n"
+ ".vvn {}\n"
+ ".vvp {}\n"
+ ".vvl {}\n"
+ ".vvm {}\n"
+ ".vvu {}\n"
+ ".vvw {}\n"
+ ".vvd {}\n"
+ ".vvo {}\n"
+ "</style>";
}
/**
* Find the status of all the paths in the input file. See the full getStatus for more information.
* @param file the source. Must be a resolved file, made with minimalDraftStatus = unconfirmed
* @param pathHeaderFactory PathHeaderFactory.
* @param foundCounter output counter of the number of paths with values having contributed or approved status
* @param unconfirmedCounter output counter of the number of paths with values, but neither contributed nor approved status
* @param missingCounter output counter of the number of paths without values
* @param missingPaths output if not null, the specific paths that are missing.
* @param unconfirmedPaths TODO
*/
public static void getStatus(CLDRFile file, PathHeader.Factory pathHeaderFactory,
Counter<Level> foundCounter, Counter<Level> unconfirmedCounter,
Counter<Level> missingCounter,
Relation<MissingStatus, String> missingPaths,
Set<String> unconfirmedPaths) {
getStatus(file.fullIterable(), file, pathHeaderFactory, foundCounter, unconfirmedCounter, missingCounter, missingPaths, unconfirmedPaths);
}
/**
* Find the status of an input set of paths in the input file.
* It partitions the returned data according to the Coverage levels.
* NOTE: MissingStatus.ALIASED is handled specially; it is mapped to ABSENT if the parent is root, and otherwise mapped to PRESENT.
* @param allPaths manual list of paths
* @param file the source. Must be a resolved file, made with minimalDraftStatus = unconfirmed
* @param pathHeaderFactory PathHeaderFactory.
* @param foundCounter output counter of the number of paths with values having contributed or approved status
* @param unconfirmedCounter output counter of the number of paths with values, but neither contributed nor approved status
* @param missingCounter output counter of the number of paths without values
* @param missingPaths output if not null, the specific paths that are missing.
* @param unconfirmedPaths TODO
*/
public static void 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) {
if (!file.isResolved()) {
throw new IllegalArgumentException("File must be resolved, no minimal draft status");
}
foundCounter.clear();
unconfirmedCounter.clear();
missingCounter.clear();
boolean latin = VettingViewer.isLatinScriptLocale(file);
CoverageLevel2 coverageLevel2 = CoverageLevel2.getInstance(SupplementalDataInfo.getInstance(), file.getLocaleID());
for (String path : allPaths) {
PathHeader ph = pathHeaderFactory.fromPath(path);
if (ph.getSectionId() == SectionId.Special) {
continue;
}
Level level = coverageLevel2.getLevel(path);
if (level.compareTo(Level.MODERN) > 0) {
continue;
}
MissingStatus missingStatus = VettingViewer.getMissingStatus(file, path, latin);
switch (missingStatus) {
case ABSENT:
missingCounter.add(level, 1);
if (missingPaths != null) {
missingPaths.put(missingStatus, path);
}
break;
case ALIASED:
case PRESENT:
String fullPath = file.getFullXPath(path);
if (fullPath.contains("unconfirmed")
|| fullPath.contains("provisional")) {
unconfirmedCounter.add(level, 1);
if (unconfirmedPaths != null) {
unconfirmedPaths.add(path);
}
} else {
foundCounter.add(level, 1);
}
break;
case MISSING_OK:
case ROOT_OK:
break;
default:
throw new IllegalArgumentException();
}
}
}
final private static EnumSet<NotificationCategory> localeCompletionCategories = EnumSet.of(
NotificationCategory.error,
NotificationCategory.hasDispute,
NotificationCategory.notApproved,
NotificationCategory.missingCoverage
);
public static EnumSet<NotificationCategory> getDashboardNotificationCategories(Organization usersOrg) {
EnumSet<NotificationCategory> choiceSet = EnumSet.allOf(NotificationCategory.class);
if (orgIsNeutralForSummary(usersOrg)) {
choiceSet = EnumSet.of(
NotificationCategory.error,
NotificationCategory.warning,
NotificationCategory.hasDispute,
NotificationCategory.notApproved,
NotificationCategory.missingCoverage
);
// skip weLost, englishChanged, changedOldValue, abstained
}
return choiceSet;
}
public static EnumSet<NotificationCategory> getPriorityItemsSummaryCategories(Organization org) {
EnumSet<NotificationCategory> set = getDashboardNotificationCategories(org);
set.remove(NotificationCategory.abstained);
return set;
}
public static EnumSet<NotificationCategory> getLocaleCompletionCategories() {
return localeCompletionCategories;
}
public interface LocaleBaselineCount {
int getBaselineProblemCount(CLDRLocale cldrLocale) throws ExecutionException;
}
private boolean summarizeAllLocales = false;
public void setSummarizeAllLocales(boolean b) {
summarizeAllLocales = b;
}
}