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