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 if (parts.containsElement("alias")) return; // skip alias XPaths 670 // check order, {0} must be before {1} 671 672 switch (parts.getAttributeValue(-1, "type")) { 673 case "start": 674 checkNothingAfter1(value, result); 675 break; 676 case "middle": 677 checkNothingBefore0(value, result); 678 checkNothingAfter1(value, result); 679 break; 680 case "end": 681 checkNothingBefore0(value, result); 682 break; 683 case "2": 684 { 685 int pos1 = value.indexOf("{0}"); 686 int pos2 = value.indexOf("{1}"); 687 if (pos1 > pos2) { 688 result.add( 689 new CheckStatus() 690 .setCause(this) 691 .setMainType(CheckStatus.errorType) 692 .setSubtype(Subtype.invalidPlaceHolder) 693 .setMessage( 694 "Invalid list pattern «" 695 + value 696 + "»: the placeholder {0} must be before {1}.")); 697 } 698 } 699 break; 700 case "3": 701 { 702 int pos1 = value.indexOf("{0}"); 703 int pos2 = value.indexOf("{1}"); 704 int pos3 = value.indexOf("{2}"); 705 if (pos1 > pos2 || pos2 > pos3) { 706 result.add( 707 new CheckStatus() 708 .setCause(this) 709 .setMainType(CheckStatus.errorType) 710 .setSubtype(Subtype.invalidPlaceHolder) 711 .setMessage( 712 "Invalid list pattern «" 713 + value 714 + "»: the placeholders {0}, {1}, {2} must appear in that order.")); 715 } 716 } 717 break; 718 } 719 } 720 } 721 722 /** Check that both patterns don't have the same literals. */ checkInitialPattern( CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)723 public static void checkInitialPattern( 724 CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) { 725 if (path.contains("initialSequence")) { 726 String valueLiterals = value.replace("{0}", "").replace("{1}", ""); 727 if (!valueLiterals.isBlank()) { 728 String otherPath = path.replace("initialSequence", "initial"); 729 String otherValue = checkAccessor.getStringValue(otherPath); 730 if (otherValue != null) { 731 String literals = otherValue.replace("{0}", ""); 732 if (!literals.isBlank() && value.contains(literals)) { 733 result.add( 734 new CheckStatus() 735 .setCause(checkAccessor) 736 .setMainType(CheckStatus.errorType) 737 .setSubtype(Subtype.namePlaceholderProblem) 738 .setMessage( 739 "The initialSequence pattern must not contain initial pattern literals: «" 740 + literals 741 + "»")); 742 return; 743 } 744 } 745 result.add( 746 new CheckStatus() 747 .setCause(checkAccessor) 748 .setMainType(CheckStatus.warningType) 749 .setSubtype(Subtype.namePlaceholderProblem) 750 .setMessage( 751 "Non-space characters are discouraged in the initialSequence pattern: «" 752 + valueLiterals.replace(" ", "") 753 + "»")); 754 } 755 } 756 // no current check for the type="initial" 757 } 758 759 static final UnicodeSet allowedForeignSpaceReplacements = 760 new UnicodeSet("[[:whitespace:][:punctuation:]]"); 761 762 /** Check that the value is limited to punctuation or space, or inherits */ checkForeignSpaceReplacement( CheckAccessor checkAccessor, String value, List<CheckStatus> result)763 public static void checkForeignSpaceReplacement( 764 CheckAccessor checkAccessor, String value, List<CheckStatus> result) { 765 if (!allowedForeignSpaceReplacements.containsAll(value) && !value.equals("↑↑↑")) { 766 result.add( 767 new CheckStatus() 768 .setCause(checkAccessor) 769 .setMainType(CheckStatus.errorType) 770 .setSubtype(Subtype.invalidLocale) 771 .setMessage( 772 "Invalid choice, must be punctuation or a space: «" 773 + value 774 + "»")); 775 } 776 } 777 778 /** Gets a string value for a modified path */ modifiedFieldValue( CheckAccessor checkAccessor, XPathParts parts, Field field, Set<Modifier> modifiers, Modifier toAdd)779 private static String modifiedFieldValue( 780 CheckAccessor checkAccessor, 781 XPathParts parts, 782 Field field, 783 Set<Modifier> modifiers, 784 Modifier toAdd) { 785 Set<Modifier> adjustedModifiers = Sets.difference(modifiers, CORE_AND_PREFIX); 786 if (toAdd != null) { 787 switch (toAdd) { 788 case core: 789 adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_CORE); 790 break; 791 case prefix: 792 adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_PREFIX); 793 break; 794 default: 795 break; 796 } 797 } 798 String modPath = 799 parts.cloneAsThawed() 800 .setAttribute( 801 -1, "type", new ModifiedField(field, adjustedModifiers).toString()) 802 .toString(); 803 String value = checkAccessor.getStringValue(modPath); 804 return "∅∅∅".equals(value) ? null : value; 805 } 806 checkForErrorsAndGetLocales( String locale, String value, Set<String> items)807 public static Set<String> checkForErrorsAndGetLocales( 808 String locale, String value, Set<String> items) { 809 if (value.isEmpty()) { 810 return null; 811 } 812 Set<String> orderErrors = null; 813 for (String item : SPLIT_SPACE.split(value)) { 814 boolean mv = (item.equals(locale)) || CLDR_LOCALES_FOR_NAME_ORDER.contains(item); 815 if (!mv) { 816 if (orderErrors == null) { 817 orderErrors = new LinkedHashSet<>(); 818 } 819 orderErrors.add(item); 820 } else { 821 items.add(item); 822 } 823 } 824 return orderErrors; 825 } 826 checkNothingAfter1(String value, List<CheckStatus> result)827 private void checkNothingAfter1(String value, List<CheckStatus> result) { 828 if (!value.endsWith("{1}")) { 829 result.add( 830 new CheckStatus() 831 .setCause(this) 832 .setMainType(CheckStatus.errorType) 833 .setSubtype(Subtype.invalidPlaceHolder) 834 .setMessage( 835 "Invalid list pattern «" 836 + value 837 + "», no text can come after {1}.")); 838 } 839 } 840 checkNothingBefore0(String value, List<CheckStatus> result)841 private void checkNothingBefore0(String value, List<CheckStatus> result) { 842 if (!value.startsWith("{0}")) { 843 result.add( 844 new CheckStatus() 845 .setCause(this) 846 .setMainType(CheckStatus.errorType) 847 .setSubtype(Subtype.invalidPlaceHolder) 848 .setMessage( 849 "Invalid list pattern «" 850 + value 851 + "», no text can come before {0}.")); 852 } 853 } 854 } 855