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