• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  ******************************************************************************
3  * Copyright (C) 2005-2014, International Business Machines Corporation and   *
4  * others. All Rights Reserved.                                               *
5  ******************************************************************************
6  */
7 
8 package org.unicode.cldr.test;
9 
10 import com.google.common.base.Supplier;
11 import com.google.common.base.Suppliers;
12 import com.google.common.collect.ComparisonChain;
13 import com.google.common.collect.ImmutableList;
14 import com.google.common.collect.ImmutableSet;
15 import com.ibm.icu.dev.util.ElapsedTimer;
16 import com.ibm.icu.impl.Row.R3;
17 import com.ibm.icu.text.ListFormatter;
18 import com.ibm.icu.text.MessageFormat;
19 import java.text.ParsePosition;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.TreeSet;
29 import java.util.function.Function;
30 import java.util.logging.Logger;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
34 import org.unicode.cldr.util.CLDRFile;
35 import org.unicode.cldr.util.CLDRInfo.CandidateInfo;
36 import org.unicode.cldr.util.CLDRInfo.PathValueInfo;
37 import org.unicode.cldr.util.CLDRInfo.UserInfo;
38 import org.unicode.cldr.util.CLDRLocale;
39 import org.unicode.cldr.util.CldrUtility;
40 import org.unicode.cldr.util.Factory;
41 import org.unicode.cldr.util.InternalCldrException;
42 import org.unicode.cldr.util.Level;
43 import org.unicode.cldr.util.PathHeader;
44 import org.unicode.cldr.util.PathHeader.SurveyToolStatus;
45 import org.unicode.cldr.util.PatternCache;
46 import org.unicode.cldr.util.RegexFileParser;
47 import org.unicode.cldr.util.RegexFileParser.RegexLineParser;
48 import org.unicode.cldr.util.StandardCodes;
49 import org.unicode.cldr.util.TransliteratorUtilities;
50 import org.unicode.cldr.util.VoteResolver;
51 import org.unicode.cldr.util.VoteResolver.Status;
52 
53 /**
54  * This class provides a foundation for both console-driven CLDR tests, and Survey Tool Tests.
55  *
56  * <p>To add a test, subclass CLDRFile and override handleCheck and possibly setCldrFileToCheck.
57  * Then put the test into getCheckAll.
58  *
59  * <p>To use the test, take a look at the main in ConsoleCheckCLDR. Note that you need to call
60  * setDisplayInformation with the CLDRFile for the locale that you want the display information (eg
61  * names for codes) to be in.<br>
62  * Some options are passed in the Map options. Examples: boolean SHOW_TIMES =
63  * options.containsKey("SHOW_TIMES"); // for printing times for doing setCldrFileToCheck.
64  *
65  * <p>Some errors/warnings will be explicitly filtered out when calling CheckCLDR's check() method.
66  * The full list of filters can be found in org/unicode/cldr/util/data/CheckCLDR-exceptions.txt.
67  *
68  * @author davis
69  */
70 public abstract class CheckCLDR implements CheckAccessor {
71 
72     /** protected so subclasses can use it */
73     protected static Logger logger = Logger.getLogger(CheckCLDR.class.getSimpleName());
74 
75     /**
76      * set the internal logger level. For ConsoleCheck.
77      *
78      * @returns the previous level
79      */
setLoggerLevel(java.util.logging.Level newLevel)80     public static java.util.logging.Level setLoggerLevel(java.util.logging.Level newLevel) {
81         // NB: we use the full package name here, to avoid conflict with other CLDR classes named
82         // Level
83         java.util.logging.Level oldLevel = logger.getLevel();
84         logger.setLevel(newLevel);
85         return oldLevel;
86     }
87 
88     /** serialize CheckCLDR as just its class name */
toString()89     public String toString() {
90         return getClass().getSimpleName();
91     }
92 
93     public static final boolean LIMITED_SUBMISSION =
94             false; // TODO: CLDR-13337: represent differently
95 
96     private static CLDRFile displayInformation;
97 
98     private CLDRFile cldrFileToCheck;
99     private CLDRFile englishFile = null;
100 
101     private boolean skipTest = false;
102     private Phase phase;
103     private Map<Subtype, List<Pattern>> filtersForLocale = new HashMap<>();
104 
105     @Override
getStringValue(String path)106     public String getStringValue(String path) {
107         return getCldrFileToCheck().getStringValue(path);
108     }
109 
110     @Override
getUnresolvedStringValue(String path)111     public String getUnresolvedStringValue(String path) {
112         return getCldrFileToCheck().getUnresolved().getStringValue(path);
113     }
114 
115     @Override
getLocaleID()116     public String getLocaleID() {
117         return getCldrFileToCheck().getLocaleID();
118     }
119 
120     @Override
getCause()121     public CheckCLDR getCause() {
122         return this;
123     }
124 
125     public enum InputMethod {
126         DIRECT,
127         BULK
128     }
129 
130     public enum StatusAction {
131         /** Allow voting and add new values (in Change column). */
132         ALLOW,
133         /** Allow voting and ticket (in Change column). */
134         ALLOW_VOTING_AND_TICKET,
135         /** Allow voting but no add new values (in Change column). */
136         ALLOW_VOTING_BUT_NO_ADD,
137         /** Only allow filing a ticket. */
138         ALLOW_TICKET_ONLY,
139         /** Disallow (for various reasons) */
140         FORBID_ERRORS(true),
141         FORBID_READONLY(true),
142         FORBID_UNLESS_DATA_SUBMISSION(true),
143         FORBID_NULL(true),
144         FORBID_ROOT(true),
145         FORBID_CODE(true),
146         FORBID_PERMANENT_WITHOUT_FORUM(true);
147 
148         private final boolean isForbidden;
149 
StatusAction()150         private StatusAction() {
151             isForbidden = false;
152         }
153 
StatusAction(boolean isForbidden)154         private StatusAction(boolean isForbidden) {
155             this.isForbidden = isForbidden;
156         }
157 
isForbidden()158         public boolean isForbidden() {
159             return isForbidden;
160         }
161 
canShow()162         public boolean canShow() {
163             return !isForbidden;
164         }
165 
canVote()166         public boolean canVote() {
167             // the one non-voting case
168             if (this == ALLOW_TICKET_ONLY) return false;
169             return !isForbidden();
170         }
171 
canSubmit()172         public boolean canSubmit() {
173             return (this == ALLOW);
174         }
175     }
176 
177     private static final HashMap<String, Phase> PHASE_NAMES = new HashMap<>();
178 
179     public enum Phase {
180         BUILD,
181         SUBMISSION,
182         VETTING,
183         FINAL_TESTING("RESOLUTION");
184 
Phase(String... alternateName)185         Phase(String... alternateName) {
186             for (String name : alternateName) {
187                 PHASE_NAMES.put(name.toUpperCase(Locale.ENGLISH), this);
188             }
189         }
190 
forString(String value)191         public static Phase forString(String value) {
192             if (value == null) {
193                 return org.unicode.cldr.util.CLDRConfig.getInstance().getPhase();
194             }
195             value = value.toUpperCase(Locale.ENGLISH);
196             Phase result = PHASE_NAMES.get(value);
197             return result != null ? result : Phase.valueOf(value);
198         }
199 
200         /** true if it's a 'unit test' phase. */
isUnitTest()201         public boolean isUnitTest() {
202             return this == BUILD || this == FINAL_TESTING;
203         }
204 
205         /**
206          * Return whether or not to show a row, and if so, how.
207          *
208          * @param pathValueInfo - may be null for a non-path entry.
209          * @param inputMethod
210          * @param ph the path header - may be null if it is a non-path entry
211          * @param userInfo null if there is no userInfo (nobody logged in).
212          * @return
213          */
getShowRowAction( PathValueInfo pathValueInfo, InputMethod inputMethod, PathHeader ph, UserInfo userInfo )214         public StatusAction getShowRowAction(
215                 PathValueInfo pathValueInfo,
216                 InputMethod inputMethod,
217                 PathHeader ph,
218                 UserInfo userInfo // can get voterInfo from this.
219                 ) {
220 
221             // default to read/write
222             PathHeader.SurveyToolStatus status = PathHeader.SurveyToolStatus.READ_WRITE;
223             boolean canReadAndWrite = true;
224 
225             if (ph != null) {
226                 status = ph.getSurveyToolStatus();
227                 canReadAndWrite = ph.canReadAndWrite();
228             }
229             /*
230              * Always forbid DEPRECATED items - don't show.
231              *
232              * Currently, bulk submission and TC voting are allowed even for SurveyToolStatus.HIDE,
233              * but not for SurveyToolStatus.DEPRECATED. If we ever want to treat HIDE and DEPRECATED
234              * the same here, then it would be simpler to call ph.shouldHide which is true for both.
235              */
236             if (status == SurveyToolStatus.DEPRECATED) {
237                 return StatusAction.FORBID_READONLY;
238             }
239 
240             if (status == SurveyToolStatus.READ_ONLY) {
241                 return StatusAction.ALLOW_TICKET_ONLY;
242             }
243 
244             // if TC+, allow anything else, even suppressed items and errors
245             if (userInfo != null
246                     && userInfo.getVoterInfo().getLevel().compareTo(VoteResolver.Level.tc) >= 0) {
247                 return StatusAction.ALLOW;
248             }
249 
250             // always forbid bulk import except in data submission.
251             if (inputMethod == InputMethod.BULK && (this != Phase.SUBMISSION && isUnitTest())) {
252                 return StatusAction.FORBID_UNLESS_DATA_SUBMISSION;
253             }
254 
255             if (status == SurveyToolStatus.HIDE) {
256                 return StatusAction.FORBID_READONLY;
257             }
258 
259             ValueStatus valueStatus = ValueStatus.NONE;
260             if (pathValueInfo != null) {
261                 CandidateInfo winner = pathValueInfo.getCurrentItem();
262                 valueStatus = getValueStatus(winner, ValueStatus.NONE, null);
263 
264                 // if limited submission, and winner doesn't have an error, limit the values
265 
266                 if (LIMITED_SUBMISSION) {
267                     if (!SubmissionLocales.allowEvenIfLimited(
268                             pathValueInfo.getLocale().toString(),
269                             pathValueInfo.getXpath(),
270                             valueStatus == ValueStatus.ERROR,
271                             pathValueInfo.getBaselineStatus() == Status.missing)) {
272                         return StatusAction.FORBID_READONLY;
273                     }
274                 }
275             }
276 
277             if (this == Phase.SUBMISSION || isUnitTest()) {
278                 return (canReadAndWrite)
279                         ? StatusAction.ALLOW
280                         : StatusAction.ALLOW_VOTING_AND_TICKET;
281             }
282 
283             // We are in vetting, not in submission
284 
285             // Only allow ADD if we have an error or warning
286             // Only check winning value for errors/warnings per ticket #8677
287             if (valueStatus != ValueStatus.NONE) {
288                 return (canReadAndWrite)
289                         ? StatusAction.ALLOW
290                         : StatusAction.ALLOW_VOTING_AND_TICKET;
291             }
292 
293             // No warnings, so allow just voting.
294             return StatusAction.ALLOW_VOTING_BUT_NO_ADD;
295         }
296 
297         /**
298          * getAcceptNewItemAction. MUST only be called if getShowRowAction(...).canShow() TODO
299          * Consider moving Phase, StatusAction, etc into CLDRInfo.
300          *
301          * @param enteredValue If null, means an abstention. If voting for an existing value,
302          *     pathValueInfo.getValues().contains(enteredValue) MUST be true
303          * @param pathValueInfo
304          * @param inputMethod
305          * @param status
306          * @param userInfo
307          * @return
308          */
getAcceptNewItemAction( CandidateInfo enteredValue, PathValueInfo pathValueInfo, InputMethod inputMethod, PathHeader ph, UserInfo userInfo )309         public StatusAction getAcceptNewItemAction(
310                 CandidateInfo enteredValue,
311                 PathValueInfo pathValueInfo,
312                 InputMethod inputMethod,
313                 PathHeader ph,
314                 UserInfo userInfo // can get voterInfo from this.
315                 ) {
316             if (!ph.canReadAndWrite()) {
317                 return StatusAction.FORBID_READONLY;
318             }
319 
320             // only logged in users can add items.
321             if (userInfo == null) {
322                 return StatusAction.FORBID_ERRORS;
323             }
324 
325             // we can always abstain
326             if (enteredValue == null) {
327                 return StatusAction.ALLOW;
328             }
329 
330             // if TC+, allow anything else, even suppressed items and errors
331             if (userInfo.getVoterInfo().getLevel().compareTo(VoteResolver.Level.tc) >= 0) {
332                 return StatusAction.ALLOW;
333             }
334 
335             // Disallow errors.
336             ValueStatus valueStatus =
337                     getValueStatus(enteredValue, ValueStatus.NONE, CheckStatus.crossCheckSubtypes);
338             if (valueStatus == ValueStatus.ERROR) {
339                 return StatusAction.FORBID_ERRORS;
340             }
341 
342             // Allow items if submission
343             if (this == Phase.SUBMISSION || isUnitTest()) {
344                 return StatusAction.ALLOW;
345             }
346 
347             // Voting for an existing value is ok
348             valueStatus = ValueStatus.NONE;
349             for (CandidateInfo value : pathValueInfo.getValues()) {
350                 if (value == enteredValue) {
351                     return StatusAction.ALLOW;
352                 }
353                 valueStatus = getValueStatus(value, valueStatus, CheckStatus.crossCheckSubtypes);
354             }
355 
356             // If there were any errors/warnings on other values, allow
357             if (valueStatus != ValueStatus.NONE) {
358                 return StatusAction.ALLOW;
359             }
360 
361             // Otherwise (we are vetting, but with no errors or warnings)
362             // DISALLOW NEW STUFF
363 
364             return StatusAction.FORBID_UNLESS_DATA_SUBMISSION;
365         }
366 
367         public enum ValueStatus {
368             ERROR,
369             WARNING,
370             NONE
371         }
372 
getValueStatus( CandidateInfo value, ValueStatus previous, Set<Subtype> changeErrorToWarning)373         public ValueStatus getValueStatus(
374                 CandidateInfo value, ValueStatus previous, Set<Subtype> changeErrorToWarning) {
375             if (previous == ValueStatus.ERROR || value == null) {
376                 return previous;
377             }
378 
379             for (CheckStatus item : value.getCheckStatusList()) {
380                 CheckStatus.Type type = item.getType();
381                 if (type.equals(CheckStatus.Type.Error)) {
382                     if (changeErrorToWarning != null
383                             && changeErrorToWarning.contains(item.getSubtype())) {
384                         return ValueStatus.WARNING;
385                     } else {
386                         return ValueStatus.ERROR;
387                     }
388                 } else if (type.equals(CheckStatus.Type.Warning)) {
389                     previous = ValueStatus.WARNING;
390                 }
391             }
392             return previous;
393         }
394     }
395 
396     public static final class Options implements Comparable<Options> {
397 
398         public enum Option {
399             locale,
400             CoverageLevel_requiredLevel("CoverageLevel.requiredLevel"),
401             CoverageLevel_localeType("CoverageLevel.localeType"),
402             SHOW_TIMES,
403             phase,
404             lgWarningCheck,
405             CheckCoverage_skip("CheckCoverage.skip"),
406             exemplarErrors;
407 
408             private String key;
409 
getKey()410             public String getKey() {
411                 return key;
412             }
413 
Option(String key)414             Option(String key) {
415                 this.key = key;
416             }
417 
Option()418             Option() {
419                 this.key = name();
420             }
421         }
422 
423         private static StandardCodes sc = StandardCodes.make();
424 
425         private final boolean DEBUG_OPTS = false;
426 
427         String options[] = new String[Option.values().length];
428         CLDRLocale locale = null;
429 
430         private final String key; // for fast compare
431 
432         /**
433          * Adopt some other map
434          *
435          * @param fromOptions
436          */
Options(Map<String, String> fromOptions)437         public Options(Map<String, String> fromOptions) {
438             clear();
439             setAll(fromOptions);
440             key = null; // no key = slow compare
441         }
442 
setAll(Map<String, String> fromOptions)443         private void setAll(Map<String, String> fromOptions) {
444             for (Map.Entry<String, String> e : fromOptions.entrySet()) {
445                 set(e.getKey(), e.getValue());
446             }
447         }
448 
449         /**
450          * @param key
451          * @param value
452          */
set(String key, String value)453         public void set(String key, String value) {
454             // TODO- cache the map
455             for (Option o : Option.values()) {
456                 if (o.getKey().equals(key)) {
457                     set(o, value);
458                     return;
459                 }
460             }
461             throw new IllegalArgumentException(
462                     "Unknown CLDR option: '"
463                             + key
464                             + "' - valid keys are: "
465                             + Options.getValidKeys());
466         }
467 
getValidKeys()468         private static String getValidKeys() {
469             Set<String> allkeys = new TreeSet<>();
470             for (Option o : Option.values()) {
471                 allkeys.add(o.getKey());
472             }
473             return ListFormatter.getInstance().format(allkeys);
474         }
475 
Options()476         public Options() {
477             clear();
478             key = "".intern(); // null Options.
479         }
480 
481         /**
482          * Deep clone
483          *
484          * @param options2
485          */
Options(Options options2)486         public Options(Options options2) {
487             this.options = Arrays.copyOf(options2.options, options2.options.length);
488             this.key = options2.key;
489             this.locale = options2.locale;
490         }
491 
Options(CLDRLocale locale)492         public Options(CLDRLocale locale) {
493             this.locale = locale;
494             options = new String[Option.values().length];
495             set(Option.locale, locale.getBaseName());
496             StringBuilder sb = new StringBuilder();
497             sb.append(locale.getBaseName()).append('/');
498             key = sb.toString().intern();
499         }
500 
Options( CLDRLocale locale, CheckCLDR.Phase testPhase, String requiredLevel, String localeType)501         public Options(
502                 CLDRLocale locale,
503                 CheckCLDR.Phase testPhase,
504                 String requiredLevel,
505                 String localeType) {
506             this.locale = locale;
507             options = new String[Option.values().length];
508             StringBuilder sb = new StringBuilder();
509             set(Option.locale, locale.getBaseName());
510             sb.append(locale.getBaseName()).append('/');
511             set(Option.CoverageLevel_requiredLevel, requiredLevel);
512             sb.append(requiredLevel).append('/');
513             set(Option.CoverageLevel_localeType, localeType);
514             sb.append(localeType).append('/');
515             set(Option.phase, testPhase.name().toLowerCase());
516             sb.append(localeType).append('/');
517             key = sb.toString().intern();
518         }
519 
520         @Override
clone()521         public Options clone() {
522             return new Options(this);
523         }
524 
525         @Override
equals(Object other)526         public boolean equals(Object other) {
527             if (this == other) return true;
528             if (!(other instanceof Options)) return false;
529             if (this.key != null && ((Options) other).key != null) {
530                 return (this.key == ((Options) other).key);
531             } else {
532                 return this.compareTo((Options) other) == 0;
533             }
534         }
535 
clear()536         private Options clear() {
537             for (int i = 0; i < options.length; i++) {
538                 options[i] = null;
539             }
540             return this;
541         }
542 
set(Option o, String v)543         private Options set(Option o, String v) {
544             options[o.ordinal()] = v;
545             if (DEBUG_OPTS) System.err.println("Setting " + o + " = " + v);
546             return this;
547         }
548 
get(Option o)549         public String get(Option o) {
550             final String v = options[o.ordinal()];
551             if (DEBUG_OPTS) System.err.println("Getting " + o + " = " + v);
552             return v;
553         }
554 
getLocale()555         public CLDRLocale getLocale() {
556             if (locale != null) return locale;
557             return CLDRLocale.getInstance(get(Option.locale));
558         }
559 
560         /**
561          * Get the required coverage level for the specified locale, for this CheckCLDR object.
562          *
563          * @param localeID
564          * @return the Level
565          *     <p>Called by CheckCoverage.setCldrFileToCheck and CheckDates.setCldrFileToCheck
566          */
getRequiredLevel(String localeID)567         public Level getRequiredLevel(String localeID) {
568             Level result;
569             // see if there is an explicit level
570             String localeType = get(Option.CoverageLevel_requiredLevel);
571             if (localeType != null) {
572                 result = Level.get(localeType);
573                 if (result != Level.UNDETERMINED) {
574                     return result;
575                 }
576             }
577             // otherwise, see if there is an organization level for the "Cldr" organization.
578             // This is not user-specific.
579             return sc.getLocaleCoverageLevel("Cldr", localeID);
580         }
581 
contains(Option o)582         public boolean contains(Option o) {
583             String s = get(o);
584             return (s != null && !s.isEmpty());
585         }
586 
587         @Override
compareTo(Options other)588         public int compareTo(Options other) {
589             if (other == this) return 0;
590             if (key != null && other.key != null) {
591                 if (key == other.key) return 0;
592                 return key.compareTo(other.key);
593             }
594             for (int i = 0; i < options.length; i++) {
595                 final String s1 = options[i];
596                 final String s2 = other.options[i];
597                 if (s1 == null && s2 == null) {
598                     // no difference
599                 } else if (s1 == null) {
600                     return -1;
601                 } else if (s2 == null) {
602                     return 1;
603                 } else {
604                     int rv = s1.compareTo(s2);
605                     if (rv != 0) {
606                         return rv;
607                     }
608                 }
609             }
610             return 0;
611         }
612 
613         @Override
hashCode()614         public int hashCode() {
615             if (key != null) return key.hashCode();
616 
617             int h = 1;
618             for (int i = 0; i < options.length; i++) {
619                 if (options[i] == null) {
620                     h *= 11;
621                 } else {
622                     h = (h * 11) + options[i].hashCode();
623                 }
624             }
625             return h;
626         }
627 
628         @Override
toString()629         public String toString() {
630             if (key != null) return "Options:" + key;
631             StringBuilder sb = new StringBuilder();
632             for (Option o : Option.values()) {
633                 if (options[o.ordinal()] != null) {
634                     sb.append(o).append('=').append(options[o.ordinal()]).append(' ');
635                 }
636             }
637             return sb.toString();
638         }
639     }
640 
isSkipTest()641     public boolean isSkipTest() {
642         return skipTest;
643     }
644 
645     // this should only be set for the test in setCldrFileToCheck
setSkipTest(boolean skipTest)646     public void setSkipTest(boolean skipTest) {
647         this.skipTest = skipTest;
648     }
649 
650     /**
651      * Here is where the list of all checks is found.
652      *
653      * @param nameMatcher Regex pattern that determines which checks are run, based on their class
654      *     name (such as .* for all checks, .*Collisions.* for CheckDisplayCollisions, etc.)
655      * @return
656      */
getCheckAll(Factory factory, String nameMatcher)657     public static CompoundCheckCLDR getCheckAll(Factory factory, String nameMatcher) {
658         return new CompoundCheckCLDR()
659                 .setFilter(Pattern.compile(nameMatcher, Pattern.CASE_INSENSITIVE).matcher(""))
660                 .add(new CheckAnnotations())
661                 // .add(new CheckAttributeValues(factory))
662                 .add(new CheckChildren(factory))
663                 .add(new CheckCoverage(factory))
664                 .add(new CheckDates(factory))
665                 .add(new CheckForCopy(factory))
666                 .add(new CheckDisplayCollisions(factory))
667                 .add(new CheckExemplars(factory))
668                 .add(new CheckForExemplars(factory))
669                 .add(new CheckForInheritanceMarkers())
670                 .add(new CheckNames())
671                 .add(new CheckNumbers(factory))
672                 // .add(new CheckZones()) // this doesn't work; many spurious errors that user can't
673                 // correct
674                 .add(new CheckMetazones())
675                 .add(new CheckLogicalGroupings(factory))
676                 .add(new CheckAlt())
677                 .add(new CheckAltOnly(factory))
678                 .add(new CheckCurrencies())
679                 .add(new CheckCasing())
680                 .add(
681                         new CheckConsistentCasing(
682                                 factory)) // this doesn't work; many spurious errors that user can't
683                 // correct
684                 .add(new CheckQuotes())
685                 .add(new CheckUnits())
686                 .add(new CheckWidths())
687                 .add(new CheckPlaceHolders())
688                 .add(new CheckPersonNames())
689                 .add(new CheckNew(factory)) // this is at the end; it will check for other certain
690         // other errors and warnings and
691         // not add a message if there are any.
692         ;
693     }
694 
695     /** These determine what language is used to display information. Must be set before use. */
getDisplayInformation()696     public static synchronized CLDRFile getDisplayInformation() {
697         return displayInformation;
698     }
699 
setDisplayInformation(CLDRFile inputDisplayInformation)700     public static synchronized void setDisplayInformation(CLDRFile inputDisplayInformation) {
701         displayInformation = inputDisplayInformation;
702     }
703 
704     /** Get the CLDRFile. */
getCldrFileToCheck()705     public final CLDRFile getCldrFileToCheck() {
706         return cldrFileToCheck;
707     }
708 
709     /**
710      * Often subclassed for initializing. If so, make the first 2 lines: if (cldrFileToCheck ==
711      * null) return this; super.handleSetCldrFileToCheck(cldrFileToCheck); do stuff
712      *
713      * <p>Called late via accept().
714      *
715      * @param cldrFileToCheck
716      * @param options
717      * @param possibleErrors any deferred possibleErrors can be set here. They will be appended to
718      *     every handleCheck() call.
719      * @return
720      */
handleSetCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)721     public CheckCLDR handleSetCldrFileToCheck(
722             CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
723 
724         // nothing by default
725         return this;
726     }
727 
728     /**
729      * Set the CLDRFile. Must be done before calling check.
730      *
731      * @param cldrFileToCheck
732      * @param options (not currently used)
733      * @param possibleErrors
734      */
setCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)735     public CheckCLDR setCldrFileToCheck(
736             CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
737         this.cldrFileToCheck = cldrFileToCheck;
738         reset();
739         // clear the *cached* possible Errors. Not counting any set immediately by subclasses.
740         cachedPossibleErrors.clear();
741         cachedOptions = new Options(options);
742         // we must load filters here, as they are used by check()
743 
744         // Shortlist error filters for this locale.
745         String locale = cldrFileToCheck.getLocaleID();
746         filtersForLocale.clear();
747         for (R3<Pattern, Subtype, Pattern> filter : getAllFilters()) {
748             if (filter.get0() == null || !filter.get0().matcher(locale).matches()) continue;
749             Subtype subtype = filter.get1();
750             List<Pattern> xpaths = filtersForLocale.get(subtype);
751             if (xpaths == null) {
752                 filtersForLocale.put(subtype, xpaths = new ArrayList<>());
753             }
754             xpaths.add(filter.get2());
755         }
756 
757         // hook for checks that want to set possibleErrors early
758         handleCheckPossibleErrors(cldrFileToCheck, options, possibleErrors);
759 
760         return this;
761     }
762 
763     /** override this if you want to return errors immediately when setCldrFileToCheck is called */
handleCheckPossibleErrors( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)764     protected void handleCheckPossibleErrors(
765             CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
766         // nothing by default.
767     }
768 
769     /** override this if you want to reset state immediately when setCldrFileToCheck is called */
reset()770     protected void reset() {
771         initted = false;
772     }
773 
774     /**
775      * Subclasses must call this, after any skip calculation to indicate that an xpath is relevant
776      * to them.
777      *
778      * @param result out-parameter to contain any deferred errors
779      * @return false if test is skipped and should exit
780      */
accept(List<CheckStatus> result)781     protected boolean accept(List<CheckStatus> result) {
782         if (!initted) {
783             if (this.cldrFileToCheck == null) {
784                 throw new NullPointerException("accept() was called before setCldrFileToCheck()");
785             }
786             // clear this again.
787             cachedPossibleErrors.clear();
788             // call into the subclass
789             handleSetCldrFileToCheck(this.cldrFileToCheck, cachedOptions, cachedPossibleErrors);
790             // all of these are entireLocale
791             cachedPossibleErrors.forEach(e -> e.setEntireLocale());
792             initted = true;
793         }
794         // unconditionally append all cached possible errors
795         result.addAll(cachedPossibleErrors);
796         if (isSkipTest()) {
797             return false;
798         }
799         return true;
800     }
801 
802     /** has accept() been called since setCldrFileToCheck() was called? */
803     boolean initted = false;
804 
805     /** cache of possible errors, for handleSetCldrFileToCheck */
806     List<CheckStatus> cachedPossibleErrors = new ArrayList<>();
807 
808     Options cachedOptions = null;
809 
810     /**
811      * abstract interface for mapping from a Subtype to a "more details" URL. see
812      * org.unicode.cldr.web.SubtypeToURLMap
813      */
814     public interface SubtypeToURLProvider extends Function<Subtype, String> {}
815 
816     /** Status value returned from check */
817     public static class CheckStatus implements Comparable<CheckStatus> {
818         public static final Type alertType = Type.Comment,
819                 warningType = Type.Warning,
820                 errorType = Type.Error,
821                 exampleType = Type.Example,
822                 demoType = Type.Demo;
823 
824         public enum Type {
825             Comment,
826             Warning,
827             Error,
828             Example,
829             Demo
830         }
831 
832         public enum Subtype {
833             none,
834             noUnproposedVariant,
835             deprecatedAttribute,
836             illegalPlural,
837             invalidLocale,
838             incorrectCasing,
839             valueMustBeOverridden,
840             valueAlwaysOverridden,
841             nullChildFile,
842             internalError,
843             coverageLevel,
844             missingPluralInfo,
845             currencySymbolTooWide,
846             incorrectDatePattern,
847             abbreviatedDateFieldTooWide,
848             displayCollision,
849             illegalExemplarSet,
850             missingAuxiliaryExemplars,
851             extraPlaceholders,
852             missingPlaceholders,
853             shouldntHavePlaceholders,
854             couldNotAccessExemplars,
855             noExemplarCharacters,
856             modifiedEnglishValue,
857             invalidCurrencyMatchSet,
858             multipleMetazoneMappings,
859             noMetazoneMapping,
860             noMetazoneMappingAfter1970,
861             noMetazoneMappingBeforeNow,
862             cannotCreateZoneFormatter,
863             insufficientCoverage,
864             missingLanguageTerritoryInfo,
865             missingEuroCountryInfo,
866             deprecatedAttributeWithReplacement,
867             missingOrExtraDateField,
868             internalUnicodeSetFormattingError,
869             auxiliaryExemplarsOverlap,
870             missingPunctuationCharacters,
871 
872             charactersNotInCurrencyExemplars,
873             asciiCharactersNotInCurrencyExemplars,
874             charactersNotInMainOrAuxiliaryExemplars,
875             asciiCharactersNotInMainOrAuxiliaryExemplars,
876 
877             narrowDateFieldTooWide,
878             illegalCharactersInExemplars,
879             orientationDisagreesWithExemplars,
880             inconsistentDatePattern,
881             inconsistentTimePattern,
882             missingDatePattern,
883             illegalDatePattern,
884             missingMainExemplars,
885             mustNotStartOrEndWithSpace,
886             illegalCharactersInNumberPattern,
887             numberPatternNotCanonical,
888             currencyPatternMissingCurrencySymbol,
889             currencyPatternUnexpectedCurrencySymbol,
890             missingMinusSign,
891             badNumericType,
892             percentPatternMissingPercentSymbol,
893             illegalNumberFormat,
894             unexpectedAttributeValue,
895             metazoneContainsDigit,
896             tooManyGroupingSeparators,
897             inconsistentPluralFormat,
898             missingZeros,
899             sameAsEnglish,
900             sameAsCode,
901             dateSymbolCollision,
902             incompleteLogicalGroup,
903             extraMetazoneString,
904             inconsistentDraftStatus,
905             errorOrWarningInLogicalGroup,
906             valueTooWide,
907             valueTooNarrow,
908             nameContainsYear,
909             patternCannotContainDigits,
910             patternContainsInvalidCharacters,
911             parenthesesNotAllowed,
912             illegalNumberingSystem,
913             unexpectedOrderOfEraYear,
914             invalidPlaceHolder,
915             asciiQuotesNotAllowed,
916             badMinimumGroupingDigits,
917             inconsistentPeriods,
918             inheritanceMarkerNotAllowed,
919             invalidDurationUnitPattern,
920             invalidDelimiter,
921             illegalCharactersInPattern,
922             badParseLenient,
923             tooManyValues,
924             invalidSymbol,
925             invalidGenderCode,
926             mismatchedUnitComponent,
927             longPowerWithSubscripts,
928             gapsInPlaceholderNumbers,
929             duplicatePlaceholders,
930             largerDifferences,
931             missingNonAltPath,
932             badSamplePersonName,
933             missingLanguage,
934             namePlaceholderProblem,
935             missingSpaceBetweenNameFields,
936             shortDateFieldInconsistentLength,
937             illegalParameterValue,
938             illegalAnnotationCode,
939             illegalCharacter;
940 
941             @Override
toString()942             public String toString() {
943                 // converts "thisThisThis" to "this this this"
944                 return TO_STRING.matcher(name()).replaceAll(" $1").toLowerCase();
945             }
946 
947             static Pattern TO_STRING = PatternCache.get("([A-Z])");
948         }
949 
950         /**
951          * These error don't prevent entry during submission, since they become valid if a different
952          * row is changed.
953          */
954         public static Set<Subtype> crossCheckSubtypes =
955                 ImmutableSet.of(
956                         Subtype.dateSymbolCollision,
957                         Subtype.displayCollision,
958                         Subtype.inconsistentDraftStatus,
959                         Subtype.incompleteLogicalGroup,
960                         Subtype.inconsistentPeriods,
961                         Subtype.abbreviatedDateFieldTooWide,
962                         Subtype.narrowDateFieldTooWide,
963                         Subtype.shortDateFieldInconsistentLength,
964                         Subtype.coverageLevel);
965 
966         public static Set<Subtype> errorCodesPath =
967                 ImmutableSet.of(
968                         Subtype.duplicatePlaceholders,
969                         Subtype.extraPlaceholders,
970                         Subtype.gapsInPlaceholderNumbers,
971                         Subtype.invalidPlaceHolder,
972                         Subtype.missingPlaceholders,
973                         Subtype.shouldntHavePlaceholders);
974 
975         private Type type;
976         private Subtype subtype = Subtype.none;
977         private String messageFormat;
978         private Object[] parameters;
979         private CheckAccessor cause;
980         private boolean checkOnSubmit = true;
981 
CheckStatus()982         public CheckStatus() {}
983 
isCheckOnSubmit()984         public boolean isCheckOnSubmit() {
985             return checkOnSubmit;
986         }
987 
setCheckOnSubmit(boolean dependent)988         public CheckStatus setCheckOnSubmit(boolean dependent) {
989             this.checkOnSubmit = dependent;
990             return this;
991         }
992 
getType()993         public Type getType() {
994             return type;
995         }
996 
setMainType(CheckStatus.Type type)997         public CheckStatus setMainType(CheckStatus.Type type) {
998             this.type = type;
999             return this;
1000         }
1001 
getMessage()1002         public String getMessage() {
1003             String message = messageFormat;
1004             if (messageFormat != null && parameters != null) {
1005                 try {
1006                     String fixedApos = MessageFormat.autoQuoteApostrophe(messageFormat);
1007                     MessageFormat format = new MessageFormat(fixedApos);
1008                     message = format.format(parameters);
1009                     if (errorCodesPath.contains(subtype)) {
1010                         message +=
1011                                 "; see <a href='http://cldr.unicode.org/translation/error-codes#"
1012                                         + subtype.name()
1013                                         + "'  target='cldr_error_codes'>"
1014                                         + subtype
1015                                         + "</a>.";
1016                     }
1017                 } catch (Exception e) {
1018                     message = messageFormat;
1019                     final String failMsg =
1020                             "MessageFormat Failure: "
1021                                     + subtype
1022                                     + "; "
1023                                     + messageFormat
1024                                     + "; "
1025                                     + (parameters == null ? null : Arrays.asList(parameters));
1026                     logger.log(java.util.logging.Level.SEVERE, e, () -> failMsg);
1027                     System.err.println(failMsg);
1028                     // throw new IllegalArgumentException(subtype + "; " + messageFormat + "; "
1029                     // + (parameters == null ? null : Arrays.asList(parameters)), e);
1030                 }
1031             }
1032             Exception[] exceptionParameters = getExceptionParameters();
1033             if (exceptionParameters != null) {
1034                 for (Exception exception : exceptionParameters) {
1035                     message += "; " + exception.getMessage(); // + " \t(" +
1036                     // exception.getClass().getName() + ")";
1037                     // for (StackTraceElement item : exception.getStackTrace()) {
1038                     // message += "\n\t" + item;
1039                     // }
1040                 }
1041             }
1042             return message.replace('\t', ' ');
1043         }
1044 
setMessage(String message)1045         public CheckStatus setMessage(String message) {
1046             if (cause == null) {
1047                 throw new IllegalArgumentException("Must have cause set.");
1048             }
1049             if (message == null) {
1050                 throw new IllegalArgumentException("Message cannot be null.");
1051             }
1052             this.messageFormat = message;
1053             this.parameters = null;
1054             return this;
1055         }
1056 
setMessage(String message, Object... messageArguments)1057         public CheckStatus setMessage(String message, Object... messageArguments) {
1058             if (cause == null) {
1059                 throw new IllegalArgumentException("Must have cause set.");
1060             }
1061             this.messageFormat = message;
1062             this.parameters = messageArguments;
1063             return this;
1064         }
1065 
1066         @Override
toString()1067         public String toString() {
1068             return getType() + ": " + getMessage();
1069         }
1070 
1071         /** Warning: don't change the contents of the parameters after retrieving. */
getParameters()1072         public Object[] getParameters() {
1073             return parameters;
1074         }
1075 
1076         /**
1077          * Returns any Exception parameters in the status, or null if there are none.
1078          *
1079          * @return
1080          */
getExceptionParameters()1081         public Exception[] getExceptionParameters() {
1082             if (parameters == null) {
1083                 return null;
1084             }
1085 
1086             List<Exception> errors = new ArrayList<>();
1087             for (Object o : parameters) {
1088                 if (o instanceof Exception) {
1089                     errors.add((Exception) o);
1090                 }
1091             }
1092             if (errors.size() == 0) {
1093                 return null;
1094             }
1095             return errors.toArray(new Exception[errors.size()]);
1096         }
1097 
1098         /** Warning: don't change the contents of the parameters after passing in. */
setParameters(Object[] parameters)1099         public CheckStatus setParameters(Object[] parameters) {
1100             if (cause == null) {
1101                 throw new IllegalArgumentException("Must have cause set.");
1102             }
1103             this.parameters = parameters;
1104             return this;
1105         }
1106 
getDemo()1107         public SimpleDemo getDemo() {
1108             return null;
1109         }
1110 
getCause()1111         public CheckCLDR getCause() {
1112             return cause instanceof CheckCLDR ? (CheckCLDR) cause : null;
1113         }
1114 
setCause(CheckAccessor cause)1115         public CheckStatus setCause(CheckAccessor cause) {
1116             this.cause = cause;
1117             return this;
1118         }
1119 
getSubtype()1120         public Subtype getSubtype() {
1121             return subtype;
1122         }
1123 
setSubtype(Subtype subtype)1124         public CheckStatus setSubtype(Subtype subtype) {
1125             this.subtype = subtype;
1126             return this;
1127         }
1128 
1129         /**
1130          * Convenience function: return true if any items in this list are of errorType
1131          *
1132          * @param result the list to check (could be null for empty)
1133          * @return true if any items in result are of errorType
1134          */
hasError(List<CheckStatus> result)1135         public static final boolean hasError(List<CheckStatus> result) {
1136             return hasType(result, errorType);
1137         }
1138 
1139         /**
1140          * Convenience function: return true if any items in this list are of errorType
1141          *
1142          * @param result the list to check (could be null for empty)
1143          * @return true if any items in result are of errorType
1144          */
hasType(List<CheckStatus> result, Type type)1145         public static boolean hasType(List<CheckStatus> result, Type type) {
1146             if (result == null) return false;
1147             for (CheckStatus s : result) {
1148                 if (s.getType().equals(type)) {
1149                     return true;
1150                 }
1151             }
1152             return false;
1153         }
1154 
1155         /**
1156          * @returns true if this status applies to the entire locale, not a single path
1157          */
getEntireLocale()1158         public boolean getEntireLocale() {
1159             return entireLocale;
1160         }
1161 
1162         /** Mark this CheckStatus as isEntireLocale */
setEntireLocale()1163         CheckStatus setEntireLocale() {
1164             entireLocale = true;
1165             return this;
1166         }
1167 
1168         private boolean entireLocale = false;
1169 
1170         @Override
compareTo(CheckStatus o)1171         public int compareTo(CheckStatus o) {
1172             if (this == o) return 0;
1173             return ComparisonChain.start()
1174                     .compare(getType(), o.getType())
1175                     .compare(getSubtype(), o.getSubtype())
1176                     .compare(getMessage(), o.getMessage())
1177                     .result();
1178         }
1179 
1180         @Override
equals(Object o)1181         public boolean equals(Object o) {
1182             if (this == o) return true;
1183             if (o instanceof CheckStatus) return false;
1184             return compareTo((CheckStatus) o) == 0;
1185         }
1186     }
1187 
1188     public abstract static class SimpleDemo {
1189         Map<String, String> internalPostArguments = new HashMap<>();
1190 
1191         /**
1192          * @param postArguments A read-write map containing post-style arguments. eg TEXTBOX=abcd,
1193          *     etc. <br>
1194          *     The first time this is called, the Map should be empty.
1195          * @return true if the map has been changed
1196          */
getHTML(Map<String, String> postArguments)1197         public abstract String getHTML(Map<String, String> postArguments) throws Exception;
1198 
1199         /** Only here for compatibility. Use the other getHTML instead */
getHTML(String path, String fullPath, String value)1200         public final String getHTML(String path, String fullPath, String value) throws Exception {
1201             return getHTML(internalPostArguments);
1202         }
1203 
1204         /**
1205          * THIS IS ONLY FOR COMPATIBILITY: you can call this, then the non-postArguments form of
1206          * getHTML; or better, call getHTML with the postArguments.
1207          *
1208          * @param postArguments A read-write map containing post-style arguments. eg TEXTBOX=abcd,
1209          *     etc.
1210          * @return true if the map has been changed
1211          */
processPost(Map<String, String> postArguments)1212         public final boolean processPost(Map<String, String> postArguments) {
1213             internalPostArguments.clear();
1214             internalPostArguments.putAll(postArguments);
1215             return true;
1216         }
1217     }
1218 
1219     public abstract static class FormatDemo extends SimpleDemo {
1220         protected String currentPattern, currentInput, currentFormatted, currentReparsed;
1221         protected ParsePosition parsePosition = new ParsePosition(0);
1222 
getPattern()1223         protected abstract String getPattern();
1224 
getSampleInput()1225         protected abstract String getSampleInput();
1226 
getArguments(Map<String, String> postArguments)1227         protected abstract void getArguments(Map<String, String> postArguments);
1228 
1229         @Override
getHTML(Map<String, String> postArguments)1230         public String getHTML(Map<String, String> postArguments) throws Exception {
1231             getArguments(postArguments);
1232             StringBuffer htmlMessage = new StringBuffer();
1233             FormatDemo.appendTitle(htmlMessage);
1234             FormatDemo.appendLine(
1235                     htmlMessage, currentPattern, currentInput, currentFormatted, currentReparsed);
1236             htmlMessage.append("</table>");
1237             return htmlMessage.toString();
1238         }
1239 
getPlainText(Map<String, String> postArguments)1240         public String getPlainText(Map<String, String> postArguments) {
1241             getArguments(postArguments);
1242             return MessageFormat.format(
1243                     "<\"\u200E{0}\u200E\", \"{1}\"> \u2192 \"\u200E{2}\u200E\" \u2192 \"{3}\"",
1244                     (Object[])
1245                             new String[] {
1246                                 currentPattern, currentInput, currentFormatted, currentReparsed
1247                             });
1248         }
1249 
1250         /**
1251          * @param htmlMessage
1252          * @param pattern
1253          * @param input
1254          * @param formatted
1255          * @param reparsed
1256          */
appendLine( StringBuffer htmlMessage, String pattern, String input, String formatted, String reparsed)1257         public static void appendLine(
1258                 StringBuffer htmlMessage,
1259                 String pattern,
1260                 String input,
1261                 String formatted,
1262                 String reparsed) {
1263             htmlMessage
1264                     .append("<tr><td><input type='text' name='pattern' value='")
1265                     .append(TransliteratorUtilities.toXML.transliterate(pattern))
1266                     .append("'></td><td><input type='text' name='input' value='")
1267                     .append(TransliteratorUtilities.toXML.transliterate(input))
1268                     .append("'></td><td>")
1269                     .append("<input type='submit' value='Test' name='Test'>")
1270                     .append("</td><td>" + "<input type='text' name='formatted' value='")
1271                     .append(TransliteratorUtilities.toXML.transliterate(formatted))
1272                     .append("'></td><td>" + "<input type='text' name='reparsed' value='")
1273                     .append(TransliteratorUtilities.toXML.transliterate(reparsed))
1274                     .append("'></td></tr>");
1275         }
1276 
1277         /**
1278          * @param htmlMessage
1279          */
appendTitle(StringBuffer htmlMessage)1280         public static void appendTitle(StringBuffer htmlMessage) {
1281             htmlMessage.append(
1282                     "<table border='1' cellspacing='0' cellpadding='2'"
1283                             +
1284                             // " style='border-collapse: collapse' style='width: 100%'" +
1285                             "><tr>"
1286                             + "<th>Pattern</th>"
1287                             + "<th>Unlocalized Input</th>"
1288                             + "<th></th>"
1289                             + "<th>Localized Format</th>"
1290                             + "<th>Re-Parsed</th>"
1291                             + "</tr>");
1292         }
1293     }
1294 
1295     /**
1296      * Checks the path/value in the cldrFileToCheck for correctness, according to some criterion. If
1297      * the path is relevant to the check, there is an alert or warning, then a CheckStatus is added
1298      * to List.
1299      *
1300      * @param path Must be a distinguished path, such as what comes out of CLDRFile.iterator()
1301      * @param fullPath Must be the full path
1302      * @param value the value associated with the path
1303      * @param result
1304      */
check( String path, String fullPath, String value, Options options, List<CheckStatus> result)1305     public final CheckCLDR check(
1306             String path, String fullPath, String value, Options options, List<CheckStatus> result) {
1307         if (cldrFileToCheck == null) {
1308             throw new InternalCldrException("CheckCLDR problem: cldrFileToCheck must not be null");
1309         }
1310         if (path == null) {
1311             throw new InternalCldrException("CheckCLDR problem: path must not be null");
1312         }
1313         // if (fullPath == null) {
1314         // throw new InternalError("CheckCLDR problem: fullPath must not be null");
1315         // }
1316         // if (value == null) {
1317         // throw new InternalError("CheckCLDR problem: value must not be null");
1318         // }
1319         result.clear();
1320 
1321         /*
1322          * If the item is non-winning, and either inherited or it is code-fallback, then don't run
1323          * any tests on this item.  See http://unicode.org/cldr/trac/ticket/7574
1324          *
1325          * The following conditional formerly used "value == ..." and "value != ...", which in Java doesn't
1326          * mean what it does in some other languages. The condition has been changed to use the equals() method.
1327          * Since value can be null, check for that first.
1328          */
1329         // if (value == cldrFileToCheck.getBaileyValue(path, null, null) && value !=
1330         // cldrFileToCheck.getWinningValue(path)) {
1331         if (value != null
1332                 && !value.equals(cldrFileToCheck.getWinningValue(path))
1333                 && cldrFileToCheck.getUnresolved().getStringValue(path) == null) {
1334             return this;
1335         }
1336 
1337         // If we're being asked to run tests for an inheritance marker, then we need to change it
1338         // to the "real" value first before running tests. Testing the value
1339         // CldrUtility.INHERITANCE_MARKER ("↑↑↑") doesn't make sense.
1340         if (CldrUtility.INHERITANCE_MARKER.equals(value)) {
1341             value = cldrFileToCheck.getBaileyValue(path, null, null);
1342             // If it hasn't changed, then don't run any tests.
1343             if (CldrUtility.INHERITANCE_MARKER.equals(value)) {
1344                 return this;
1345             }
1346         }
1347         CheckCLDR instance = handleCheck(path, fullPath, value, options, result);
1348         Iterator<CheckStatus> iterator = result.iterator();
1349         // Filter out any errors/warnings that match the filter list in CheckCLDR-exceptions.txt.
1350         while (iterator.hasNext()) {
1351             CheckStatus status = iterator.next();
1352             if (shouldExcludeStatus(fullPath, status)) {
1353                 iterator.remove();
1354             }
1355         }
1356         return instance;
1357     }
1358 
1359     /**
1360      * Returns any examples in the result parameter. Both examples and demos can be returned. A demo
1361      * will have getType() == CheckStatus.demoType. In that case, there will be no getMessage
1362      * available; instead, call getDemo() to get the demo, then call getHTML() to get the initial
1363      * HTML.
1364      */
getExamples( String path, String fullPath, String value, Options options, List<CheckStatus> result)1365     public final CheckCLDR getExamples(
1366             String path, String fullPath, String value, Options options, List<CheckStatus> result) {
1367         result.clear();
1368         return handleGetExamples(path, fullPath, value, options, result);
1369     }
1370 
1371     @SuppressWarnings("unused")
handleGetExamples( String path, String fullPath, String value, Options options2, List<CheckStatus> result)1372     protected CheckCLDR handleGetExamples(
1373             String path,
1374             String fullPath,
1375             String value,
1376             Options options2,
1377             List<CheckStatus> result) {
1378         return this; // NOOP unless overridden
1379     }
1380 
1381     /**
1382      * This is what the subclasses override.
1383      *
1384      * <p>If a path is not applicable, exit early with <code>return this;</code> Once a path is
1385      * applicable, call <code>accept(result);</code> to add deferred possible problems.
1386      *
1387      * <p>If something is found, a CheckStatus is added to result. This can be done multiple times
1388      * in one call, if multiple errors or warnings are found. The CheckStatus may return warnings,
1389      * errors, examples, or demos. We may expand that in the future.
1390      *
1391      * <p>The code to add the CheckStatus will look something like::
1392      *
1393      * <pre>
1394      * result.add(new CheckStatus()
1395      *     .setType(CheckStatus.errorType)
1396      *     .setMessage(&quot;Value should be {0}&quot;, new Object[] { pattern }));
1397      * </pre>
1398      */
handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)1399     public abstract CheckCLDR handleCheck(
1400             String path, String fullPath, String value, Options options, List<CheckStatus> result);
1401 
1402     /** Only for use in ConsoleCheck, for debugging */
handleFinish()1403     public void handleFinish() {}
1404 
1405     /**
1406      * Internal class used to bundle up a number of Checks.
1407      *
1408      * @author davis
1409      */
1410     static class CompoundCheckCLDR extends CheckCLDR {
1411         private Matcher filter;
1412         private List<CheckCLDR> checkList = new ArrayList<>();
1413         private List<CheckCLDR> filteredCheckList = new ArrayList<>();
1414 
add(CheckCLDR item)1415         public CompoundCheckCLDR add(CheckCLDR item) {
1416             checkList.add(item);
1417             if (filter == null) {
1418                 filteredCheckList.add(item);
1419             } else {
1420                 final String className = item.getClass().getName();
1421                 if (filter.reset(className).find()) {
1422                     filteredCheckList.add(item);
1423                 }
1424             }
1425             return this;
1426         }
1427 
1428         @Override
handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)1429         public CheckCLDR handleCheck(
1430                 String path,
1431                 String fullPath,
1432                 String value,
1433                 Options options,
1434                 List<CheckStatus> result) {
1435             result.clear();
1436 
1437             if (!accept(result)) return this;
1438 
1439             // If we're being asked to run tests for an inheritance marker, then we need to change
1440             // it
1441             // to the "real" value first before running tests. Testing the value
1442             // CldrUtility.INHERITANCE_MARKER ("↑↑↑") doesn't make sense.
1443             if (CldrUtility.INHERITANCE_MARKER.equals(value)) {
1444                 value = getCldrFileToCheck().getBaileyValue(path, null, null);
1445             }
1446             for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) {
1447                 CheckCLDR item = it.next();
1448                 // skip proposed items in final testing.
1449                 if (Phase.FINAL_TESTING == item.getPhase()) {
1450                     if (path.contains("proposed") && path.contains("[@alt=")) {
1451                         continue;
1452                     }
1453                 }
1454                 try {
1455                     if (!item.isSkipTest()) {
1456                         item.handleCheck(path, fullPath, value, options, result);
1457                     }
1458                 } catch (Exception e) {
1459                     addError(result, item, e);
1460                     return this;
1461                 }
1462             }
1463             return this;
1464         }
1465 
1466         @Override
handleFinish()1467         public void handleFinish() {
1468             for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) {
1469                 CheckCLDR item = it.next();
1470                 item.handleFinish();
1471             }
1472         }
1473 
1474         @Override
handleGetExamples( String path, String fullPath, String value, Options options, List<CheckStatus> result)1475         protected CheckCLDR handleGetExamples(
1476                 String path,
1477                 String fullPath,
1478                 String value,
1479                 Options options,
1480                 List<CheckStatus> result) {
1481             result.clear();
1482             for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) {
1483                 CheckCLDR item = it.next();
1484                 try {
1485                     item.handleGetExamples(path, fullPath, value, options, result);
1486                 } catch (Exception e) {
1487                     addError(result, item, e);
1488                     return this;
1489                 }
1490             }
1491             return this;
1492         }
1493 
addError(List<CheckStatus> result, CheckCLDR item, Exception e)1494         private void addError(List<CheckStatus> result, CheckCLDR item, Exception e) {
1495             // send to java.util.logging, useful for servers
1496             logger.log(
1497                     java.util.logging.Level.SEVERE,
1498                     e,
1499                     () -> {
1500                         String locale = "(unknown)";
1501                         if (item.cldrFileToCheck != null) {
1502                             locale = item.cldrFileToCheck.getLocaleID();
1503                         }
1504                         return String.format(
1505                                 "Internal error: %s in %s", item.getClass().getName(), locale);
1506                     });
1507             // also add as a check
1508             result.add(
1509                     new CheckStatus()
1510                             .setCause(this)
1511                             .setMainType(CheckStatus.errorType)
1512                             .setSubtype(Subtype.internalError)
1513                             .setMessage(
1514                                     "Internal error in {0}. Exception: {1}, Message: {2}, Trace: {3}",
1515                                     new Object[] {
1516                                         item.getClass().getName(),
1517                                         e.getClass().getName(),
1518                                         e,
1519                                         Arrays.asList(e.getStackTrace())
1520                                     }));
1521         }
1522 
1523         @Override
handleCheckPossibleErrors( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)1524         public void handleCheckPossibleErrors(
1525                 CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
1526             ElapsedTimer testTime = null, testOverallTime = null;
1527             if (cldrFileToCheck == null) return;
1528             boolean SHOW_TIMES = options.contains(Options.Option.SHOW_TIMES);
1529             setPhase(Phase.forString(options.get(Options.Option.phase)));
1530             if (SHOW_TIMES)
1531                 testOverallTime = new ElapsedTimer("Test setup time for setCldrFileToCheck: {0}");
1532             super.handleCheckPossibleErrors(cldrFileToCheck, options, possibleErrors);
1533             possibleErrors.clear();
1534 
1535             for (Iterator<CheckCLDR> it = filteredCheckList.iterator(); it.hasNext(); ) {
1536                 CheckCLDR item = it.next();
1537                 if (SHOW_TIMES)
1538                     testTime =
1539                             new ElapsedTimer(
1540                                     "Test setup time for " + item.getClass().toString() + ": {0}");
1541                 try {
1542                     item.setPhase(getPhase());
1543                     item.setCldrFileToCheck(cldrFileToCheck, options, possibleErrors);
1544                     if (SHOW_TIMES) {
1545                         if (item.isSkipTest()) {
1546                             System.out.println("Disabled : " + testTime);
1547                         } else {
1548                             System.out.println("OK : " + testTime);
1549                         }
1550                     }
1551                 } catch (RuntimeException e) {
1552                     addError(possibleErrors, item, e);
1553                     if (SHOW_TIMES) System.out.println("ERR: " + testTime + " - " + e.toString());
1554                 }
1555             }
1556             if (SHOW_TIMES) System.out.println("Overall: " + testOverallTime + ": {0}");
1557             // all of these are entire locale
1558             possibleErrors.forEach(e -> e.setEntireLocale());
1559         }
1560 
getFilter()1561         public Matcher getFilter() {
1562             return filter;
1563         }
1564 
setFilter(Matcher filter)1565         public CompoundCheckCLDR setFilter(Matcher filter) {
1566             this.filter = filter;
1567             filteredCheckList.clear();
1568             for (Iterator<CheckCLDR> it = checkList.iterator(); it.hasNext(); ) {
1569                 CheckCLDR item = it.next();
1570                 if (filter == null || filter.reset(item.getClass().getName()).matches()) {
1571                     filteredCheckList.add(item);
1572                     item.handleSetCldrFileToCheck(getCldrFileToCheck(), (Options) null, null);
1573                 }
1574             }
1575             return this;
1576         }
1577 
getFilteredTests()1578         public String getFilteredTests() {
1579             return filteredCheckList.toString();
1580         }
1581 
getFilteredTestList()1582         public List<CheckCLDR> getFilteredTestList() {
1583             return filteredCheckList;
1584         }
1585     }
1586 
1587     @Override
getPhase()1588     public Phase getPhase() {
1589         return phase;
1590     }
1591 
setPhase(Phase phase)1592     public void setPhase(Phase phase) {
1593         this.phase = phase;
1594     }
1595 
1596     /** A map of error/warning types to their filters. */
1597     private static Supplier<List<R3<Pattern, Subtype, Pattern>>> filterSupplier =
1598             Suppliers.memoize(
1599                     () -> {
1600                         final List<R3<Pattern, Subtype, Pattern>> newFilters = new ArrayList<>();
1601                         RegexFileParser fileParser = new RegexFileParser();
1602                         fileParser.setLineParser(
1603                                 new RegexLineParser() {
1604                                     @Override
1605                                     public void parse(String line) {
1606                                         String[] fields = line.split("\\s*;\\s*");
1607                                         Subtype subtype = Subtype.valueOf(fields[0]);
1608                                         Pattern locale = PatternCache.get(fields[1]);
1609                                         Pattern xpathRegex =
1610                                                 PatternCache.get(
1611                                                         fields[2].replaceAll("\\[@", "\\\\[@"));
1612                                         newFilters.add(new R3<>(locale, subtype, xpathRegex));
1613                                     }
1614                                 });
1615                         fileParser.parse(
1616                                 CheckCLDR.class,
1617                                 "/org/unicode/cldr/util/data/CheckCLDR-exceptions.txt");
1618                         return ImmutableList.copyOf(newFilters);
1619                     });
1620 
getAllFilters()1621     private static final List<R3<Pattern, Subtype, Pattern>> getAllFilters() {
1622         return filterSupplier.get();
1623     }
1624 
1625     /**
1626      * Checks if a status should be excluded from the list of results returned from CheckCLDR.
1627      *
1628      * @param xpath the xpath that the status belongs to
1629      * @param status the status
1630      * @return true if the status should be included
1631      */
shouldExcludeStatus(String xpath, CheckStatus status)1632     private boolean shouldExcludeStatus(String xpath, CheckStatus status) {
1633         List<Pattern> xpathPatterns = filtersForLocale.get(status.getSubtype());
1634         if (xpathPatterns == null) {
1635             return false;
1636         }
1637         for (Pattern xpathPattern : xpathPatterns) {
1638             if (xpathPattern.matcher(xpath).matches()) {
1639                 return true;
1640             }
1641         }
1642         return false;
1643     }
1644 
getEnglishFile()1645     public CLDRFile getEnglishFile() {
1646         return englishFile;
1647     }
1648 
setEnglishFile(CLDRFile englishFile)1649     public void setEnglishFile(CLDRFile englishFile) {
1650         this.englishFile = englishFile;
1651     }
1652 
fixedValueIfInherited(String value, String path)1653     public CharSequence fixedValueIfInherited(String value, String path) {
1654         return !CldrUtility.INHERITANCE_MARKER.equals(value)
1655                 ? value
1656                 : getCldrFileToCheck().getStringValueWithBailey(path);
1657     }
1658 }
1659