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