1 package org.unicode.cldr.util; 2 3 import java.util.Collection; 4 import java.util.Collections; 5 import java.util.EnumMap; 6 import java.util.EnumSet; 7 import java.util.LinkedHashMap; 8 import java.util.LinkedHashSet; 9 import java.util.List; 10 import java.util.Locale; 11 import java.util.Map; 12 import java.util.Map.Entry; 13 import java.util.Objects; 14 import java.util.Set; 15 import java.util.TreeMap; 16 import java.util.TreeSet; 17 import java.util.regex.Pattern; 18 19 import org.unicode.cldr.util.LanguageInfo.CldrDir; 20 import org.unicode.cldr.util.StandardCodes.LstrType; 21 import org.unicode.cldr.util.SupplementalDataInfo.AttributeValidityInfo; 22 23 import com.google.common.base.Splitter; 24 import com.google.common.collect.ComparisonChain; 25 import com.ibm.icu.impl.Relation; 26 import com.ibm.icu.impl.Row; 27 import com.ibm.icu.impl.Row.R2; 28 import com.ibm.icu.impl.Row.R3; 29 import com.ibm.icu.text.UnicodeSet; 30 import com.ibm.icu.util.ICUException; 31 import com.ibm.icu.util.Output; 32 33 public class AttributeValueValidity { 34 35 public enum Status { 36 ok, deprecated, illegal, noTest 37 } 38 39 public enum LocaleSpecific { 40 pluralCardinal, pluralOrdinal, dayPeriodFormat, dayPeriodSelection 41 } 42 43 static final Splitter BAR = Splitter.on('|').trimResults().omitEmptyStrings(); 44 static final Splitter SPACE = Splitter.on(PatternCache.get("\\s+")).trimResults().omitEmptyStrings(); 45 46 private static final Set<DtdType> ALL_DTDs = Collections.unmodifiableSet(EnumSet.allOf(DtdType.class)); 47 48 private static final SupplementalDataInfo supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo(); 49 50 private static Map<DtdType, Map<String, Map<String, MatcherPattern>>> dtd_element_attribute_validity = new EnumMap<>(DtdType.class); 51 private static Map<String, MatcherPattern> common_attribute_validity = new LinkedHashMap<String, MatcherPattern>(); 52 private static Map<String, MatcherPattern> variables = new LinkedHashMap<String, MatcherPattern>(); 53 private static final RegexMatcher NOT_DONE_YET = new RegexMatcher(".*", Pattern.COMMENTS); 54 private static final Map<AttributeValidityInfo, String> failures = new LinkedHashMap<>(); 55 private static final boolean DEBUG = false; 56 57 static { 58 59 Relation<R2<String, String>, String> bcp47Aliases = supplementalData.getBcp47Aliases(); 60 Set<String> bcp47Keys = new LinkedHashSet<>(); 61 Set<String> bcp47Values = new LinkedHashSet<>(); 62 for (Entry<String, Set<String>> keyValues : supplementalData.getBcp47Keys().keyValuesSet()) { 63 Set<String> fullValues = new TreeSet<>(); 64 String key = keyValues.getKey(); 65 bcp47Keys.add(key); 66 67 Set<String> rawValues = keyValues.getValue(); 68 69 for (String value : rawValues) { 70 if (key.equals("cu")) { // Currency codes are in upper case. 71 fullValues.add(value.toUpperCase()); 72 } else { 73 fullValues.add(value); 74 } 75 R2<String, String> keyValue = R2.of(key, value); 76 Set<String> aliases = bcp47Aliases.getAll(keyValue); 77 if (aliases != null) { 78 fullValues.addAll(aliases); 79 } 80 } 81 // Special case exception for generic calendar, since we don't want to expose it in bcp47 82 if (key.equals("ca")) { 83 fullValues.add("generic"); 84 } 85 fullValues = Collections.unmodifiableSet(fullValues); 86 addCollectionVariable("$_bcp47_" + key, fullValues); 87 88 // add aliased keys 89 Set<String> aliases = supplementalData.getBcp47Aliases().getAll(Row.of(key, "")); 90 if (aliases != null) { 91 for (String aliasKey : aliases) { 92 bcp47Keys.add(aliasKey); 93 addCollectionVariable("$_bcp47_" + aliasKey, fullValues); 94 } 95 } 96 bcp47Values.addAll(fullValues); 97 } 98 bcp47Keys.add("x"); // special-case private use 99 bcp47Keys.add("x0"); // special-case, has no subtypes 100 addCollectionVariable("$_bcp47_keys", bcp47Keys); 101 addCollectionVariable("$_bcp47_value", bcp47Values); 102 103 Validity validity = Validity.getInstance(); 104 for (LstrType key : LstrType.values()) { 105 final Map<Validity.Status, Set<String>> statusToCodes = validity.getStatusToCodes(key); 106 if (statusToCodes == null) { 107 continue; 108 } 109 String keyName = "$_" + key; 110 Set<String> all = new LinkedHashSet<>(); 111 Set<String> prefix = new LinkedHashSet<>(); 112 Set<String> suffix = new LinkedHashSet<>(); 113 Set<String> regularAndUnknown = new LinkedHashSet<>(); 114 for (Entry<Validity.Status, Set<String>> item2 : statusToCodes.entrySet()) { 115 Validity.Status status = item2.getKey(); 116 Set<String> validItems = item2.getValue(); 117 if (key == LstrType.variant) { // uppercased in CLDR 118 Set<String> temp2 = new LinkedHashSet<>(validItems); 119 for (String item : validItems) { 120 temp2.add(item.toUpperCase(Locale.ROOT)); 121 } 122 validItems = temp2; 123 } else if (key == LstrType.subdivision) { 124 for (String item : validItems) { 125 if (item.contains("-")) { 126 List<String> parts = Splitter.on('-').splitToList(item); 127 prefix.add(parts.get(0)); 128 suffix.add(parts.get(1)); 129 } else { 130 int prefixWidth = item.charAt(0) < 'A' ? 3 : 2; 131 prefix.add(item.substring(0, prefixWidth)); 132 suffix.add(item.substring(prefixWidth)); 133 } 134 } 135 } 136 all.addAll(validItems); 137 if (status == Validity.Status.regular || status == Validity.Status.special || status == Validity.Status.unknown) { 138 regularAndUnknown.addAll(validItems); 139 } 140 addCollectionVariable(keyName + "_" + status, validItems); 141 // MatcherPattern m = new MatcherPattern(key.toString(), validItems.toString(), new CollectionMatcher(validItems)); 142 // variables.put(keyName+"_"+status, m); 143 } 144 if (key == LstrType.subdivision) { 145 addCollectionVariable(keyName + "_prefix", prefix); 146 addCollectionVariable(keyName + "_suffix", suffix); 147 } 148 addCollectionVariable(keyName, all); 149 addCollectionVariable(keyName + "_plus", regularAndUnknown); 150 151 // MatcherPattern m = new MatcherPattern(key.toString(), all.toString(), new CollectionMatcher(all)); 152 // variables.put(keyName, m); 153 // MatcherPattern m2 = new MatcherPattern(key.toString(), regularAndUnknown.toString(), new CollectionMatcher(regularAndUnknown)); 154 // variables.put(keyName + "_plus", m2); 155 } 156 157 Set<String> main = new LinkedHashSet<>(); 158 main.addAll(StandardCodes.LstrType.language.specials); 159 Set<String> coverage = new LinkedHashSet<>(); 160 Set<String> large_official = new LinkedHashSet<>(); 161 final LocaleIDParser lip = new LocaleIDParser(); 162 163 for (String language : LanguageInfo.getAvailable()) { 164 LanguageInfo info = LanguageInfo.get(language); 165 CldrDir cldrDir = info.getCldrDir(); 166 String base = lip.set(language).getLanguage(); 167 if (cldrDir == CldrDir.main || cldrDir == CldrDir.base) { 168 main.add(base); 169 } 170 if (info.getCldrLevel() == Level.MODERN) { 171 coverage.add(base); 172 } 173 if (info.getLiteratePopulation() > 1000000 && !info.getStatusToRegions().isEmpty()) { 174 large_official.add(base); 175 } 176 } 177 addCollectionVariable("$_language_main", main); 178 addCollectionVariable("$_language_coverage", coverage); 179 addCollectionVariable("$_language_large_official", large_official); 180 Set<String> cldrLang = new TreeSet<>(main); 181 cldrLang.addAll(coverage); 182 cldrLang.addAll(large_official); 183 addCollectionVariable("$_language_cldr", large_official); 184 // System.out.println("\ncldrLang:\n" + Joiner.on(' ').join(cldrLang)); 185 186 Map<String, R2<String, String>> rawVariables = supplementalData.getValidityInfo(); 187 for (Entry<String, R2<String, String>> item : rawVariables.entrySet()) { 188 String id = item.getKey(); 189 String type = item.getValue().get0(); 190 String value = item.getValue().get1(); 191 MatcherPattern mp = getMatcherPattern2(type, value); 192 if (mp != null) { 193 variables.put(id, mp); 194 // variableReplacer.add(id, value); 195 } else { 196 throw new IllegalArgumentException("Duplicate element " + mp); 197 } 198 } 199 //System.out.println("Variables: " + variables.keySet()); 200 201 Map<AttributeValidityInfo, String> rawAttributeValueInfo = supplementalData.getAttributeValidity(); 202 int x = 0; 203 for (Entry<AttributeValidityInfo, String> entry : rawAttributeValueInfo.entrySet()) { 204 AttributeValidityInfo item = entry.getKey(); 205 String value = entry.getValue(); 206 //System.out.println(item); 207 MatcherPattern mp = getMatcherPattern2(item.getType(), value); 208 if (mp == null) { 209 getMatcherPattern2(item.getType(), value); // for debugging 210 failures.put(item, value); 211 continue; 212 } 213 Set<DtdType> dtds = item.getDtds(); 214 if (dtds == null) { 215 dtds = ALL_DTDs; 216 } 217 for (DtdType dtdType : dtds) { 218 DtdData data = DtdData.getInstance(dtdType); 219 Map<String, Map<String, MatcherPattern>> element_attribute_validity = dtd_element_attribute_validity.get(dtdType); 220 if (element_attribute_validity == null) { 221 dtd_element_attribute_validity.put(dtdType, element_attribute_validity = new TreeMap<String, Map<String, MatcherPattern>>()); 222 } 223 224 // <attributeValues dtds="supplementalData" elements="currency" attributes="before from to">$currencyDate</attributeValues> 225 226 Set<String> attributeList = item.getAttributes(); 227 Set<String> elementList = item.getElements(); 228 if (elementList.size() == 0) { 229 addAttributes(attributeList, common_attribute_validity, mp); 230 } else { 231 for (String element : elementList) { 232 // check if unnecessary 233 DtdData.Element elementInfo = data.getElementFromName().get(element); 234 if (elementInfo == null) { 235 throw new ICUException( 236 "Illegal <attributeValues>, element not valid: " 237 + dtdType 238 + ", element: " + element); 239 } else { 240 for (String attribute : attributeList) { 241 DtdData.Attribute attributeInfo = elementInfo.getAttributeNamed(attribute); 242 if (attributeInfo == null) { 243 throw new ICUException( 244 "Illegal <attributeValues>, attribute not valid: " 245 + dtdType 246 + ", element: " + element 247 + ", attribute: " + attribute); 248 } else if (!attributeInfo.values.isEmpty()) { 249 // if (false) { 250 // System.out.println("Unnecessary <attributeValues >, the DTD has specific list: element: " + element + ", attribute: " + attribute + ", " + attributeInfo.values); 251 // } 252 } 253 } 254 } 255 // System.out.println("\t" + element); 256 Map<String, MatcherPattern> attribute_validity = element_attribute_validity.get(element); 257 if (attribute_validity == null) { 258 element_attribute_validity.put(element, attribute_validity = new TreeMap<String, MatcherPattern>()); 259 } 260 addAttributes(attributeList, attribute_validity, mp); 261 } 262 } 263 } 264 } 265 // show values 266 // for (Entry<DtdType, Map<String, Map<String, MatcherPattern>>> entry1 : dtd_element_attribute_validity.entrySet()) { 267 // final DtdType dtdType = entry1.getKey(); 268 // Map<String, Map<String, MatcherPattern>> element_attribute_validity = entry1.getValue(); 269 // DtdData dtdData2 = DtdData.getInstance(dtdType); 270 // for (Element element : dtdData2.getElements()) { 271 // Set<Attribute> attributes = element.getAttributes().keySet(); 272 // 273 // } 274 // for (Entry<String, Map<String, MatcherPattern>> entry2 : entry1.getValue().entrySet()) { 275 // for (Entry<String, MatcherPattern> entry3 : entry2.getValue().entrySet()) { 276 // System.out.println(dtdType + "\t" + entry2.getKey() + "\t" + entry3.getKey() + "\t" + entry3.getValue()); 277 // } 278 // } 279 // } 280 281 // private LocaleIDParser localeIDParser = new LocaleIDParser(); 282 // 283 // @Override 284 // public CheckCLDR setCldrFileToCheck(CLDRFile cldrFileToCheck, Options options, 285 // List<CheckStatus> possibleErrors) { 286 // if (cldrFileToCheck == null) return this; 287 // if (Phase.FINAL_TESTING == getPhase() || Phase.BUILD == getPhase()) { 288 // setSkipTest(false); // ok 289 // } else { 290 // setSkipTest(true); 291 // return this; 292 // } 293 // 294 // pluralInfo = supplementalData.getPlurals(PluralType.cardinal, cldrFileToCheck.getLocaleID()); 295 // super.setCldrFileToCheck(cldrFileToCheck, options, possibleErrors); 296 // isEnglish = "en".equals(localeIDParser.set(cldrFileToCheck.getLocaleID()).getLanguage()); 297 // synchronized (elementOrder) { 298 // if (!initialized) { 299 // getMetadata(); 300 // initialized = true; 301 // localeMatcher = LocaleMatcher.make(); 302 // } 303 // } 304 // if (!localeMatcher.matches(cldrFileToCheck.getLocaleID())) { 305 // possibleErrors.add(new CheckStatus() 306 // .setCause(null).setMainType(CheckStatus.errorType).setSubtype(Subtype.invalidLocale) 307 // .setMessage("Invalid Locale {0}", 308 // new Object[] { cldrFileToCheck.getLocaleID() })); 309 // 310 // } 311 // return this; 312 // } 313 } 314 315 private static void addCollectionVariable(String name, Set<String> validItems) { 316 variables.put(name, new CollectionMatcher(validItems)); 317 } 318 319 public static Relation<String, String> getAllPossibleMissing(DtdType dtdType) { 320 Relation<String, String> missing = Relation.of(new TreeMap<String, Set<String>>(), LinkedHashSet.class); 321 322 if (dtdType == DtdType.ldmlICU) { 323 return missing; 324 } 325 326 DtdData dtdData2 = DtdData.getInstance(dtdType); 327 Map<String, Map<String, MatcherPattern>> element_attribute_validity = CldrUtility.ifNull( 328 dtd_element_attribute_validity.get(dtdType), 329 Collections.<String, Map<String, MatcherPattern>> emptyMap()); 330 331 for (DtdData.Element element : dtdData2.getElements()) { 332 if (element.isDeprecated()) { 333 continue; 334 } 335 Map<String, MatcherPattern> attribute_validity = CldrUtility.ifNull( 336 element_attribute_validity.get(element.name), 337 Collections.<String, MatcherPattern> emptyMap()); 338 for (DtdData.Attribute attribute : element.getAttributes().keySet()) { 339 if (attribute.isDeprecated()) { 340 continue; 341 } 342 if (!attribute.values.isEmpty()) { 343 continue; 344 } 345 MatcherPattern validity = attribute_validity.get(attribute.name); 346 if (validity != null) { 347 continue; 348 } 349 // <attributeValues attributes="alt" type="choice">$alt</attributeValues> 350 // <attributeValues dtds="supplementalData" elements="character" attributes="value" type="regex">.</attributeValues> 351 missing.put(attribute.name, 352 new AttributeValueSpec(dtdType, element.name, attribute.name, "$xxx").toString()); 353 } 354 } 355 return missing; 356 } 357 358 public static abstract class MatcherPattern { 359 360 public abstract boolean matches(String value, Output<String> reason); 361 362 public String getPattern() { 363 String temp = _getPattern(); 364 return temp.length() <= MAX_STRING ? temp : temp.substring(0, MAX_STRING) + ""; 365 } 366 367 public abstract String _getPattern(); 368 369 public String toString() { 370 return getClass().getName() + "\t" + getPattern(); 371 } 372 } 373 374 // private static MatcherPattern getBcp47MatcherPattern(String key) { 375 // // <key type="calendar">Calendar</key> 376 // // <type key="calendar" type="chinese">Chinese Calendar</type> 377 // 378 // //<attributeValues elements="key" attributes="type" type="bcp47">key</attributeValues> 379 // //<attributeValues elements="type" attributes="key" type="bcp47">key</attributeValues> 380 // //<attributeValues elements="type" attributes="type" type="bcp47">use-key</attributeValues> 381 // 382 // Set<String> values; 383 // if (key.equals("key")) { 384 // values = BCP47_KEY_VALUES.keySet(); 385 // } else { 386 // values = BCP47_KEY_VALUES.get(key); 387 // } 388 // return new CollectionMatcher(values); 389 // } 390 391 enum MatcherTypes { 392 single, choice, list, unicodeSet, unicodeSetOrString, regex, locale, bcp47, subdivision, localeSpecific, TODO; 393 } 394 395 private static MatcherPattern getMatcherPattern2(String type, String value) { 396 final MatcherTypes matcherType = type == null ? MatcherTypes.single : MatcherTypes.valueOf(type); 397 398 if (matcherType != MatcherTypes.TODO && value.startsWith("$")) { 399 MatcherPattern result = getVariable(matcherType, value); 400 if (result != null) { 401 return result; 402 } 403 throw new IllegalArgumentException("Unknown variable: " + value); 404 } 405 406 MatcherPattern result; 407 408 switch (matcherType) { 409 case single: 410 result = new CollectionMatcher(Collections.singleton(value.trim())); 411 break; 412 case choice: 413 result = new CollectionMatcher(SPACE.splitToList(value)); 414 break; 415 case unicodeSet: 416 result = new UnicodeSetMatcher(new UnicodeSet(value)); 417 break; 418 case unicodeSetOrString: 419 result = new UnicodeSetOrStringMatcher(new UnicodeSet(value)); 420 break; 421 // case bcp47: 422 // return getBcp47MatcherPattern(value); 423 case regex: 424 result = new RegexMatcher(value, Pattern.COMMENTS); // Pattern.COMMENTS to get whitespace 425 break; 426 case locale: 427 result = value.equals("all") ? LocaleMatcher.ALL_LANGUAGES : LocaleMatcher.REGULAR; 428 break; 429 case localeSpecific: 430 result = LocaleSpecificMatcher.getInstance(value); 431 break; 432 case TODO: 433 result = NOT_DONE_YET; 434 break; 435 case list: 436 result = new ListMatcher(new CollectionMatcher(SPACE.splitToList(value))); 437 break; 438 default: 439 return null; 440 } 441 442 return result; 443 } 444 445 private static MatcherPattern getVariable(final MatcherTypes matcherType, String value) { 446 List<String> values = BAR.splitToList(value); //value.trim().split("|"); 447 MatcherPattern[] reasons = new MatcherPattern[values.size()]; 448 for (int i = 0; i < values.size(); ++i) { 449 reasons[i] = getNonNullVariable(values.get(i)); 450 } 451 MatcherPattern result; 452 453 if (reasons.length == 1) { 454 result = reasons[0]; 455 } else { 456 result = new OrMatcher(reasons); 457 } 458 if (matcherType == MatcherTypes.list) { 459 result = new ListMatcher(result); 460 } 461 return result; 462 } 463 464 private static void addAttributes(Set<String> attributes, Map<String, MatcherPattern> attribute_validity, MatcherPattern mp) { 465 for (String attribute : attributes) { 466 MatcherPattern old = attribute_validity.get(attribute); 467 if (old != null) { 468 mp = new OrMatcher(old, mp); 469 } 470 attribute_validity.put(attribute, mp); 471 } 472 } 473 474 public static class RegexMatcher extends MatcherPattern { 475 476 private java.util.regex.Matcher matcher; 477 478 public RegexMatcher(String pattern, int flags) { 479 matcher = Pattern.compile(pattern, flags).matcher(""); 480 } 481 482 public boolean matches(String value, Output<String> reason) { 483 matcher.reset(value.toString()); 484 boolean result = matcher.matches(); 485 if (!result && reason != null) { 486 reason.value = RegexUtilities.showMismatch(matcher, value.toString()); 487 } 488 return result; 489 } 490 491 @Override 492 public String _getPattern() { 493 return matcher.toString(); 494 } 495 } 496 497 private static EnumMap<LocaleSpecific, Set<String>> LOCALE_SPECIFIC = null; 498 499 /** WARNING, not thread-safe. Needs cleanup **/ 500 public static void setLocaleSpecifics(EnumMap<LocaleSpecific, Set<String>> newValues) { 501 LOCALE_SPECIFIC = newValues; 502 } 503 504 public static class LocaleSpecificMatcher extends MatcherPattern { 505 final LocaleSpecific ls; 506 507 public LocaleSpecificMatcher(LocaleSpecific ls) { 508 this.ls = ls; 509 } 510 511 public static LocaleSpecificMatcher getInstance(String value) { 512 return new LocaleSpecificMatcher(LocaleSpecific.valueOf(value)); 513 } 514 515 public boolean matches(String value) { 516 return LOCALE_SPECIFIC.get(ls).contains(value); 517 } 518 519 static final int MAX_STRING = 64; 520 521 public boolean matches(String value, Output<String> reason) { 522 boolean result = LOCALE_SPECIFIC.get(ls).contains(value); 523 if (!result && reason != null) { 524 reason.value = " " + getPattern(); 525 } 526 return result; 527 } 528 529 @Override 530 public String _getPattern() { 531 return LOCALE_SPECIFIC.get(ls).toString(); 532 } 533 } 534 535 static final int MAX_STRING = 64; 536 537 public static class CollectionMatcher extends MatcherPattern { 538 private final Collection<String> collection; 539 540 public CollectionMatcher(Collection<String> collection) { 541 this.collection = Collections.unmodifiableCollection(new LinkedHashSet<>(collection)); 542 } 543 544 @Override 545 public boolean matches(String value, Output<String> reason) { 546 boolean result = collection.contains(value); 547 if (!result && reason != null) { 548 reason.value = " " + getPattern(); 549 } 550 return result; 551 } 552 553 @Override 554 public String _getPattern() { 555 return collection.toString(); 556 } 557 } 558 559 public static class UnicodeSetMatcher extends MatcherPattern { 560 private final UnicodeSet collection; 561 562 public UnicodeSetMatcher(UnicodeSet collection) { 563 this.collection = collection.freeze(); 564 } 565 566 @Override 567 public boolean matches(String value, Output<String> reason) { 568 boolean result = false; 569 try { 570 UnicodeSet valueSet = new UnicodeSet(value); 571 result = collection.containsAll(valueSet); 572 if (!result && reason != null) { 573 reason.value = " " + getPattern(); 574 } 575 } catch (Exception e) { 576 reason.value = " illegal pattern " + getPattern() + ": " + value; 577 } 578 return result; 579 } 580 581 @Override 582 public String _getPattern() { 583 return collection.toPattern(false); 584 } 585 } 586 587 public static class UnicodeSetOrStringMatcher extends MatcherPattern { 588 private final UnicodeSet collection; 589 590 public UnicodeSetOrStringMatcher(UnicodeSet collection) { 591 this.collection = collection.freeze(); 592 } 593 594 @Override 595 public boolean matches(String value, Output<String> reason) { 596 boolean result = false; 597 if (UnicodeSet.resemblesPattern(value, 0)) { 598 try { 599 UnicodeSet valueSet = new UnicodeSet(value); 600 result = collection.containsAll(valueSet); 601 if (!result && reason != null) { 602 reason.value = " " + getPattern(); 603 } 604 } catch (Exception e) { 605 reason.value = " illegal pattern " + getPattern() + ": " + value; 606 } 607 } else { 608 result = collection.contains(value); 609 if (!result && reason != null) { 610 reason.value = " " + getPattern(); 611 } 612 } 613 return result; 614 } 615 616 @Override 617 public String _getPattern() { 618 return collection.toPattern(false); 619 } 620 } 621 622 public static class OrMatcher extends MatcherPattern { 623 private final MatcherPattern[] operands; 624 625 public OrMatcher(MatcherPattern... operands) { 626 for (MatcherPattern operand : operands) { 627 if (operand == null) { 628 throw new NullPointerException(); 629 } 630 } 631 this.operands = operands; 632 } 633 634 public boolean matches(String value, Output<String> reason) { 635 StringBuilder fullReason = reason == null ? null : new StringBuilder(); 636 for (MatcherPattern operand : operands) { 637 if (operand.matches(value, reason)) { 638 return true; 639 } 640 if (fullReason != null) { 641 if (fullReason.length() != 0) { 642 fullReason.append("&"); 643 } 644 fullReason.append(reason.value); 645 } 646 } 647 if (fullReason != null) { 648 reason.value = fullReason.toString(); 649 } 650 return false; 651 } 652 653 @Override 654 public String _getPattern() { 655 StringBuffer result = new StringBuffer(); 656 for (MatcherPattern operand : operands) { 657 if (result.length() != 0) { 658 result.append('|'); 659 } 660 result.append(operand._getPattern()); 661 } 662 return result.toString(); 663 } 664 } 665 666 public static class ListMatcher extends MatcherPattern { 667 private MatcherPattern other; 668 669 public ListMatcher(MatcherPattern other) { 670 this.other = other; 671 } 672 673 public boolean matches(String value, Output<String> reason) { 674 List<String> values = SPACE.splitToList(value); 675 if (values.isEmpty()) return true; 676 for (String valueItem : values) { 677 if (!other.matches(valueItem, reason)) { 678 if (reason != null) { 679 reason.value = "" + valueItem + " " + other.getPattern(); 680 } 681 return false; 682 } 683 } 684 return true; 685 } 686 687 @Override 688 public String _getPattern() { 689 return "List of " + other._getPattern(); 690 } 691 } 692 693 public static class LocaleMatcher extends MatcherPattern { 694 //final ObjectMatcherReason grandfathered = getNonNullVariable("$grandfathered").matcher; 695 final MatcherPattern language; 696 final MatcherPattern script = getNonNullVariable("$_script"); 697 final MatcherPattern territory = getNonNullVariable("$_region"); 698 final MatcherPattern variant = getNonNullVariable("$_variant"); 699 final LocaleIDParser lip = new LocaleIDParser(); 700 701 public static LocaleMatcher REGULAR = new LocaleMatcher("$_language_plus"); 702 public static LocaleMatcher ALL_LANGUAGES = new LocaleMatcher("$_language"); 703 704 private LocaleMatcher(String variable) { 705 language = getNonNullVariable(variable); 706 } 707 708 public boolean matches(String value, Output<String> reason) { 709 // if (grandfathered.matches(value, reason)) { 710 // return true; 711 // } 712 lip.set((String) value); 713 String field = lip.getLanguage(); 714 if (!language.matches(field, reason)) { 715 if (reason != null) { 716 reason.value = "invalid base language"; 717 } 718 return false; 719 } 720 field = lip.getScript(); 721 if (field.length() != 0 && !script.matches(field, reason)) { 722 if (reason != null) { 723 reason.value = "invalid script"; 724 } 725 return false; 726 } 727 field = lip.getRegion(); 728 if (field.length() != 0 && !territory.matches(field, reason)) { 729 if (reason != null) { 730 reason.value = "invalid region"; 731 } 732 return false; 733 } 734 String[] fields = lip.getVariants(); 735 for (int i = 0; i < fields.length; ++i) { 736 if (!variant.matches(fields[i], reason)) { 737 if (reason != null) { 738 reason.value = "invalid variant"; 739 } 740 return false; 741 } 742 } 743 return true; 744 } 745 746 @Override 747 public String _getPattern() { 748 return "Unicode_Language_Subtag"; 749 } 750 } 751 752 public static final class AttributeValueSpec implements Comparable<AttributeValueSpec> { 753 public AttributeValueSpec(DtdType type, String element, String attribute, String attributeValue) { 754 this.type = type; 755 this.element = element; 756 this.attribute = attribute; 757 this.attributeValue = attributeValue; 758 } 759 760 public final DtdType type; 761 public final String element; 762 public final String attribute; 763 public final String attributeValue; 764 765 @Override 766 public int hashCode() { 767 return Objects.hash(type, element, attribute, attributeValue); 768 } 769 770 @Override 771 public boolean equals(Object obj) { 772 AttributeValueSpec other = (AttributeValueSpec) obj; 773 return CldrUtility.deepEquals( 774 type, other.type, 775 element, other.element, 776 attribute, other.attribute, 777 attributeValue, other.attributeValue); 778 } 779 780 @Override 781 public int compareTo(AttributeValueSpec other) { 782 return ComparisonChain.start() 783 .compare(type, other.type) 784 .compare(element, other.element) 785 .compare(attribute, other.attribute) 786 .compare(attributeValue, other.attributeValue) 787 .result(); 788 } 789 790 @Override 791 public String toString() { 792 return "<attributeValues" 793 + " dtds='" + type + "\'" 794 + " elements='" + element + "\'" 795 + " attributes='" + attribute + "\'" 796 + " type='TODO\'>" 797 + attributeValue 798 + "</attributeValues>"; 799 } 800 } 801 802 /** 803 * return Status 804 * @param attribute_validity 805 * @param attribute 806 * @param attributeValue 807 * @param result 808 * @return 809 */ 810 private static Status check(Map<String, MatcherPattern> attribute_validity, 811 String element, String attribute, String attributeValue, 812 Output<String> reason) { 813 814 if (attribute_validity == null) { 815 return Status.noTest; // no test 816 } 817 MatcherPattern matcherPattern = attribute_validity.get(attribute); 818 if (matcherPattern == null) { 819 return Status.noTest; // no test 820 } 821 if (matcherPattern.matches(attributeValue, reason)) { 822 return Status.ok; 823 } 824 return Status.illegal; 825 } 826 827 public static Status check(DtdData dtdData, String element, String attribute, String attributeValue, Output<String> reason) { 828 if (dtdData.isDeprecated(element, attribute, attributeValue)) { 829 return Status.deprecated; 830 } 831 Status haveTest = check(common_attribute_validity, element, attribute, attributeValue, reason); 832 833 if (haveTest == Status.noTest) { 834 final Map<String, Map<String, MatcherPattern>> element_attribute_validity = dtd_element_attribute_validity.get(dtdData.dtdType); 835 if (element_attribute_validity == null) { 836 return Status.noTest; 837 } 838 839 Map<String, MatcherPattern> attribute_validity = element_attribute_validity.get(element); 840 if (attribute_validity == null) { 841 return Status.noTest; 842 } 843 844 haveTest = check(attribute_validity, element, attribute, attributeValue, reason); 845 } 846 return haveTest; 847 } 848 849 public static Set<R3<DtdType, String, String>> getTodoTests() { 850 Set<Row.R3<DtdType, String, String>> result = new LinkedHashSet<>(); 851 for (Entry<DtdType, Map<String, Map<String, MatcherPattern>>> entry1 : dtd_element_attribute_validity.entrySet()) { 852 for (Entry<String, Map<String, MatcherPattern>> entry2 : entry1.getValue().entrySet()) { 853 for (Entry<String, MatcherPattern> entry3 : entry2.getValue().entrySet()) { 854 if (entry3.getValue() == NOT_DONE_YET) { 855 result.add(Row.of(entry1.getKey(), entry2.getKey(), entry3.getKey())); 856 } 857 } 858 } 859 } 860 return result; 861 } 862 863 public static Map<AttributeValidityInfo, String> getReadFailures() { 864 return Collections.unmodifiableMap(failures); 865 } 866 867 public static MatcherPattern getMatcherPattern(String variable) { 868 return variables.get(variable); 869 } 870 871 private static MatcherPattern getNonNullVariable(String variable) { 872 MatcherPattern result = variables.get(variable); 873 if (result == null) { 874 throw new NullPointerException(); 875 } 876 return result; 877 } 878 879 public static Set<String> getMatcherPatternIds() { 880 return Collections.unmodifiableSet(variables.keySet()); 881 } 882 883 public static void main(String[] args) { 884 for (DtdType type : DtdType.values()) { 885 Relation<String, String> missing = getAllPossibleMissing(type); 886 for (Entry<String, String> x : missing.keyValueSet()) { 887 System.out.println(type + "\t" + CldrUtility.toString(x)); 888 } 889 } 890 } 891 } 892