1 /* 2 * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package build.tools.cldrconverter; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.text.DateFormatSymbols; 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Locale; 37 import java.util.Map; 38 import java.util.Set; 39 import org.xml.sax.Attributes; 40 import org.xml.sax.InputSource; 41 import org.xml.sax.SAXException; 42 43 /** 44 * Handles parsing of files in Locale Data Markup Language and produces a map 45 * that uses the keys and values of JRE locale data. 46 */ 47 class LDMLParseHandler extends AbstractLDMLHandler<Object> { 48 private String defaultNumberingSystem; 49 private String currentNumberingSystem = ""; 50 private CalendarType currentCalendarType; 51 private String zoneNameStyle; // "long" or "short" for time zone names 52 private String zonePrefix; 53 private final String id; 54 private String currentContext = ""; // "format"/"stand-alone" 55 private String currentWidth = ""; // "wide"/"narrow"/"abbreviated" 56 private String currentStyle = ""; // short, long for decimalFormat 57 LDMLParseHandler(String id)58 LDMLParseHandler(String id) { 59 this.id = id; 60 } 61 62 @Override resolveEntity(String publicID, String systemID)63 public InputSource resolveEntity(String publicID, String systemID) throws IOException, SAXException { 64 // avoid HTTP traffic to unicode.org 65 if (systemID.startsWith(CLDRConverter.LDML_DTD_SYSTEM_ID)) { 66 return new InputSource((new File(CLDRConverter.LOCAL_LDML_DTD)).toURI().toString()); 67 } 68 return null; 69 } 70 71 @Override startElement(String uri, String localName, String qName, Attributes attributes)72 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { 73 switch (qName) { 74 // 75 // Generic information 76 // 77 case "identity": 78 // ignore this element - it has language and territory elements that aren't locale data 79 pushIgnoredContainer(qName); 80 break; 81 82 // for LocaleNames 83 // copy string 84 case "localeSeparator": 85 pushStringEntry(qName, attributes, 86 CLDRConverter.LOCALE_SEPARATOR); 87 break; 88 case "localeKeyTypePattern": 89 pushStringEntry(qName, attributes, 90 CLDRConverter.LOCALE_KEYTYPE); 91 break; 92 93 case "language": 94 case "script": 95 case "territory": 96 case "variant": 97 // for LocaleNames 98 // copy string 99 pushStringEntry(qName, attributes, 100 CLDRConverter.LOCALE_NAME_PREFIX + 101 (qName.equals("variant") ? "%%" : "") + 102 attributes.getValue("type")); 103 break; 104 105 case "key": 106 // for LocaleNames 107 // copy string 108 { 109 String key = convertOldKeyName(attributes.getValue("type")); 110 if (key.length() == 2) { 111 pushStringEntry(qName, attributes, 112 CLDRConverter.LOCALE_KEY_PREFIX + key); 113 } else { 114 pushIgnoredContainer(qName); 115 } 116 } 117 break; 118 119 case "type": 120 // for LocaleNames/CalendarNames 121 // copy string 122 { 123 String key = convertOldKeyName(attributes.getValue("key")); 124 if (key.length() == 2) { 125 pushStringEntry(qName, attributes, 126 CLDRConverter.LOCALE_TYPE_PREFIX + key + "." + 127 attributes.getValue("type")); 128 } else { 129 pushIgnoredContainer(qName); 130 } 131 } 132 break; 133 134 // 135 // Currency information 136 // 137 case "currency": 138 // for CurrencyNames 139 // stash away "type" value for nested <symbol> 140 pushKeyContainer(qName, attributes, attributes.getValue("type")); 141 break; 142 case "symbol": 143 // for CurrencyNames 144 // need to get the key from the containing <currency> element 145 pushStringEntry(qName, attributes, CLDRConverter.CURRENCY_SYMBOL_PREFIX 146 + getContainerKey()); 147 break; 148 149 // Calendar or currency 150 case "displayName": 151 { 152 if (currentContainer.getqName().equals("field")) { 153 pushStringEntry(qName, attributes, 154 (currentCalendarType != null ? currentCalendarType.keyElementName() : "") 155 + "field." + getContainerKey()); 156 } else { 157 // for CurrencyNames 158 // need to get the key from the containing <currency> element 159 // ignore if is has "count" attribute 160 String containerKey = getContainerKey(); 161 if (containerKey != null && attributes.getValue("count") == null) { 162 pushStringEntry(qName, attributes, 163 CLDRConverter.CURRENCY_NAME_PREFIX 164 + containerKey.toLowerCase(Locale.ROOT), 165 attributes.getValue("type")); 166 } else { 167 pushIgnoredContainer(qName); 168 } 169 } 170 } 171 break; 172 173 // 174 // Calendar information 175 // 176 case "calendar": 177 { 178 // mostly for FormatData (CalendarData items firstDay and minDays are also nested) 179 // use only if it's supported by java.util.Calendar. 180 String calendarName = attributes.getValue("type"); 181 currentCalendarType = CalendarType.forName(calendarName); 182 if (currentCalendarType != null) { 183 pushContainer(qName, attributes); 184 } else { 185 pushIgnoredContainer(qName); 186 } 187 } 188 break; 189 case "fields": 190 { 191 pushContainer(qName, attributes); 192 } 193 break; 194 case "field": 195 { 196 String type = attributes.getValue("type"); 197 switch (type) { 198 case "era": 199 case "year": 200 case "month": 201 case "week": 202 case "weekday": 203 case "dayperiod": 204 case "hour": 205 case "minute": 206 case "second": 207 case "zone": 208 pushKeyContainer(qName, attributes, type); 209 break; 210 default: 211 pushIgnoredContainer(qName); 212 break; 213 } 214 } 215 break; 216 case "monthContext": 217 { 218 // for FormatData 219 // need to keep stand-alone and format, to allow for inheritance in CLDR 220 String type = attributes.getValue("type"); 221 if ("stand-alone".equals(type) || "format".equals(type)) { 222 currentContext = type; 223 pushKeyContainer(qName, attributes, type); 224 } else { 225 pushIgnoredContainer(qName); 226 } 227 } 228 break; 229 case "monthWidth": 230 { 231 // for FormatData 232 // create string array for the two types that the JRE knows 233 // keep info about the context type so we can sort out inheritance later 234 if (currentCalendarType == null) { 235 pushIgnoredContainer(qName); 236 break; 237 } 238 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 239 currentWidth = attributes.getValue("type"); 240 switch (currentWidth) { 241 case "wide": 242 pushStringArrayEntry(qName, attributes, prefix + "MonthNames/" + getContainerKey(), 13); 243 break; 244 case "abbreviated": 245 pushStringArrayEntry(qName, attributes, prefix + "MonthAbbreviations/" + getContainerKey(), 13); 246 break; 247 case "narrow": 248 pushStringArrayEntry(qName, attributes, prefix + "MonthNarrows/" + getContainerKey(), 13); 249 break; 250 default: 251 pushIgnoredContainer(qName); 252 break; 253 } 254 } 255 break; 256 case "month": 257 // for FormatData 258 // add to string array entry of monthWidth element 259 pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1); 260 break; 261 case "dayContext": 262 { 263 // for FormatData 264 // need to keep stand-alone and format, to allow for multiple inheritance in CLDR 265 String type = attributes.getValue("type"); 266 if ("stand-alone".equals(type) || "format".equals(type)) { 267 currentContext = type; 268 pushKeyContainer(qName, attributes, type); 269 } else { 270 pushIgnoredContainer(qName); 271 } 272 } 273 break; 274 case "dayWidth": 275 { 276 // for FormatData 277 // create string array for the two types that the JRE knows 278 // keep info about the context type so we can sort out inheritance later 279 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 280 currentWidth = attributes.getValue("type"); 281 switch (currentWidth) { 282 case "wide": 283 pushStringArrayEntry(qName, attributes, prefix + "DayNames/" + getContainerKey(), 7); 284 break; 285 case "abbreviated": 286 pushStringArrayEntry(qName, attributes, prefix + "DayAbbreviations/" + getContainerKey(), 7); 287 break; 288 case "narrow": 289 pushStringArrayEntry(qName, attributes, prefix + "DayNarrows/" + getContainerKey(), 7); 290 break; 291 default: 292 pushIgnoredContainer(qName); 293 break; 294 } 295 } 296 break; 297 case "day": 298 // for FormatData 299 // add to string array entry of monthWidth element 300 pushStringArrayElement(qName, attributes, Integer.parseInt(DAY_OF_WEEK_MAP.get(attributes.getValue("type"))) - 1); 301 break; 302 case "dayPeriodContext": 303 // for FormatData 304 // need to keep stand-alone and format, to allow for multiple inheritance in CLDR 305 { 306 String type = attributes.getValue("type"); 307 if ("stand-alone".equals(type) || "format".equals(type)) { 308 currentContext = type; 309 pushKeyContainer(qName, attributes, type); 310 } else { 311 pushIgnoredContainer(qName); 312 } 313 } 314 break; 315 case "dayPeriodWidth": 316 // for FormatData 317 // create string array entry for am/pm. 318 currentWidth = attributes.getValue("type"); 319 switch (currentWidth) { 320 case "wide": 321 pushStringArrayEntry(qName, attributes, "AmPmMarkers/" + getContainerKey(), 12); 322 break; 323 case "narrow": 324 pushStringArrayEntry(qName, attributes, "narrow.AmPmMarkers/" + getContainerKey(), 12); 325 break; 326 case "abbreviated": 327 pushStringArrayEntry(qName, attributes, "abbreviated.AmPmMarkers/" + getContainerKey(), 12); 328 break; 329 default: 330 pushIgnoredContainer(qName); 331 break; 332 } 333 break; 334 case "dayPeriod": 335 // for FormatData 336 // add to string array entry of AmPmMarkers element 337 if (attributes.getValue("alt") == null) { 338 switch (attributes.getValue("type")) { 339 case "am": 340 pushStringArrayElement(qName, attributes, 0); 341 break; 342 case "pm": 343 pushStringArrayElement(qName, attributes, 1); 344 break; 345 case "midnight": 346 pushStringArrayElement(qName, attributes, 2); 347 break; 348 case "noon": 349 pushStringArrayElement(qName, attributes, 3); 350 break; 351 case "morning1": 352 pushStringArrayElement(qName, attributes, 4); 353 break; 354 case "morning2": 355 pushStringArrayElement(qName, attributes, 5); 356 break; 357 case "afternoon1": 358 pushStringArrayElement(qName, attributes, 6); 359 break; 360 case "afternoon2": 361 pushStringArrayElement(qName, attributes, 7); 362 break; 363 case "evening1": 364 pushStringArrayElement(qName, attributes, 8); 365 break; 366 case "evening2": 367 pushStringArrayElement(qName, attributes, 9); 368 break; 369 case "night1": 370 pushStringArrayElement(qName, attributes, 10); 371 break; 372 case "night2": 373 pushStringArrayElement(qName, attributes, 11); 374 break; 375 default: 376 pushIgnoredContainer(qName); 377 break; 378 } 379 } else { 380 // discard alt values 381 pushIgnoredContainer(qName); 382 } 383 break; 384 case "eraNames": 385 // CLDR era names are inconsistent in terms of their lengths. For example, 386 // the full names of Japanese imperial eras are eraAbbr, while the full names 387 // of the Julian eras are eraNames. 388 if (currentCalendarType == null) { 389 assert currentContainer instanceof IgnoredContainer; 390 pushIgnoredContainer(qName); 391 } else { 392 String key = currentCalendarType.keyElementName() + "long.Eras"; // for now 393 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName)); 394 } 395 break; 396 case "eraAbbr": 397 // for FormatData 398 // create string array entry 399 if (currentCalendarType == null) { 400 assert currentContainer instanceof IgnoredContainer; 401 pushIgnoredContainer(qName); 402 } else { 403 String key = currentCalendarType.keyElementName() + "Eras"; 404 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName)); 405 } 406 break; 407 case "eraNarrow": 408 // mainly used for the Japanese imperial calendar 409 if (currentCalendarType == null) { 410 assert currentContainer instanceof IgnoredContainer; 411 pushIgnoredContainer(qName); 412 } else { 413 String key = currentCalendarType.keyElementName() + "narrow.Eras"; 414 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName)); 415 } 416 break; 417 case "era": 418 // for FormatData 419 // add to string array entry of eraAbbr element 420 if (currentCalendarType == null) { 421 assert currentContainer instanceof IgnoredContainer; 422 pushIgnoredContainer(qName); 423 } else { 424 int index = Integer.parseInt(attributes.getValue("type")); 425 index = currentCalendarType.normalizeEraIndex(index); 426 if (index >= 0) { 427 pushStringArrayElement(qName, attributes, index); 428 } else { 429 pushIgnoredContainer(qName); 430 } 431 if (currentContainer.getParent() == null) { 432 throw new InternalError("currentContainer: null parent"); 433 } 434 } 435 break; 436 case "quarterContext": 437 { 438 // for FormatData 439 // need to keep stand-alone and format, to allow for inheritance in CLDR 440 String type = attributes.getValue("type"); 441 if ("stand-alone".equals(type) || "format".equals(type)) { 442 currentContext = type; 443 pushKeyContainer(qName, attributes, type); 444 } else { 445 pushIgnoredContainer(qName); 446 } 447 } 448 break; 449 case "quarterWidth": 450 { 451 // for FormatData 452 // keep info about the context type so we can sort out inheritance later 453 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 454 currentWidth = attributes.getValue("type"); 455 switch (currentWidth) { 456 case "wide": 457 pushStringArrayEntry(qName, attributes, prefix + "QuarterNames/" + getContainerKey(), 4); 458 break; 459 case "abbreviated": 460 pushStringArrayEntry(qName, attributes, prefix + "QuarterAbbreviations/" + getContainerKey(), 4); 461 break; 462 case "narrow": 463 pushStringArrayEntry(qName, attributes, prefix + "QuarterNarrows/" + getContainerKey(), 4); 464 break; 465 default: 466 pushIgnoredContainer(qName); 467 break; 468 } 469 } 470 break; 471 case "quarter": 472 // for FormatData 473 // add to string array entry of quarterWidth element 474 pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1); 475 break; 476 477 // 478 // Time zone names 479 // 480 case "timeZoneNames": 481 pushContainer(qName, attributes); 482 break; 483 case "hourFormat": 484 pushStringEntry(qName, attributes, "timezone.hourFormat"); 485 break; 486 case "gmtFormat": 487 pushStringEntry(qName, attributes, "timezone.gmtFormat"); 488 break; 489 case "gmtZeroFormat": 490 pushStringEntry(qName, attributes, "timezone.gmtZeroFormat"); 491 break; 492 case "regionFormat": 493 { 494 String type = attributes.getValue("type"); 495 pushStringEntry(qName, attributes, "timezone.regionFormat" + 496 (type == null ? "" : "." + type)); 497 } 498 break; 499 case "zone": 500 { 501 String tzid = attributes.getValue("type"); // Olson tz id 502 zonePrefix = CLDRConverter.TIMEZONE_ID_PREFIX; 503 put(zonePrefix + tzid, new HashMap<String, String>()); 504 pushKeyContainer(qName, attributes, tzid); 505 } 506 break; 507 case "metazone": 508 { 509 String zone = attributes.getValue("type"); // LDML meta zone id 510 zonePrefix = CLDRConverter.METAZONE_ID_PREFIX; 511 put(zonePrefix + zone, new HashMap<String, String>()); 512 pushKeyContainer(qName, attributes, zone); 513 } 514 break; 515 case "long": 516 zoneNameStyle = "long"; 517 pushContainer(qName, attributes); 518 break; 519 case "short": 520 zoneNameStyle = "short"; 521 pushContainer(qName, attributes); 522 break; 523 case "generic": // generic name 524 case "standard": // standard time name 525 case "daylight": // daylight saving (summer) time name 526 pushStringEntry(qName, attributes, CLDRConverter.ZONE_NAME_PREFIX + qName + "." + zoneNameStyle); 527 break; 528 case "exemplarCity": 529 pushStringEntry(qName, attributes, CLDRConverter.EXEMPLAR_CITY_PREFIX); 530 break; 531 532 // 533 // Number format information 534 // 535 case "decimalFormatLength": 536 String type = attributes.getValue("type"); 537 if (null == type) { 538 // format data for decimal number format 539 pushStringEntry(qName, attributes, 540 currentNumberingSystem + "NumberPatterns/decimal"); 541 currentStyle = type; 542 } else { 543 switch (type) { 544 case "short": 545 case "long": 546 // considering "short" and long for 547 // compact number formatting patterns 548 pushKeyContainer(qName, attributes, type); 549 currentStyle = type; 550 break; 551 default: 552 pushIgnoredContainer(qName); 553 break; 554 } 555 } 556 break; 557 case "decimalFormat": 558 if(currentStyle == null) { 559 pushContainer(qName, attributes); 560 } else { 561 switch (currentStyle) { 562 case "short": 563 case "long": 564 pushStringListEntry(qName, attributes, 565 currentStyle+".CompactNumberPatterns"); 566 break; 567 default: 568 pushIgnoredContainer(qName); 569 break; 570 } 571 } 572 break; 573 case "currencyFormat": 574 case "percentFormat": 575 pushKeyContainer(qName, attributes, attributes.getValue("type")); 576 break; 577 578 case "pattern": 579 String containerName = currentContainer.getqName(); 580 switch (containerName) { 581 case "currencyFormat": 582 case "percentFormat": 583 { 584 // for FormatData 585 // copy string for later assembly into NumberPatterns 586 if (currentContainer instanceof KeyContainer) { 587 String fStyle = ((KeyContainer)currentContainer).getKey(); 588 if (fStyle.equals("standard")) { 589 pushStringEntry(qName, attributes, 590 currentNumberingSystem + "NumberPatterns/" + containerName.replaceFirst("Format", "")); 591 } else if (fStyle.equals("accounting") && containerName.equals("currencyFormat")) { 592 pushStringEntry(qName, attributes, 593 currentNumberingSystem + "NumberPatterns/accounting"); 594 } else { 595 pushIgnoredContainer(qName); 596 } 597 } else { 598 pushIgnoredContainer(qName); 599 } 600 } 601 break; 602 603 case "decimalFormat": 604 if (currentStyle == null) { 605 pushContainer(qName, attributes); 606 } else { 607 switch (currentStyle) { 608 case "short": 609 case "long": 610 pushStringListElement(qName, attributes, 611 (int) Math.log10(Double.parseDouble(attributes.getValue("type"))), 612 attributes.getValue("count")); 613 break; 614 default: 615 pushIgnoredContainer(qName); 616 break; 617 } 618 } 619 break; 620 default: 621 pushContainer(qName, attributes); 622 break; 623 } 624 break; 625 case "currencyFormats": 626 case "decimalFormats": 627 case "percentFormats": 628 { 629 String script = attributes.getValue("numberSystem"); 630 if (script != null) { 631 addNumberingScript(script); 632 currentNumberingSystem = script + "."; 633 } 634 pushContainer(qName, attributes); 635 } 636 break; 637 case "currencyFormatLength": 638 if (attributes.getValue("type") == null) { 639 // skipping type="short" data 640 // for FormatData 641 pushContainer(qName, attributes); 642 } else { 643 pushIgnoredContainer(qName); 644 } 645 break; 646 case "defaultNumberingSystem": 647 // default numbering system if multiple numbering systems are used. 648 pushStringEntry(qName, attributes, "DefaultNumberingSystem"); 649 break; 650 case "symbols": 651 // for FormatData 652 // look up numberingSystems 653 symbols: { 654 String script = attributes.getValue("numberSystem"); 655 if (script == null) { 656 // Has no script. Just ignore. 657 pushIgnoredContainer(qName); 658 break; 659 } 660 661 // Use keys as <script>."NumberElements/<symbol>" 662 currentNumberingSystem = script + "."; 663 String digits = CLDRConverter.handlerNumbering.get(script); 664 if (digits == null) { 665 pushIgnoredContainer(qName); 666 break; 667 } 668 669 addNumberingScript(script); 670 put(currentNumberingSystem + "NumberElements/zero", digits.substring(0, 1)); 671 pushContainer(qName, attributes); 672 } 673 break; 674 case "decimal": 675 case "group": 676 case "currencyDecimal": 677 case "currencyGroup": 678 // for FormatData 679 // copy string for later assembly into NumberElements 680 if (currentContainer.getqName().equals("symbols")) { 681 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/" + qName); 682 } else { 683 pushIgnoredContainer(qName); 684 } 685 break; 686 case "list": 687 // for FormatData 688 // copy string for later assembly into NumberElements 689 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/list"); 690 break; 691 case "percentSign": 692 // for FormatData 693 // copy string for later assembly into NumberElements 694 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/percent"); 695 break; 696 case "nativeZeroDigit": 697 // for FormatData 698 // copy string for later assembly into NumberElements 699 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/zero"); 700 break; 701 case "patternDigit": 702 // for FormatData 703 // copy string for later assembly into NumberElements 704 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/pattern"); 705 break; 706 case "plusSign": 707 // TODO: DecimalFormatSymbols doesn't support plusSign 708 pushIgnoredContainer(qName); 709 break; 710 case "minusSign": 711 // for FormatData 712 // copy string for later assembly into NumberElements 713 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/minus"); 714 break; 715 case "exponential": 716 // for FormatData 717 // copy string for later assembly into NumberElements 718 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/exponential"); 719 break; 720 case "perMille": 721 // for FormatData 722 // copy string for later assembly into NumberElements 723 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/permille"); 724 break; 725 case "infinity": 726 // for FormatData 727 // copy string for later assembly into NumberElements 728 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/infinity"); 729 break; 730 case "nan": 731 // for FormatData 732 // copy string for later assembly into NumberElements 733 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/nan"); 734 break; 735 case "timeFormatLength": 736 { 737 // for FormatData 738 // copy string for later assembly into DateTimePatterns 739 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 740 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-time"); 741 } 742 break; 743 case "dateFormatLength": 744 { 745 // for FormatData 746 // copy string for later assembly into DateTimePatterns 747 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 748 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-date"); 749 } 750 break; 751 case "dateTimeFormatLength": 752 { 753 // for FormatData 754 // copy string for later assembly into DateTimePatterns 755 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 756 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime"); 757 } 758 break; 759 case "localizedPatternChars": 760 { 761 // for FormatData 762 // copy string for later adaptation to JRE use 763 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 764 pushStringEntry(qName, attributes, prefix + "DateTimePatternChars"); 765 } 766 break; 767 768 // "alias" for root 769 case "alias": 770 { 771 if (id.equals("root") && !isIgnored(attributes) 772 && ((currentContainer.getqName().equals("decimalFormatLength")) 773 || (currentContainer.getqName().equals("currencyFormat")) 774 || (currentContainer.getqName().equals("percentFormat")) 775 || (currentCalendarType != null && !currentCalendarType.lname().startsWith("islamic-")))) { // ignore islamic variants 776 pushAliasEntry(qName, attributes, attributes.getValue("path")); 777 } else { 778 pushIgnoredContainer(qName); 779 } 780 } 781 break; 782 783 default: 784 // treat anything else as a container 785 pushContainer(qName, attributes); 786 break; 787 } 788 } 789 790 private static final String[] CONTEXTS = {"stand-alone", "format"}; 791 private static final String[] WIDTHS = {"wide", "narrow", "abbreviated"}; 792 private static final String[] LENGTHS = {"full", "long", "medium", "short"}; 793 populateWidthAlias(String type, Set<String> keys)794 private void populateWidthAlias(String type, Set<String> keys) { 795 for (String context : CONTEXTS) { 796 for (String width : WIDTHS) { 797 String keyName = toJDKKey(type+"Width", context, width); 798 if (keyName.length() > 0) { 799 keys.add(keyName + "," + context + "," + width); 800 } 801 } 802 } 803 } 804 populateFormatLengthAlias(String type, Set<String> keys)805 private void populateFormatLengthAlias(String type, Set<String> keys) { 806 for (String length: LENGTHS) { 807 String keyName = toJDKKey(type+"FormatLength", currentContext, length); 808 if (keyName.length() > 0) { 809 keys.add(keyName + "," + currentContext + "," + length); 810 } 811 } 812 } 813 populateAliasKeys(String qName, String context, String width)814 private Set<String> populateAliasKeys(String qName, String context, String width) { 815 HashSet<String> ret = new HashSet<>(); 816 String keyName = qName; 817 818 switch (qName) { 819 case "monthWidth": 820 case "dayWidth": 821 case "quarterWidth": 822 case "dayPeriodWidth": 823 case "dateFormatLength": 824 case "timeFormatLength": 825 case "dateTimeFormatLength": 826 case "eraNames": 827 case "eraAbbr": 828 case "eraNarrow": 829 ret.add(toJDKKey(qName, context, width) + "," + context + "," + width); 830 break; 831 case "days": 832 populateWidthAlias("day", ret); 833 break; 834 case "months": 835 populateWidthAlias("month", ret); 836 break; 837 case "quarters": 838 populateWidthAlias("quarter", ret); 839 break; 840 case "dayPeriods": 841 populateWidthAlias("dayPeriod", ret); 842 break; 843 case "eras": 844 ret.add(toJDKKey("eraNames", context, width) + "," + context + "," + width); 845 ret.add(toJDKKey("eraAbbr", context, width) + "," + context + "," + width); 846 ret.add(toJDKKey("eraNarrow", context, width) + "," + context + "," + width); 847 break; 848 case "dateFormats": 849 populateFormatLengthAlias("date", ret); 850 break; 851 case "timeFormats": 852 populateFormatLengthAlias("time", ret); 853 break; 854 default: 855 break; 856 } 857 return ret; 858 } 859 translateWidthAlias(String qName, String context, String width)860 private String translateWidthAlias(String qName, String context, String width) { 861 String keyName = qName; 862 String type = Character.toUpperCase(qName.charAt(0)) + qName.substring(1, qName.indexOf("Width")); 863 864 switch (width) { 865 case "wide": 866 keyName = type + "Names/" + context; 867 break; 868 case "abbreviated": 869 keyName = type + "Abbreviations/" + context; 870 break; 871 case "narrow": 872 keyName = type + "Narrows/" + context; 873 break; 874 default: 875 assert false; 876 } 877 878 return keyName; 879 } 880 toJDKKey(String containerqName, String context, String type)881 private String toJDKKey(String containerqName, String context, String type) { 882 String keyName = containerqName; 883 884 switch (containerqName) { 885 case "monthWidth": 886 case "dayWidth": 887 case "quarterWidth": 888 keyName = translateWidthAlias(keyName, context, type); 889 break; 890 case "dayPeriodWidth": 891 switch (type) { 892 case "wide": 893 keyName = "AmPmMarkers/" + context; 894 break; 895 case "narrow": 896 keyName = "narrow.AmPmMarkers/" + context; 897 break; 898 case "abbreviated": 899 keyName = "abbreviated.AmPmMarkers/" + context; 900 break; 901 } 902 break; 903 case "dateFormatLength": 904 case "timeFormatLength": 905 case "dateTimeFormatLength": 906 keyName = "DateTimePatterns/" + 907 type + "-" + 908 keyName.substring(0, keyName.indexOf("FormatLength")); 909 break; 910 case "eraNames": 911 keyName = "long.Eras"; 912 break; 913 case "eraAbbr": 914 keyName = "Eras"; 915 break; 916 case "eraNarrow": 917 keyName = "narrow.Eras"; 918 break; 919 case "dateFormats": 920 case "timeFormats": 921 case "days": 922 case "months": 923 case "quarters": 924 case "dayPeriods": 925 case "eras": 926 break; 927 case "decimalFormatLength": // used for compact number formatting patterns 928 keyName = type + ".CompactNumberPatterns"; 929 break; 930 case "currencyFormat": 931 case "percentFormat": 932 keyName = currentNumberingSystem + 933 "NumberPatterns/" + 934 (type.equals("standard") ? containerqName.replaceFirst("Format", "") : type); 935 break; 936 default: 937 keyName = ""; 938 break; 939 } 940 941 return keyName; 942 } 943 getTarget(String path, String calType, String context, String width)944 private String getTarget(String path, String calType, String context, String width) { 945 // Target qName 946 int lastSlash = path.lastIndexOf('/'); 947 String qName = path.substring(lastSlash+1); 948 int bracket = qName.indexOf('['); 949 if (bracket != -1) { 950 qName = qName.substring(0, bracket); 951 } 952 953 // calType 954 String typeKey = "/calendar[@type='"; 955 int start = path.indexOf(typeKey); 956 if (start != -1) { 957 calType = path.substring(start+typeKey.length(), path.indexOf("']", start)); 958 } 959 960 // context 961 typeKey = "Context[@type='"; 962 start = path.indexOf(typeKey); 963 if (start != -1) { 964 context = (path.substring(start+typeKey.length(), path.indexOf("']", start))); 965 } 966 967 // width 968 typeKey = "Width[@type='"; 969 start = path.indexOf(typeKey); 970 if (start != -1) { 971 width = path.substring(start+typeKey.length(), path.indexOf("']", start)); 972 } 973 974 // used for compact number formatting patterns aliases 975 typeKey = "decimalFormatLength[@type='"; 976 start = path.indexOf(typeKey); 977 if (start != -1) { 978 String style = path.substring(start + typeKey.length(), path.indexOf("']", start)); 979 return toJDKKey(qName, "", style); 980 } 981 982 // currencyFormat 983 typeKey = "currencyFormat[@type='"; 984 start = path.indexOf(typeKey); 985 if (start != -1) { 986 String style = path.substring(start + typeKey.length(), path.indexOf("']", start)); 987 return toJDKKey(qName, "", style); 988 } 989 990 // percentFormat 991 typeKey = "percentFormat[@type='"; 992 start = path.indexOf(typeKey); 993 if (start != -1) { 994 String style = path.substring(start + typeKey.length(), path.indexOf("']", start)); 995 return toJDKKey(qName, "", style); 996 } 997 998 return calType + "." + toJDKKey(qName, context, width); 999 } 1000 1001 @Override 1002 @SuppressWarnings("fallthrough") endElement(String uri, String localName, String qName)1003 public void endElement(String uri, String localName, String qName) throws SAXException { 1004 assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName; 1005 switch (qName) { 1006 case "calendar": 1007 assert !(currentContainer instanceof Entry); 1008 currentCalendarType = null; 1009 break; 1010 1011 case "defaultNumberingSystem": 1012 if (currentContainer instanceof StringEntry) { 1013 defaultNumberingSystem = (String) putIfEntry(); 1014 } else { 1015 defaultNumberingSystem = null; 1016 } 1017 break; 1018 1019 case "timeZoneNames": 1020 zonePrefix = null; 1021 break; 1022 1023 case "generic": 1024 case "standard": 1025 case "daylight": 1026 case "exemplarCity": 1027 if (zonePrefix != null && (currentContainer instanceof Entry)) { 1028 @SuppressWarnings("unchecked") 1029 Map<String, String> valmap = (Map<String, String>) get(zonePrefix + getContainerKey()); 1030 Entry<?> entry = (Entry<?>) currentContainer; 1031 if (qName.equals("exemplarCity")) { 1032 put(CLDRConverter.EXEMPLAR_CITY_PREFIX + getContainerKey(), (String) entry.getValue()); 1033 } else { 1034 valmap.put(entry.getKey(), (String) entry.getValue()); 1035 } 1036 } 1037 break; 1038 1039 case "monthWidth": 1040 case "dayWidth": 1041 case "dayPeriodWidth": 1042 case "quarterWidth": 1043 currentWidth = ""; 1044 putIfEntry(); 1045 break; 1046 1047 case "monthContext": 1048 case "dayContext": 1049 case "dayPeriodContext": 1050 case "quarterContext": 1051 currentContext = ""; 1052 putIfEntry(); 1053 break; 1054 case "decimalFormatLength": 1055 currentStyle = ""; 1056 putIfEntry(); 1057 break; 1058 case "currencyFormats": 1059 case "decimalFormats": 1060 case "percentFormats": 1061 case "symbols": 1062 currentNumberingSystem = ""; 1063 putIfEntry(); 1064 break; 1065 default: 1066 putIfEntry(); 1067 } 1068 currentContainer = currentContainer.getParent(); 1069 } 1070 putIfEntry()1071 private Object putIfEntry() { 1072 if (currentContainer instanceof AliasEntry) { 1073 Entry<?> entry = (Entry<?>) currentContainer; 1074 String containerqName = entry.getParent().getqName(); 1075 if (containerqName.equals("decimalFormatLength")) { 1076 String srcKey = toJDKKey(containerqName, "", currentStyle); 1077 String targetKey = getTarget(entry.getKey(), "", "", ""); 1078 CLDRConverter.aliases.put(srcKey, targetKey); 1079 } else if (containerqName.equals("currencyFormat") || 1080 containerqName.equals("percentFormat")) { 1081 KeyContainer kc = (KeyContainer)entry.getParent(); 1082 CLDRConverter.aliases.put( 1083 toJDKKey(containerqName, "", kc.getKey()), 1084 getTarget(entry.getKey(), "", "", "") 1085 ); 1086 } else { 1087 Set<String> keyNames = populateAliasKeys(containerqName, currentContext, currentWidth); 1088 if (!keyNames.isEmpty()) { 1089 for (String keyName : keyNames) { 1090 String[] tmp = keyName.split(",", 3); 1091 String calType = currentCalendarType.lname(); 1092 String src = calType+"."+tmp[0]; 1093 String target = getTarget( 1094 entry.getKey(), 1095 calType, 1096 tmp[1].length()>0 ? tmp[1] : currentContext, 1097 tmp[2].length()>0 ? tmp[2] : currentWidth); 1098 if (target.substring(target.lastIndexOf('.')+1).equals(containerqName)) { 1099 target = target.substring(0, target.indexOf('.'))+"."+tmp[0]; 1100 } 1101 CLDRConverter.aliases.put(src.replaceFirst("^gregorian.", ""), 1102 target.replaceFirst("^gregorian.", "")); 1103 } 1104 } 1105 } 1106 } else if (currentContainer instanceof Entry) { 1107 Entry<?> entry = (Entry<?>) currentContainer; 1108 Object value = entry.getValue(); 1109 if (value != null) { 1110 String key = entry.getKey(); 1111 // Tweak for MonthNames for the root locale, Needed for 1112 // SimpleDateFormat.format()/parse() roundtrip. 1113 if (id.equals("root") && key.startsWith("MonthNames")) { 1114 value = new DateFormatSymbols(Locale.US).getShortMonths(); 1115 } 1116 return put(entry.getKey(), value); 1117 } 1118 } 1119 return null; 1120 } 1121 convertOldKeyName(String key)1122 public String convertOldKeyName(String key) { 1123 // Explicitly obtained from "alias" attribute in each "key" element. 1124 switch (key) { 1125 case "calendar": 1126 return "ca"; 1127 case "currency": 1128 return "cu"; 1129 case "collation": 1130 return "co"; 1131 case "numbers": 1132 return "nu"; 1133 case "timezone": 1134 return "tz"; 1135 default: 1136 return key; 1137 } 1138 } 1139 addNumberingScript(String script)1140 private void addNumberingScript(String script) { 1141 @SuppressWarnings("unchecked") 1142 List<String> numberingScripts = (List<String>) get("numberingScripts"); 1143 if (numberingScripts == null) { 1144 numberingScripts = new ArrayList<>(); 1145 put("numberingScripts", numberingScripts); 1146 } 1147 if (!numberingScripts.contains(script)) { 1148 numberingScripts.add(script); 1149 } 1150 } 1151 } 1152