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