• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.test;
2 
3 import java.util.Collection;
4 import java.util.Collections;
5 import java.util.EnumSet;
6 import java.util.HashSet;
7 import java.util.Iterator;
8 import java.util.LinkedHashSet;
9 import java.util.List;
10 import java.util.Map.Entry;
11 import java.util.Set;
12 import java.util.TreeSet;
13 import java.util.regex.Matcher;
14 import java.util.regex.Pattern;
15 
16 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
17 import org.unicode.cldr.util.CLDRConfig;
18 import org.unicode.cldr.util.LocaleIDParser;
19 import org.unicode.cldr.util.Pair;
20 import org.unicode.cldr.util.PatternCache;
21 import org.unicode.cldr.util.XPathParts;
22 import org.unicode.cldr.util.personname.PersonNameFormatter;
23 import org.unicode.cldr.util.personname.PersonNameFormatter.Field;
24 import org.unicode.cldr.util.personname.PersonNameFormatter.FormatParameters;
25 import org.unicode.cldr.util.personname.PersonNameFormatter.ModifiedField;
26 import org.unicode.cldr.util.personname.PersonNameFormatter.Modifier;
27 import org.unicode.cldr.util.personname.PersonNameFormatter.NamePattern;
28 import org.unicode.cldr.util.personname.PersonNameFormatter.Order;
29 import org.unicode.cldr.util.personname.PersonNameFormatter.Usage;
30 
31 import com.google.common.base.Joiner;
32 import com.google.common.base.Splitter;
33 import com.google.common.collect.ImmutableSet;
34 import com.google.common.collect.Multimap;
35 import com.google.common.collect.Sets;
36 import com.ibm.icu.text.UnicodeSet;
37 import com.ibm.icu.util.Output;
38 
39 public class CheckPlaceHolders extends CheckCLDR {
40 
41     private static final Pattern PLACEHOLDER_PATTERN = PatternCache.get("([0-9]|[1-9][0-9]+)");
42     private static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults();
43     private static final Joiner JOIN_SPACE = Joiner.on(' ');
44 
45     private static final Pattern SKIP_PATH_LIST = Pattern
46         .compile("//ldml/characters/(exemplarCharacters|parseLenient).*");
47 
48 //    private static final LocaleMatchValue LOCALE_MATCH_VALUE = new LocaleMatchValue(ImmutableSet.of(
49 //        Validity.Status.regular,
50 //        Validity.Status.special,
51 //        Validity.Status.unknown)
52 //        );
53 
54     /**
55      * Contains all CLDR locales, plus some special cases
56      */
57     private static final Set<String> CLDR_LOCALES_FOR_NAME_ORDER;
58     static {
59         Set<String> valid = new HashSet<>();
60         valid.addAll(CLDRConfig.getInstance().getCldrFactory().getAvailable());
61         valid.add("zxx");
62         valid.add("und");
63         CLDR_LOCALES_FOR_NAME_ORDER = ImmutableSet.copyOf(valid);
64     }
65 
66     private static final ImmutableSet<Modifier> SINGLE_CORE = ImmutableSet.of(Modifier.core);
67     private static final ImmutableSet<Modifier> SINGLE_PREFIX = ImmutableSet.of(Modifier.prefix);
68     private static final ImmutableSet<Modifier> CORE_AND_PREFIX = ImmutableSet.of(Modifier.prefix, Modifier.core);
69 
70     @Override
handleCheck(String path, String fullPath, String value, Options options, List<CheckStatus> result)71     public CheckCLDR handleCheck(String path, String fullPath, String value, Options options,
72         List<CheckStatus> result) {
73         if (value == null
74             || path.endsWith("/alias")
75             || SKIP_PATH_LIST.matcher(path).matches()) {
76             return this;
77         }
78 
79         if (path.contains("/personNames")) {
80             XPathParts parts = XPathParts.getFrozenInstance(path);
81             switch(parts.getElement(2)) {
82             default:
83                 break;// skip to rest of handleCheck
84             case "initialPattern":
85                 checkInitialPattern(this, path, value, result);
86                 break;// skip to rest of handleCheck
87             case "foreignSpaceReplacement":
88                 checkForeignSpaceReplacement(this, value, result);
89                 return this;
90             case "nameOrderLocales":
91                 checkNameOrder(this, path, value, result);
92                 return this;
93             case "sampleName":
94                 checkSampleNames(this, parts, value, result);
95                 return this;
96             case "personName":
97                 checkPersonNamePatterns(this, path, parts, value, result);
98                 return this;
99             }
100             // done with person names
101             // note: depending on the switch value, may fall through
102         }
103 
104         checkBasicPlaceholders(value, result);
105         checkListPatterns(path, value, result);
106         return this;
107     }
108 
109 
110     /**
111      * Verify the that nameOrder items are clean.
112      */
checkNameOrder(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)113     public static void checkNameOrder(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) {
114         //ldml/personNames/nameOrderLocales[@order="givenFirst"]
115         final String localeID = checkAccessor.getLocaleID();
116         Set<String> items = new TreeSet<>();
117         Set<String> orderErrors = checkForErrorsAndGetLocales(localeID, value, items);
118         if (orderErrors != null) {
119             result.add(new CheckStatus().setCause(checkAccessor)
120                 .setMainType(CheckStatus.errorType)
121                 .setSubtype(Subtype.invalidLocale)
122                 .setMessage("Invalid locales: " + JOIN_SPACE.join(orderErrors)));
123             return;
124         }
125         // Check to see that user's language and und are explicitly mentioned.
126         // but only if the value is not inherited.
127         String unresolvedValue = checkAccessor.getUnresolvedStringValue(path);
128         if (unresolvedValue != null) {
129             // And the other value is not inherited.
130             String otherPath = path.contains("givenFirst")
131                 ? path.replace("givenFirst", "surnameFirst")
132                     : path.replace("surnameFirst", "givenFirst");
133             String otherValue = checkAccessor.getStringValue(otherPath);
134             if (otherValue != null) {
135                 String myLanguage = localeID;
136                 if (!myLanguage.equals("root")) { // skip root
137 
138                     Set<String> items2 = new TreeSet<>();
139                     orderErrors = checkForErrorsAndGetLocales(localeID, otherValue, items2); // adds locales from other path. We don't check for errors there.
140                     if (!Collections.disjoint(items, items2)) {
141                         result.add(new CheckStatus().setCause(checkAccessor)
142                             .setMainType(CheckStatus.errorType)
143                             .setSubtype(Subtype.invalidLocale)
144                             .setMessage("Locale codes can occur only once: " + JOIN_SPACE.join(Sets.intersection(items, items2))));
145                     }
146 
147                     items.addAll(items2); // get the union for checking below
148                     myLanguage = new LocaleIDParser().set(myLanguage).getLanguage();
149 
150                     if (!items.contains(myLanguage)) {
151                         result.add(new CheckStatus().setCause(checkAccessor)
152                             .setMainType(CheckStatus.errorType)
153                             .setSubtype(Subtype.missingLanguage)
154                             .setMessage("Your locale code (" + myLanguage
155                                 + ") must be explicitly listed in one of the nameOrderLocales:"
156                                 + " either in givenFirst or in surnameFirst."));
157                     }
158 
159                     if (!items.contains("und")) {
160                         result.add(new CheckStatus().setCause(checkAccessor)
161                             .setMainType(CheckStatus.errorType)
162                             .setSubtype(Subtype.missingLanguage)
163                             .setMessage("The special code ‘und’ must be explicitly listed in one of the nameOrderLocales: either givenFirst or surnameFirst."));
164                     }
165                 }
166             }
167         }
168     }
169 
170     /**
171      * Verify the that sampleName items are clean.
172      * @param checkAccessor
173      */
checkSampleNames(CheckAccessor checkAccessor, XPathParts pathParts, String value, List<CheckStatus> result)174     public static void checkSampleNames(CheckAccessor checkAccessor, XPathParts pathParts, String value, List<CheckStatus> result) {
175         //ldml/personNames/sampleName[@item="informal"]/nameField[@type="surname"]
176 
177         // check basic consistency of modifier set
178         ModifiedField fieldType = ModifiedField.from(pathParts.getAttributeValue(-1, "type"));
179         Field field = fieldType.getField();
180         Set<Modifier> modifiers = fieldType.getModifiers();
181         Output<String> errorMessage = new Output<>();
182         Modifier.getCleanSet(modifiers, errorMessage);
183         if (errorMessage.value != null) {
184             result.add(new CheckStatus().setCause(checkAccessor)
185                 .setMainType(CheckStatus.warningType)
186                 .setSubtype(Subtype.invalidPlaceHolder)
187                 .setMessage(errorMessage.value));
188             return;
189         }
190 
191         if (value.equals("∅∅∅")) {
192             // check for required values
193 
194             switch(field) {
195             case given:
196                 // we must have a given
197                 if (fieldType.getModifiers().isEmpty()) {
198                     result.add(new CheckStatus().setCause(checkAccessor)
199                         .setMainType(CheckStatus.warningType)
200                         .setSubtype(Subtype.invalidPlaceHolder)
201                         .setMessage("Names must have a value for the ‘given‘ field. Mononyms (like ‘Lady Gaga’) use given, not surname"));
202                 }
203                 break;
204             case surname:
205                 // can't have surname2 unless we have surname
206                 String modPath = pathParts.cloneAsThawed().setAttribute(-1, "type", Field.surname2.toString()).toString();
207                 String surname2Value = checkAccessor.getStringValue(modPath);
208                 if (surname2Value != null && !surname2Value.equals("∅∅∅")) {
209                     result.add(new CheckStatus().setCause(checkAccessor)
210                         .setMainType(CheckStatus.errorType)
211                         .setSubtype(Subtype.invalidPlaceHolder)
212                         .setMessage("Names must have a value for the ‘surname’ field if they have a ‘surname2’ field."));
213                 }
214                 break;
215             default:
216                 break;
217             }
218         } else if (value.equals("zxx")) { // mistaken "we don't use this"
219             result.add(new CheckStatus().setCause(checkAccessor)
220                 .setMainType(CheckStatus.errorType)
221                 .setSubtype(Subtype.invalidPlaceHolder)
222                 .setMessage("Illegal name field; zxx is only appropriate for NameOrder locales"));
223         } else { // real value
224             // special checks for prefix/core
225             final boolean hasPrefix = modifiers.contains(Modifier.prefix);
226             final boolean hasCore = modifiers.contains(Modifier.core);
227             if (hasPrefix || hasCore) {
228                 // We need consistency among the 3 values if we have either prefix or core
229 
230                 String coreValue = hasCore ? value : modifiedFieldValue(checkAccessor, pathParts, field, modifiers, Modifier.core);
231                 String prefixValue = hasPrefix ? value : modifiedFieldValue(checkAccessor, pathParts, field, modifiers, Modifier.prefix);
232                 String plainValue = modifiedFieldValue(checkAccessor, pathParts, field, modifiers, null);
233 
234                 String errorMessage2 = Modifier.inconsistentPrefixCorePlainValues(prefixValue, coreValue, plainValue);
235                 if (errorMessage2 != null) {
236                     result.add(new CheckStatus().setCause(checkAccessor)
237                         .setMainType(CheckStatus.errorType)
238                         .setSubtype(Subtype.invalidPlaceHolder)
239                         .setMessage(errorMessage2));
240                 }
241             }
242         }
243     }
244 
245     static final ImmutableSet<Object> givenFirstSortingLocales = ImmutableSet.of("is", "ta", "si"); // TODO should be data-driven
246 
247     /**
248      * Verify the that personName patterns are clean.
249      * @param path TODO
250      */
checkPersonNamePatterns(CheckAccessor checkAccessor, String path, XPathParts pathParts, String value, List<CheckStatus> result)251     public static void checkPersonNamePatterns(CheckAccessor checkAccessor, String path, XPathParts pathParts, String value, List<CheckStatus> result) {
252         //ldml/personNames/personName[@order="sorting"][@length="long"][@usage="addressing"][@style="formal"]/namePattern
253 
254         // check that the name pattern is valid
255 
256         Pair<FormatParameters, NamePattern> pair = null;
257         try {
258             pair = PersonNameFormatter.fromPathValue(pathParts, value);
259         } catch (Exception e) {
260             result.add(new CheckStatus().setCause(checkAccessor)
261                 .setMainType(CheckStatus.errorType)
262                 .setSubtype(Subtype.invalidPlaceHolder)
263                 .setMessage("Invalid placeholder in value: \"" + value + "\""));
264             return; // fatal error, don't bother with others
265         }
266 
267         final FormatParameters parameterMatcher = pair.getFirst();
268         final NamePattern namePattern = pair.getSecond();
269 
270         // now check that the namePattern is reasonable
271 
272         Multimap<Field, Integer> fieldToPositions = namePattern.getFieldPositions();
273 
274         // Check for special cases: https://unicode-org.atlassian.net/browse/CLDR-15782
275 
276         boolean usageIsMonogram = parameterMatcher.matches(new FormatParameters(null, null, Usage.monogram, null));
277 
278         ModifiedField lastModifiedField = null;
279         for (int i = 0; i < namePattern.getElementCount(); ++i) {
280             ModifiedField modifiedField = namePattern.getModifiedField(i);
281             if (modifiedField == null) { // literal
282                 String literal = namePattern.getLiteral(i);
283                 if (literal.contains(".")) {
284                     if (lastModifiedField != null) {
285                         Set<Modifier> lastModifiers = lastModifiedField.getModifiers();
286                         if (lastModifiers.contains(Modifier.initial) && lastModifiers.contains(Modifier.initialCap)) {
287                             result.add(new CheckStatus().setCause(checkAccessor)
288                                 .setMainType(CheckStatus.warningType)
289                                 .setSubtype(Subtype.namePlaceholderProblem)
290                                 .setMessage("“.” is strongly discouraged after an -initial or -initialCap placeholder in {" + lastModifiedField + "}"));
291                             continue;
292                         }
293                     }
294                     if (usageIsMonogram) {
295                         result.add(new CheckStatus().setCause(checkAccessor)
296                             .setMainType(CheckStatus.warningType)
297                             .setSubtype(Subtype.namePlaceholderProblem)
298                             .setMessage("“.” is discouraged when usage=monogram, in " + namePattern));
299                     }
300                 }
301             } else {
302                 lastModifiedField = modifiedField;
303                 Set<Modifier> modifiers = modifiedField.getModifiers();
304                 Field field = modifiedField.getField();
305                 switch (field) {
306                 case prefix: case suffix:
307                     if (usageIsMonogram) {
308                         result.add(new CheckStatus().setCause(checkAccessor)
309                             .setMainType(CheckStatus.errorType)
310                             .setSubtype(Subtype.invalidPlaceHolder)
311                             .setMessage("Disallowed when usage=monogram: {" + field + "…}"));
312                     }
313                     break;
314                 default:
315                     final boolean monogramModifier = modifiers.contains(Modifier.monogram);
316                     final boolean allCapsModifier = modifiers.contains(Modifier.allCaps);
317                     if (!usageIsMonogram) {
318                         if (monogramModifier) {
319                             result.add(new CheckStatus().setCause(checkAccessor)
320                                 .setMainType(CheckStatus.warningType)
321                                 .setSubtype(Subtype.invalidPlaceHolder)
322                                 .setMessage("-monogram is strongly discouraged when usage≠monogram, in {" + modifiedField + "}"));
323                         }
324                     } else if (usageIsMonogram) {
325                         if (!monogramModifier) {
326                             result.add(new CheckStatus().setCause(checkAccessor)
327                                 .setMainType(CheckStatus.errorType)
328                                 .setSubtype(Subtype.invalidPlaceHolder)
329                                 .setMessage("-monogram is required when usage=monogram, in {" + modifiedField + "}"));
330                         } else if (!allCapsModifier) {
331                             result.add(new CheckStatus().setCause(checkAccessor)
332                                 .setMainType(CheckStatus.warningType)
333                                 .setSubtype(Subtype.invalidPlaceHolder)
334                                 .setMessage("-allCaps is strongly encouraged with -monogram, in {" + modifiedField + "}"));
335                         }
336                     }
337                 }
338             }
339             lastModifiedField = modifiedField;
340         }
341 
342         // gather information about the fields
343         int firstSurname = Integer.MAX_VALUE;
344         int firstGiven = Integer.MAX_VALUE;
345 
346         // TODO ALL check for combinations we should enforce; eg, only have given2 if there is a given; only have surname2 if there is a surname; others?
347 
348         for (Entry<Field, Collection<Integer>> entry : fieldToPositions.asMap().entrySet()) {
349 
350             // If a field occurs twice, probably an error. Could relax this upon feedback
351 
352             Collection<Integer> positions = entry.getValue();
353             if (positions.size() > 1) {
354 
355                 // However, do allow prefix&core together
356 
357                 boolean skip = false;
358                 if (entry.getKey() == Field.surname) {
359                     Iterator<Integer> it = positions.iterator();
360                     Set<Modifier> m1 = namePattern.getModifiedField(it.next()).getModifiers();
361                     Set<Modifier> m2 = namePattern.getModifiedField(it.next()).getModifiers();
362                     skip = m1.contains(Modifier.core) && m2.contains(Modifier.prefix)
363                         || m1.contains(Modifier.prefix) && m2.contains(Modifier.core);
364                 }
365 
366                 if (!skip) {
367                     result.add(new CheckStatus().setCause(checkAccessor)
368                         .setMainType(CheckStatus.errorType)
369                         .setSubtype(Subtype.invalidPlaceHolder)
370                         .setMessage("Duplicate fields: " + entry));
371                 }
372             }
373 
374             // gather some info for later
375 
376             Integer leastPosition = positions.iterator().next();
377             switch (entry.getKey()) {
378             case given: case given2:
379                 firstGiven = Math.min(leastPosition, firstGiven);
380                 break;
381             case surname: case surname2:
382                 firstSurname = Math.min(leastPosition, firstSurname);
383                 break;
384             default: // ignore
385             }
386         }
387 
388         // the rest of the tests are of the pattern, and only apply when we have both given and surname
389         // and not inheriting
390 
391         if (firstGiven < Integer.MAX_VALUE && firstSurname < Integer.MAX_VALUE
392             && checkAccessor.getUnresolvedStringValue(path) != null) {
393 
394             Order orderRaw = parameterMatcher.getOrder();
395             Set<Order> order = orderRaw == null ? Order.ALL : ImmutableSet.of(orderRaw);
396             // TODO, fix to avoid set (a holdover from using PatternMatcher)
397 
398             // Handle 'sorting' value. Will usually be compatible with surnameFirst in foundOrder, except for known exceptions
399 
400             if (order.contains(Order.sorting)) {
401                 EnumSet<Order> temp = EnumSet.noneOf(Order.class);
402                 temp.addAll(order);
403                 temp.remove(Order.sorting);
404                 if (givenFirstSortingLocales.contains(checkAccessor.getLocaleID())) { // TODO Mark cover contains-by-inheritance also
405                     temp.add(Order.givenFirst);
406                 } else {
407                     temp.add(Order.surnameFirst);
408                 }
409                 order = temp;
410             }
411 
412             if (order.isEmpty()) {
413                 order = Order.ALL;
414             }
415 
416             // check that we don't have a difference in the order AND there is a surname or surname2
417             // that is, it is ok to coalesce patterns of different orders where the order doesn't make a difference
418 
419             { // TODO: clean up to avoid block
420 
421                 if(order.contains(Order.givenFirst)
422                     && order.contains(Order.surnameFirst)
423                     ) {
424                     result.add(new CheckStatus().setCause(checkAccessor)
425                         .setMainType(CheckStatus.errorType)
426                         .setSubtype(Subtype.invalidPlaceHolder)
427                         .setMessage("Conflicting Order values: " + order));
428                 }
429 
430                 // now check order in pattern is consistent with Order
431 
432                 Order foundOrder = firstGiven < firstSurname ? Order.givenFirst : Order.surnameFirst;
433                 final Order first = order.iterator().next();
434 
435                 if (first != foundOrder) {
436 
437 //                    if (first == Order.givenFirst && !"en".equals(checkAccessor.getLocaleID())) { // TODO Mark Drop HACK once root is ok
438 //                        return;
439 //                    }
440 
441                     result.add(new CheckStatus().setCause(checkAccessor)
442                         .setMainType(CheckStatus.errorType)
443                         .setSubtype(Subtype.invalidPlaceHolder)
444                         .setMessage("Pattern order {0} is inconsistent with code order {1}", foundOrder, first));
445                 }
446             }
447         }
448     }
449 
450     /**
451      * Check that {\d+} placeholders are ok; no unterminated, only digits
452      */
453     private void checkBasicPlaceholders(String value, List<CheckStatus> result) {
454         int startPlaceHolder = 0;
455         int endPlaceHolder;
456         while (startPlaceHolder != -1 && startPlaceHolder < value.length()) {
457             startPlaceHolder = value.indexOf('{', startPlaceHolder + 1);
458             if (startPlaceHolder != -1) {
459                 endPlaceHolder = value.indexOf('}', startPlaceHolder + 1);
460                 if (endPlaceHolder == -1) {
461                     result.add(new CheckStatus().setCause(this)
462                         .setMainType(CheckStatus.errorType)
463                         .setSubtype(Subtype.invalidPlaceHolder)
464                         .setMessage("Invalid placeholder (missing terminator) in value \"" + value + "\""));
465                 } else {
466                     String placeHolderString = value.substring(startPlaceHolder + 1, endPlaceHolder);
467                     Matcher matcher = PLACEHOLDER_PATTERN.matcher(placeHolderString);
468                     if (!matcher.matches()) {
469                         result.add(new CheckStatus().setCause(this)
470                             .setMainType(CheckStatus.errorType)
471                             .setSubtype(Subtype.invalidPlaceHolder)
472                             .setMessage("Invalid placeholder (contents \"" + placeHolderString + "\") in value \"" + value + "\""));
473                     }
474                     startPlaceHolder = endPlaceHolder;
475                 }
476             }
477         }
478     }
479 
480     /**
481      * Check that list patterns are "ordered" so that they only compose from the right.
482      */
483 
484     private void checkListPatterns(String path, String value, List<CheckStatus> result) {
485         // eg
486         //ldml/listPatterns/listPattern/listPatternPart[@type="start"]
487         //ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"]
488         if (path.startsWith("//ldml/listPatterns/listPattern")) {
489             XPathParts parts = XPathParts.getFrozenInstance(path);
490             // check order, {0} must be before {1}
491 
492             switch(parts.getAttributeValue(-1, "type")) {
493             case "start":
494                 checkNothingAfter1(value, result);
495                 break;
496             case "middle":
497                 checkNothingBefore0(value, result);
498                 checkNothingAfter1(value, result);
499                 break;
500             case "end":
501                 checkNothingBefore0(value, result);
502                 break;
503             case "2": {
504                 int pos1 = value.indexOf("{0}");
505                 int pos2 = value.indexOf("{1}");
506                 if (pos1 > pos2) {
507                     result.add(new CheckStatus().setCause(this)
508                         .setMainType(CheckStatus.errorType)
509                         .setSubtype(Subtype.invalidPlaceHolder)
510                         .setMessage("Invalid list pattern «" + value + "»: the placeholder {0} must be before {1}."));
511                 }}
512             break;
513             case "3": {
514                 int pos1 = value.indexOf("{0}");
515                 int pos2 = value.indexOf("{1}");
516                 int pos3 = value.indexOf("{2}");
517                 if (pos1 > pos2 || pos2 > pos3) {
518                     result.add(new CheckStatus().setCause(this)
519                         .setMainType(CheckStatus.errorType)
520                         .setSubtype(Subtype.invalidPlaceHolder)
521                         .setMessage("Invalid list pattern «" + value + "»: the placeholders {0}, {1}, {2} must appear in that order."));
522                 }}
523             break;
524             }
525         }
526     }
527 
528     /**
529      * Check that both patterns don't have the same literals.
530      */
checkInitialPattern(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)531     public static void checkInitialPattern(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) {
532         if (path.contains("initialSequence")) {
533             String valueLiterals = value.replace("{0}", "").replace("{1}", "");
534             if (!valueLiterals.isBlank()) {
535                 String otherPath = path.replace("initialSequence", "initial");
536                 String otherValue = checkAccessor.getStringValue(otherPath);
537                 if (otherValue != null) {
538                     String literals = otherValue.replace("{0}", "");
539                     if (!literals.isBlank() && value.contains(literals)) {
540                         result.add(new CheckStatus().setCause(checkAccessor)
541                             .setMainType(CheckStatus.errorType)
542                             .setSubtype(Subtype.namePlaceholderProblem)
543                             .setMessage("The initialSequence pattern must not contain initial pattern literals: «" + literals + "»"));
544                         return;
545                     }
546                 }
547                 result.add(new CheckStatus().setCause(checkAccessor)
548                     .setMainType(CheckStatus.warningType)
549                     .setSubtype(Subtype.namePlaceholderProblem)
550                     .setMessage("Non-space characters are discouraged in the initialSequence pattern: «" + valueLiterals.replace(" ", "") + "»"));
551             }
552         }
553         // no current check for the type="initial"
554     }
555 
556     static final UnicodeSet allowedForeignSpaceReplacements = new UnicodeSet("[[:whitespace:][:punctuation:]]");
557 
558     /**
559      * Check that the value is limited to punctuation or space, or inherits
560      */
checkForeignSpaceReplacement(CheckAccessor checkAccessor, String value, List<CheckStatus> result)561     public static void checkForeignSpaceReplacement(CheckAccessor checkAccessor, String value, List<CheckStatus> result) {
562         if (!allowedForeignSpaceReplacements.containsAll(value) && !value.equals("↑↑↑")) {
563             result.add(new CheckStatus().setCause(checkAccessor)
564                 .setMainType(CheckStatus.errorType)
565                 .setSubtype(Subtype.invalidLocale)
566                 .setMessage("Invalid choice, must be punctuation or a space: «" + value + "»"));
567         }
568     }
569 
570 
571     /**
572      * Gets a string value for a modified path
573      */
modifiedFieldValue(CheckAccessor checkAccessor, XPathParts parts, Field field, Set<Modifier> modifiers, Modifier toAdd)574     private static String modifiedFieldValue(CheckAccessor checkAccessor, XPathParts parts, Field field, Set<Modifier> modifiers, Modifier toAdd) {
575         Set<Modifier> adjustedModifiers = Sets.difference(modifiers, CORE_AND_PREFIX);
576         if (toAdd != null) {
577             switch (toAdd) {
578             case core:
579                 adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_CORE);
580                 break;
581             case prefix:
582                 adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_PREFIX);
583                 break;
584             default:
585                 break;
586             }
587         }
588         String modPath  = parts.cloneAsThawed().setAttribute(-1, "type", new ModifiedField(field, adjustedModifiers).toString()).toString();
589         String value = checkAccessor.getStringValue(modPath);
590         return "∅∅∅".equals(value) ? null : value;
591     }
592 
checkForErrorsAndGetLocales(String locale, String value, Set<String> items)593     public static Set<String> checkForErrorsAndGetLocales(String locale, String value, Set<String> items) {
594         if (value.isEmpty()) {
595             return null;
596         }
597         Set<String> orderErrors = null;
598         for (String item : SPLIT_SPACE.split(value)) {
599             boolean mv = (item.equals(locale))
600                || CLDR_LOCALES_FOR_NAME_ORDER.contains(item);
601             if (!mv) {
602                 if (orderErrors == null) {
603                     orderErrors = new LinkedHashSet<>();
604                 }
605                 orderErrors.add(item);
606             } else {
607                 items.add(item);
608             }
609         }
610         return orderErrors;
611     }
612 
checkNothingAfter1(String value, List<CheckStatus> result)613     private void checkNothingAfter1(String value, List<CheckStatus> result) {
614         if (!value.endsWith("{1}")) {
615             result.add(new CheckStatus().setCause(this)
616                 .setMainType(CheckStatus.errorType)
617                 .setSubtype(Subtype.invalidPlaceHolder)
618                 .setMessage("Invalid list pattern «" + value + "», no text can come after {1}."));
619         }
620 
621     }
622 
checkNothingBefore0(String value, List<CheckStatus> result)623     private void checkNothingBefore0(String value, List<CheckStatus> result) {
624         if (!value.startsWith("{0}")) {
625             result.add(new CheckStatus().setCause(this)
626                 .setMainType(CheckStatus.errorType)
627                 .setSubtype(Subtype.invalidPlaceHolder)
628                 .setMessage("Invalid list pattern «" + value + "», no text can come before {0}."));
629         }
630     }
631 }
632