1 /* 2 * Copyright (c) 2012, 2019, 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 // for FormatData 306 // need to keep stand-alone and format, to allow for multiple inheritance in CLDR 307 { 308 String type = attributes.getValue("type"); 309 if ("stand-alone".equals(type) || "format".equals(type)) { 310 currentContext = type; 311 pushKeyContainer(qName, attributes, type); 312 } else { 313 pushIgnoredContainer(qName); 314 } 315 } 316 break; 317 case "dayPeriodWidth": 318 // for FormatData 319 // create string array entry for am/pm. only keeping wide 320 currentWidth = attributes.getValue("type"); 321 switch (currentWidth) { 322 case "wide": 323 pushStringArrayEntry(qName, attributes, "AmPmMarkers/" + getContainerKey(), 2); 324 break; 325 case "narrow": 326 pushStringArrayEntry(qName, attributes, "narrow.AmPmMarkers/" + getContainerKey(), 2); 327 break; 328 case "abbreviated": 329 pushStringArrayEntry(qName, attributes, "abbreviated.AmPmMarkers/" + getContainerKey(), 2); 330 break; 331 default: 332 pushIgnoredContainer(qName); 333 break; 334 } 335 break; 336 case "dayPeriod": 337 // for FormatData 338 // add to string array entry of AmPmMarkers element 339 if (attributes.getValue("alt") == null) { 340 switch (attributes.getValue("type")) { 341 case "am": 342 pushStringArrayElement(qName, attributes, 0); 343 break; 344 case "pm": 345 pushStringArrayElement(qName, attributes, 1); 346 break; 347 default: 348 pushIgnoredContainer(qName); 349 break; 350 } 351 } else { 352 // discard alt values 353 pushIgnoredContainer(qName); 354 } 355 break; 356 case "eraNames": 357 // CLDR era names are inconsistent in terms of their lengths. For example, 358 // the full names of Japanese imperial eras are eraAbbr, while the full names 359 // of the Julian eras are eraNames. 360 if (currentCalendarType == null) { 361 assert currentContainer instanceof IgnoredContainer; 362 pushIgnoredContainer(qName); 363 } else { 364 String key = currentCalendarType.keyElementName() + "long.Eras"; // for now 365 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName)); 366 } 367 break; 368 case "eraAbbr": 369 // for FormatData 370 // create string array entry 371 if (currentCalendarType == null) { 372 assert currentContainer instanceof IgnoredContainer; 373 pushIgnoredContainer(qName); 374 } else { 375 String key = currentCalendarType.keyElementName() + "Eras"; 376 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName)); 377 } 378 break; 379 case "eraNarrow": 380 // mainly used for the Japanese imperial calendar 381 if (currentCalendarType == null) { 382 assert currentContainer instanceof IgnoredContainer; 383 pushIgnoredContainer(qName); 384 } else { 385 String key = currentCalendarType.keyElementName() + "narrow.Eras"; 386 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName)); 387 } 388 break; 389 case "era": 390 // for FormatData 391 // add to string array entry of eraAbbr element 392 if (currentCalendarType == null) { 393 assert currentContainer instanceof IgnoredContainer; 394 pushIgnoredContainer(qName); 395 } else { 396 int index = Integer.parseInt(attributes.getValue("type")); 397 index = currentCalendarType.normalizeEraIndex(index); 398 if (index >= 0) { 399 pushStringArrayElement(qName, attributes, index); 400 } else { 401 pushIgnoredContainer(qName); 402 } 403 if (currentContainer.getParent() == null) { 404 throw new InternalError("currentContainer: null parent"); 405 } 406 } 407 break; 408 case "quarterContext": 409 { 410 // for FormatData 411 // need to keep stand-alone and format, to allow for inheritance in CLDR 412 String type = attributes.getValue("type"); 413 if ("stand-alone".equals(type) || "format".equals(type)) { 414 currentContext = type; 415 pushKeyContainer(qName, attributes, type); 416 } else { 417 pushIgnoredContainer(qName); 418 } 419 } 420 break; 421 case "quarterWidth": 422 { 423 // for FormatData 424 // keep info about the context type so we can sort out inheritance later 425 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 426 currentWidth = attributes.getValue("type"); 427 switch (currentWidth) { 428 case "wide": 429 pushStringArrayEntry(qName, attributes, prefix + "QuarterNames/" + getContainerKey(), 4); 430 break; 431 case "abbreviated": 432 pushStringArrayEntry(qName, attributes, prefix + "QuarterAbbreviations/" + getContainerKey(), 4); 433 break; 434 case "narrow": 435 pushStringArrayEntry(qName, attributes, prefix + "QuarterNarrows/" + getContainerKey(), 4); 436 break; 437 default: 438 pushIgnoredContainer(qName); 439 break; 440 } 441 } 442 break; 443 case "quarter": 444 // for FormatData 445 // add to string array entry of quarterWidth element 446 pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1); 447 break; 448 449 // 450 // Time zone names 451 // 452 case "timeZoneNames": 453 pushContainer(qName, attributes); 454 break; 455 case "hourFormat": 456 pushStringEntry(qName, attributes, "timezone.hourFormat"); 457 break; 458 case "gmtFormat": 459 pushStringEntry(qName, attributes, "timezone.gmtFormat"); 460 break; 461 case "gmtZeroFormat": 462 pushStringEntry(qName, attributes, "timezone.gmtZeroFormat"); 463 break; 464 case "regionFormat": 465 { 466 String type = attributes.getValue("type"); 467 pushStringEntry(qName, attributes, "timezone.regionFormat" + 468 (type == null ? "" : "." + type)); 469 } 470 break; 471 case "zone": 472 { 473 String tzid = attributes.getValue("type"); // Olson tz id 474 zonePrefix = CLDRConverter.TIMEZONE_ID_PREFIX; 475 put(zonePrefix + tzid, new HashMap<String, String>()); 476 pushKeyContainer(qName, attributes, tzid); 477 } 478 break; 479 case "metazone": 480 { 481 String zone = attributes.getValue("type"); // LDML meta zone id 482 zonePrefix = CLDRConverter.METAZONE_ID_PREFIX; 483 put(zonePrefix + zone, new HashMap<String, String>()); 484 pushKeyContainer(qName, attributes, zone); 485 } 486 break; 487 case "long": 488 zoneNameStyle = "long"; 489 pushContainer(qName, attributes); 490 break; 491 case "short": 492 zoneNameStyle = "short"; 493 pushContainer(qName, attributes); 494 break; 495 case "generic": // generic name 496 case "standard": // standard time name 497 case "daylight": // daylight saving (summer) time name 498 pushStringEntry(qName, attributes, CLDRConverter.ZONE_NAME_PREFIX + qName + "." + zoneNameStyle); 499 break; 500 case "exemplarCity": 501 pushStringEntry(qName, attributes, CLDRConverter.EXEMPLAR_CITY_PREFIX); 502 break; 503 504 // 505 // Number format information 506 // 507 case "decimalFormatLength": 508 String type = attributes.getValue("type"); 509 if (null == type) { 510 // format data for decimal number format 511 pushStringEntry(qName, attributes, 512 currentNumberingSystem + "NumberPatterns/decimal"); 513 currentStyle = type; 514 } else { 515 switch (type) { 516 case "short": 517 case "long": 518 // considering "short" and long for 519 // compact number formatting patterns 520 pushKeyContainer(qName, attributes, type); 521 currentStyle = type; 522 break; 523 default: 524 pushIgnoredContainer(qName); 525 break; 526 } 527 } 528 break; 529 case "decimalFormat": 530 if(currentStyle == null) { 531 pushContainer(qName, attributes); 532 } else { 533 switch (currentStyle) { 534 case "short": 535 case "long": 536 pushStringListEntry(qName, attributes, 537 currentStyle+".CompactNumberPatterns"); 538 break; 539 default: 540 pushIgnoredContainer(qName); 541 break; 542 } 543 } 544 break; 545 case "currencyFormat": 546 case "percentFormat": 547 pushKeyContainer(qName, attributes, attributes.getValue("type")); 548 break; 549 550 case "pattern": 551 String containerName = currentContainer.getqName(); 552 switch (containerName) { 553 case "currencyFormat": 554 case "percentFormat": 555 { 556 // for FormatData 557 // copy string for later assembly into NumberPatterns 558 if (currentContainer instanceof KeyContainer) { 559 String fStyle = ((KeyContainer)currentContainer).getKey(); 560 if (fStyle.equals("standard")) { 561 pushStringEntry(qName, attributes, 562 currentNumberingSystem + "NumberPatterns/" + containerName.replaceFirst("Format", "")); 563 } else if (fStyle.equals("accounting") && containerName.equals("currencyFormat")) { 564 pushStringEntry(qName, attributes, 565 currentNumberingSystem + "NumberPatterns/accounting"); 566 } else { 567 pushIgnoredContainer(qName); 568 } 569 } else { 570 pushIgnoredContainer(qName); 571 } 572 } 573 break; 574 575 case "decimalFormat": 576 if (currentStyle == null) { 577 pushContainer(qName, attributes); 578 } else { 579 switch (currentStyle) { 580 case "short": 581 case "long": 582 pushStringListElement(qName, attributes, 583 (int) Math.log10(Double.parseDouble(attributes.getValue("type"))), 584 attributes.getValue("count")); 585 break; 586 default: 587 pushIgnoredContainer(qName); 588 break; 589 } 590 } 591 break; 592 default: 593 pushContainer(qName, attributes); 594 break; 595 } 596 break; 597 case "currencyFormats": 598 case "decimalFormats": 599 case "percentFormats": 600 { 601 String script = attributes.getValue("numberSystem"); 602 if (script != null) { 603 addNumberingScript(script); 604 currentNumberingSystem = script + "."; 605 } 606 pushContainer(qName, attributes); 607 } 608 break; 609 case "currencyFormatLength": 610 if (attributes.getValue("type") == null) { 611 // skipping type="short" data 612 // for FormatData 613 pushContainer(qName, attributes); 614 } else { 615 pushIgnoredContainer(qName); 616 } 617 break; 618 case "defaultNumberingSystem": 619 // default numbering system if multiple numbering systems are used. 620 pushStringEntry(qName, attributes, "DefaultNumberingSystem"); 621 break; 622 case "symbols": 623 // for FormatData 624 // look up numberingSystems 625 symbols: { 626 String script = attributes.getValue("numberSystem"); 627 if (script == null) { 628 // Has no script. Just ignore. 629 pushIgnoredContainer(qName); 630 break; 631 } 632 633 // Use keys as <script>."NumberElements/<symbol>" 634 currentNumberingSystem = script + "."; 635 String digits = CLDRConverter.handlerNumbering.get(script); 636 if (digits == null) { 637 pushIgnoredContainer(qName); 638 break; 639 } 640 641 addNumberingScript(script); 642 put(currentNumberingSystem + "NumberElements/zero", digits.substring(0, 1)); 643 pushContainer(qName, attributes); 644 } 645 break; 646 case "decimal": 647 // for FormatData 648 // copy string for later assembly into NumberElements 649 if (currentContainer.getqName().equals("symbols")) { 650 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/decimal"); 651 } else { 652 pushIgnoredContainer(qName); 653 } 654 break; 655 case "group": 656 // for FormatData 657 // copy string for later assembly into NumberElements 658 if (currentContainer.getqName().equals("symbols")) { 659 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/group"); 660 } else { 661 pushIgnoredContainer(qName); 662 } 663 break; 664 case "list": 665 // for FormatData 666 // copy string for later assembly into NumberElements 667 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/list"); 668 break; 669 case "percentSign": 670 // for FormatData 671 // copy string for later assembly into NumberElements 672 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/percent"); 673 break; 674 case "nativeZeroDigit": 675 // for FormatData 676 // copy string for later assembly into NumberElements 677 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/zero"); 678 break; 679 case "patternDigit": 680 // for FormatData 681 // copy string for later assembly into NumberElements 682 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/pattern"); 683 break; 684 case "plusSign": 685 // TODO: DecimalFormatSymbols doesn't support plusSign 686 pushIgnoredContainer(qName); 687 break; 688 case "minusSign": 689 // for FormatData 690 // copy string for later assembly into NumberElements 691 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/minus"); 692 break; 693 case "exponential": 694 // for FormatData 695 // copy string for later assembly into NumberElements 696 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/exponential"); 697 break; 698 case "perMille": 699 // for FormatData 700 // copy string for later assembly into NumberElements 701 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/permille"); 702 break; 703 case "infinity": 704 // for FormatData 705 // copy string for later assembly into NumberElements 706 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/infinity"); 707 break; 708 case "nan": 709 // for FormatData 710 // copy string for later assembly into NumberElements 711 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/nan"); 712 break; 713 case "timeFormatLength": 714 { 715 // for FormatData 716 // copy string for later assembly into DateTimePatterns 717 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 718 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-time"); 719 } 720 break; 721 case "dateFormatLength": 722 { 723 // for FormatData 724 // copy string for later assembly into DateTimePatterns 725 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 726 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-date"); 727 } 728 break; 729 case "dateTimeFormatLength": 730 { 731 // for FormatData 732 // copy string for later assembly into DateTimePatterns 733 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 734 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime"); 735 } 736 break; 737 case "localizedPatternChars": 738 { 739 // for FormatData 740 // copy string for later adaptation to JRE use 741 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); 742 pushStringEntry(qName, attributes, prefix + "DateTimePatternChars"); 743 } 744 break; 745 746 // "alias" for root 747 case "alias": 748 { 749 if (id.equals("root") && !isIgnored(attributes) 750 && ((currentContainer.getqName().equals("decimalFormatLength")) 751 || (currentContainer.getqName().equals("currencyFormat")) 752 || (currentContainer.getqName().equals("percentFormat")) 753 || (currentCalendarType != null && !currentCalendarType.lname().startsWith("islamic-")))) { // ignore islamic variants 754 pushAliasEntry(qName, attributes, attributes.getValue("path")); 755 } else { 756 pushIgnoredContainer(qName); 757 } 758 } 759 break; 760 761 default: 762 // treat anything else as a container 763 pushContainer(qName, attributes); 764 break; 765 } 766 } 767 768 private static final String[] CONTEXTS = {"stand-alone", "format"}; 769 private static final String[] WIDTHS = {"wide", "narrow", "abbreviated"}; 770 private static final String[] LENGTHS = {"full", "long", "medium", "short"}; 771 populateWidthAlias(String type, Set<String> keys)772 private void populateWidthAlias(String type, Set<String> keys) { 773 for (String context : CONTEXTS) { 774 for (String width : WIDTHS) { 775 String keyName = toJDKKey(type+"Width", context, width); 776 if (keyName.length() > 0) { 777 keys.add(keyName + "," + context + "," + width); 778 } 779 } 780 } 781 } 782 populateFormatLengthAlias(String type, Set<String> keys)783 private void populateFormatLengthAlias(String type, Set<String> keys) { 784 for (String length: LENGTHS) { 785 String keyName = toJDKKey(type+"FormatLength", currentContext, length); 786 if (keyName.length() > 0) { 787 keys.add(keyName + "," + currentContext + "," + length); 788 } 789 } 790 } 791 populateAliasKeys(String qName, String context, String width)792 private Set<String> populateAliasKeys(String qName, String context, String width) { 793 HashSet<String> ret = new HashSet<>(); 794 String keyName = qName; 795 796 switch (qName) { 797 case "monthWidth": 798 case "dayWidth": 799 case "quarterWidth": 800 case "dayPeriodWidth": 801 case "dateFormatLength": 802 case "timeFormatLength": 803 case "dateTimeFormatLength": 804 case "eraNames": 805 case "eraAbbr": 806 case "eraNarrow": 807 ret.add(toJDKKey(qName, context, width) + "," + context + "," + width); 808 break; 809 case "days": 810 populateWidthAlias("day", ret); 811 break; 812 case "months": 813 populateWidthAlias("month", ret); 814 break; 815 case "quarters": 816 populateWidthAlias("quarter", ret); 817 break; 818 case "dayPeriods": 819 populateWidthAlias("dayPeriod", ret); 820 break; 821 case "eras": 822 ret.add(toJDKKey("eraNames", context, width) + "," + context + "," + width); 823 ret.add(toJDKKey("eraAbbr", context, width) + "," + context + "," + width); 824 ret.add(toJDKKey("eraNarrow", context, width) + "," + context + "," + width); 825 break; 826 case "dateFormats": 827 populateFormatLengthAlias("date", ret); 828 break; 829 case "timeFormats": 830 populateFormatLengthAlias("time", ret); 831 break; 832 default: 833 break; 834 } 835 return ret; 836 } 837 translateWidthAlias(String qName, String context, String width)838 private String translateWidthAlias(String qName, String context, String width) { 839 String keyName = qName; 840 String type = Character.toUpperCase(qName.charAt(0)) + qName.substring(1, qName.indexOf("Width")); 841 842 switch (width) { 843 case "wide": 844 keyName = type + "Names/" + context; 845 break; 846 case "abbreviated": 847 keyName = type + "Abbreviations/" + context; 848 break; 849 case "narrow": 850 keyName = type + "Narrows/" + context; 851 break; 852 default: 853 assert false; 854 } 855 856 return keyName; 857 } 858 toJDKKey(String containerqName, String context, String type)859 private String toJDKKey(String containerqName, String context, String type) { 860 String keyName = containerqName; 861 862 switch (containerqName) { 863 case "monthWidth": 864 case "dayWidth": 865 case "quarterWidth": 866 keyName = translateWidthAlias(keyName, context, type); 867 break; 868 case "dayPeriodWidth": 869 switch (type) { 870 case "wide": 871 keyName = "AmPmMarkers/" + context; 872 break; 873 case "narrow": 874 keyName = "narrow.AmPmMarkers/" + context; 875 break; 876 case "abbreviated": 877 keyName = "abbreviated.AmPmMarkers/" + context; 878 break; 879 } 880 break; 881 case "dateFormatLength": 882 case "timeFormatLength": 883 case "dateTimeFormatLength": 884 keyName = "DateTimePatterns/" + 885 type + "-" + 886 keyName.substring(0, keyName.indexOf("FormatLength")); 887 break; 888 case "eraNames": 889 keyName = "long.Eras"; 890 break; 891 case "eraAbbr": 892 keyName = "Eras"; 893 break; 894 case "eraNarrow": 895 keyName = "narrow.Eras"; 896 break; 897 case "dateFormats": 898 case "timeFormats": 899 case "days": 900 case "months": 901 case "quarters": 902 case "dayPeriods": 903 case "eras": 904 break; 905 case "decimalFormatLength": // used for compact number formatting patterns 906 keyName = type + ".CompactNumberPatterns"; 907 break; 908 case "currencyFormat": 909 case "percentFormat": 910 keyName = currentNumberingSystem + 911 "NumberPatterns/" + 912 (type.equals("standard") ? containerqName.replaceFirst("Format", "") : type); 913 break; 914 default: 915 keyName = ""; 916 break; 917 } 918 919 return keyName; 920 } 921 getTarget(String path, String calType, String context, String width)922 private String getTarget(String path, String calType, String context, String width) { 923 // Target qName 924 int lastSlash = path.lastIndexOf('/'); 925 String qName = path.substring(lastSlash+1); 926 int bracket = qName.indexOf('['); 927 if (bracket != -1) { 928 qName = qName.substring(0, bracket); 929 } 930 931 // calType 932 String typeKey = "/calendar[@type='"; 933 int start = path.indexOf(typeKey); 934 if (start != -1) { 935 calType = path.substring(start+typeKey.length(), path.indexOf("']", start)); 936 } 937 938 // context 939 typeKey = "Context[@type='"; 940 start = path.indexOf(typeKey); 941 if (start != -1) { 942 context = (path.substring(start+typeKey.length(), path.indexOf("']", start))); 943 } 944 945 // width 946 typeKey = "Width[@type='"; 947 start = path.indexOf(typeKey); 948 if (start != -1) { 949 width = path.substring(start+typeKey.length(), path.indexOf("']", start)); 950 } 951 952 // used for compact number formatting patterns aliases 953 typeKey = "decimalFormatLength[@type='"; 954 start = path.indexOf(typeKey); 955 if (start != -1) { 956 String style = path.substring(start + typeKey.length(), path.indexOf("']", start)); 957 return toJDKKey(qName, "", style); 958 } 959 960 // currencyFormat 961 typeKey = "currencyFormat[@type='"; 962 start = path.indexOf(typeKey); 963 if (start != -1) { 964 String style = path.substring(start + typeKey.length(), path.indexOf("']", start)); 965 return toJDKKey(qName, "", style); 966 } 967 968 // percentFormat 969 typeKey = "percentFormat[@type='"; 970 start = path.indexOf(typeKey); 971 if (start != -1) { 972 String style = path.substring(start + typeKey.length(), path.indexOf("']", start)); 973 return toJDKKey(qName, "", style); 974 } 975 976 return calType + "." + toJDKKey(qName, context, width); 977 } 978 979 @Override endElement(String uri, String localName, String qName)980 public void endElement(String uri, String localName, String qName) throws SAXException { 981 assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName; 982 switch (qName) { 983 case "calendar": 984 assert !(currentContainer instanceof Entry); 985 currentCalendarType = null; 986 break; 987 988 case "defaultNumberingSystem": 989 if (currentContainer instanceof StringEntry) { 990 defaultNumberingSystem = (String) putIfEntry(); 991 } else { 992 defaultNumberingSystem = null; 993 } 994 break; 995 996 case "timeZoneNames": 997 zonePrefix = null; 998 break; 999 1000 case "generic": 1001 case "standard": 1002 case "daylight": 1003 case "exemplarCity": 1004 if (zonePrefix != null && (currentContainer instanceof Entry)) { 1005 @SuppressWarnings("unchecked") 1006 Map<String, String> valmap = (Map<String, String>) get(zonePrefix + getContainerKey()); 1007 Entry<?> entry = (Entry<?>) currentContainer; 1008 if (qName.equals("exemplarCity")) { 1009 put(CLDRConverter.EXEMPLAR_CITY_PREFIX + getContainerKey(), (String) entry.getValue()); 1010 } else { 1011 valmap.put(entry.getKey(), (String) entry.getValue()); 1012 } 1013 } 1014 break; 1015 1016 case "monthWidth": 1017 case "dayWidth": 1018 case "dayPeriodWidth": 1019 case "quarterWidth": 1020 currentWidth = ""; 1021 putIfEntry(); 1022 break; 1023 1024 case "monthContext": 1025 case "dayContext": 1026 case "dayPeriodContext": 1027 case "quarterContext": 1028 currentContext = ""; 1029 putIfEntry(); 1030 break; 1031 case "decimalFormatLength": 1032 currentStyle = ""; 1033 putIfEntry(); 1034 break; 1035 case "currencyFormats": 1036 case "decimalFormats": 1037 case "percentFormats": 1038 case "symbols": 1039 currentNumberingSystem = ""; 1040 putIfEntry(); 1041 break; 1042 default: 1043 putIfEntry(); 1044 } 1045 currentContainer = currentContainer.getParent(); 1046 } 1047 putIfEntry()1048 private Object putIfEntry() { 1049 if (currentContainer instanceof AliasEntry) { 1050 Entry<?> entry = (Entry<?>) currentContainer; 1051 String containerqName = entry.getParent().getqName(); 1052 if (containerqName.equals("decimalFormatLength")) { 1053 String srcKey = toJDKKey(containerqName, "", currentStyle); 1054 String targetKey = getTarget(entry.getKey(), "", "", ""); 1055 CLDRConverter.aliases.put(srcKey, targetKey); 1056 } else if (containerqName.equals("currencyFormat") || 1057 containerqName.equals("percentFormat")) { 1058 KeyContainer kc = (KeyContainer)entry.getParent(); 1059 CLDRConverter.aliases.put( 1060 toJDKKey(containerqName, "", kc.getKey()), 1061 getTarget(entry.getKey(), "", "", "") 1062 ); 1063 } else { 1064 Set<String> keyNames = populateAliasKeys(containerqName, currentContext, currentWidth); 1065 if (!keyNames.isEmpty()) { 1066 for (String keyName : keyNames) { 1067 String[] tmp = keyName.split(",", 3); 1068 String calType = currentCalendarType.lname(); 1069 String src = calType+"."+tmp[0]; 1070 String target = getTarget( 1071 entry.getKey(), 1072 calType, 1073 tmp[1].length()>0 ? tmp[1] : currentContext, 1074 tmp[2].length()>0 ? tmp[2] : currentWidth); 1075 if (target.substring(target.lastIndexOf('.')+1).equals(containerqName)) { 1076 target = target.substring(0, target.indexOf('.'))+"."+tmp[0]; 1077 } 1078 CLDRConverter.aliases.put(src.replaceFirst("^gregorian.", ""), 1079 target.replaceFirst("^gregorian.", "")); 1080 } 1081 } 1082 } 1083 } else if (currentContainer instanceof Entry) { 1084 Entry<?> entry = (Entry<?>) currentContainer; 1085 Object value = entry.getValue(); 1086 if (value != null) { 1087 String key = entry.getKey(); 1088 // Tweak for MonthNames for the root locale, Needed for 1089 // SimpleDateFormat.format()/parse() roundtrip. 1090 if (id.equals("root") && key.startsWith("MonthNames")) { 1091 value = new DateFormatSymbols(Locale.US).getShortMonths(); 1092 } 1093 return put(entry.getKey(), value); 1094 } 1095 } 1096 return null; 1097 } 1098 convertOldKeyName(String key)1099 public String convertOldKeyName(String key) { 1100 // Explicitly obtained from "alias" attribute in each "key" element. 1101 switch (key) { 1102 case "calendar": 1103 return "ca"; 1104 case "currency": 1105 return "cu"; 1106 case "collation": 1107 return "co"; 1108 case "numbers": 1109 return "nu"; 1110 case "timezone": 1111 return "tz"; 1112 default: 1113 return key; 1114 } 1115 } 1116 addNumberingScript(String script)1117 private void addNumberingScript(String script) { 1118 @SuppressWarnings("unchecked") 1119 List<String> numberingScripts = (List<String>) get("numberingScripts"); 1120 if (numberingScripts == null) { 1121 numberingScripts = new ArrayList<>(); 1122 put("numberingScripts", numberingScripts); 1123 } 1124 if (!numberingScripts.contains(script)) { 1125 numberingScripts.add(script); 1126 } 1127 } 1128 } 1129