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