1 /*
   2  * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package build.tools.cldrconverter;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.text.DateFormatSymbols;
  31 import java.util.ArrayList;
  32 import java.util.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,
 512                     currentNumberingSystem + "NumberPatterns/decimal");
 513                 currentStyle = type;
 514             } else {
 515                 switch (type) {
 516                     case "short":
 517                     case "long":
 518                         // considering "short" and long for
 519                         // compact number formatting patterns
 520                         pushKeyContainer(qName, attributes, type);
 521                         currentStyle = type;
 522                         break;
 523                     default:
 524                         pushIgnoredContainer(qName);
 525                         break;
 526                 }
 527             }
 528             break;
 529         case "decimalFormat":
 530             if(currentStyle == null) {
 531                 pushContainer(qName, attributes);
 532             } else {
 533                 switch (currentStyle) {
 534                     case "short":
 535                         pushStringListEntry(qName, attributes,
 536                                 currentStyle+".CompactNumberPatterns");
 537                         break;
 538                     case "long":
 539                         pushStringListEntry(qName, attributes,
 540                                 currentStyle+".CompactNumberPatterns");
 541                         break;
 542                     default:
 543                         pushIgnoredContainer(qName);
 544                         break;
 545                 }
 546             }
 547             break;
 548         case "pattern":
 549             String containerName = currentContainer.getqName();
 550             if (containerName.equals("decimalFormat")) {
 551                 if (currentStyle == null) {
 552                     pushContainer(qName, attributes);
 553                 } else {
 554                     // The compact number patterns parsing assumes that the order
 555                     // of patterns are always in the increasing order of their
 556                     // type attribute i.e. type = 1000...
 557                     // Between the inflectional forms for a type (e.g.
 558                     // count = "one" and count = "other" for type = 1000), it is
 559                     // assumed that the count = "one" always appears before
 560                     // count = "other"
 561                     switch (currentStyle) {
 562                         case "short":
 563                         case "long":
 564                             String count = attributes.getValue("count");
 565                             // first pattern of count = "one" or count = "other"
 566                             if ((count.equals("one") || count.equals("other"))
 567                                     && compactCount.equals("")) {
 568                                 compactCount = count;
 569                                 pushStringListElement(qName, attributes,
 570                                         (int) Math.log10(Double.parseDouble(attributes.getValue("type"))));
 571                             } else if ((count.equals("one") || count.equals("other"))
 572                                     && compactCount.equals(count)) {
 573                                 // extract patterns with similar "count"
 574                                 // attribute value
 575                                 pushStringListElement(qName, attributes,
 576                                         (int) Math.log10(Double.parseDouble(attributes.getValue("type"))));
 577                             } else {
 578                                 pushIgnoredContainer(qName);
 579                             }
 580                             break;
 581                         default:
 582                             pushIgnoredContainer(qName);
 583                             break;
 584                     }
 585                 }
 586             } else {
 587                 pushContainer(qName, attributes);
 588             }
 589             break;
 590         case "currencyFormats":
 591         case "decimalFormats":
 592         case "percentFormats":
 593             {
 594                 String script = attributes.getValue("numberSystem");
 595                 if (script != null) {
 596                     addNumberingScript(script);
 597                     currentNumberingSystem = script + ".";
 598                 }
 599                 pushContainer(qName, attributes);
 600             }
 601             break;
 602         case "currencyFormatLength":
 603             if (attributes.getValue("type") == null) {
 604                 // skipping type="short" data
 605                 // for FormatData
 606                 pushContainer(qName, attributes);
 607             } else {
 608                 pushIgnoredContainer(qName);
 609             }
 610             break;
 611         case "currencyFormat":
 612             {
 613                 // for FormatData
 614                 // copy string for later assembly into NumberPatterns
 615                 String cfStyle = attributes.getValue("type");
 616                 if (cfStyle.equals("standard")) {
 617                     pushStringEntry(qName, attributes,
 618                         currentNumberingSystem + "NumberPatterns/currency");
 619                 } else if (cfStyle.equals("accounting")) {
 620                     pushStringEntry(qName, attributes,
 621                         currentNumberingSystem + "NumberPatterns/accounting");
 622                 } else {
 623                     pushIgnoredContainer(qName);
 624                 }
 625             }
 626             break;
 627         case "percentFormat":
 628             // for FormatData
 629             // copy string for later assembly into NumberPatterns
 630             if (attributes.getValue("type").equals("standard")) {
 631                 pushStringEntry(qName, attributes,
 632                     currentNumberingSystem + "NumberPatterns/percent");
 633             } else {
 634                 pushIgnoredContainer(qName);
 635             }
 636             break;
 637         case "defaultNumberingSystem":
 638             // default numbering system if multiple numbering systems are used.
 639             pushStringEntry(qName, attributes, "DefaultNumberingSystem");
 640             break;
 641         case "symbols":
 642             // for FormatData
 643             // look up numberingSystems
 644             symbols: {
 645                 String script = attributes.getValue("numberSystem");
 646                 if (script == null) {
 647                     // Has no script. Just ignore.
 648                     pushIgnoredContainer(qName);
 649                     break;
 650                 }
 651 
 652                 // Use keys as <script>."NumberElements/<symbol>"
 653                 currentNumberingSystem = script + ".";
 654                 String digits = CLDRConverter.handlerNumbering.get(script);
 655                 if (digits == null) {
 656                     pushIgnoredContainer(qName);
 657                     break;
 658                 }
 659 
 660                 addNumberingScript(script);
 661                 put(currentNumberingSystem + "NumberElements/zero", digits.substring(0, 1));
 662                 pushContainer(qName, attributes);
 663             }
 664             break;
 665         case "decimal":
 666             // for FormatData
 667             // copy string for later assembly into NumberElements
 668             if (currentContainer.getqName().equals("symbols")) {
 669                 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/decimal");
 670             } else {
 671                 pushIgnoredContainer(qName);
 672             }
 673             break;
 674         case "group":
 675             // for FormatData
 676             // copy string for later assembly into NumberElements
 677             if (currentContainer.getqName().equals("symbols")) {
 678                 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/group");
 679             } else {
 680                 pushIgnoredContainer(qName);
 681             }
 682             break;
 683         case "list":
 684             // for FormatData
 685             // copy string for later assembly into NumberElements
 686             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/list");
 687             break;
 688         case "percentSign":
 689             // for FormatData
 690             // copy string for later assembly into NumberElements
 691             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/percent");
 692             break;
 693         case "nativeZeroDigit":
 694             // for FormatData
 695             // copy string for later assembly into NumberElements
 696             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/zero");
 697             break;
 698         case "patternDigit":
 699             // for FormatData
 700             // copy string for later assembly into NumberElements
 701             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/pattern");
 702             break;
 703         case "plusSign":
 704             // TODO: DecimalFormatSymbols doesn't support plusSign
 705             pushIgnoredContainer(qName);
 706             break;
 707         case "minusSign":
 708             // for FormatData
 709             // copy string for later assembly into NumberElements
 710             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/minus");
 711             break;
 712         case "exponential":
 713             // for FormatData
 714             // copy string for later assembly into NumberElements
 715             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/exponential");
 716             break;
 717         case "perMille":
 718             // for FormatData
 719             // copy string for later assembly into NumberElements
 720             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/permille");
 721             break;
 722         case "infinity":
 723             // for FormatData
 724             // copy string for later assembly into NumberElements
 725             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/infinity");
 726             break;
 727         case "nan":
 728             // for FormatData
 729             // copy string for later assembly into NumberElements
 730             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/nan");
 731             break;
 732         case "timeFormatLength":
 733             {
 734                 // for FormatData
 735                 // copy string for later assembly into DateTimePatterns
 736                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 737                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-time");
 738             }
 739             break;
 740         case "dateFormatLength":
 741             {
 742                 // for FormatData
 743                 // copy string for later assembly into DateTimePatterns
 744                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 745                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-date");
 746             }
 747             break;
 748         case "dateTimeFormatLength":
 749             {
 750                 // for FormatData
 751                 // copy string for later assembly into DateTimePatterns
 752                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 753                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime");
 754             }
 755             break;
 756         case "localizedPatternChars":
 757             {
 758                 // for FormatData
 759                 // copy string for later adaptation to JRE use
 760                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 761                 pushStringEntry(qName, attributes, prefix + "DateTimePatternChars");
 762             }
 763             break;
 764 
 765         // "alias" for root
 766         case "alias":
 767             {
 768                 if (id.equals("root") && !isIgnored(attributes)
 769                         && ((currentContainer.getqName().equals("decimalFormatLength"))
 770                         || (currentCalendarType != null && !currentCalendarType.lname().startsWith("islamic-")))) { // ignore islamic variants
 771                     pushAliasEntry(qName, attributes, attributes.getValue("path"));
 772                 } else {
 773                     pushIgnoredContainer(qName);
 774                 }
 775             }
 776             break;
 777 
 778         default:
 779             // treat anything else as a container
 780             pushContainer(qName, attributes);
 781             break;
 782         }
 783     }
 784 
 785     private static final String[] CONTEXTS = {"stand-alone", "format"};
 786     private static final String[] WIDTHS = {"wide", "narrow", "abbreviated"};
 787     private static final String[] LENGTHS = {"full", "long", "medium", "short"};
 788 
 789     private void populateWidthAlias(String type, Set<String> keys) {
 790         for (String context : CONTEXTS) {
 791             for (String width : WIDTHS) {
 792                 String keyName = toJDKKey(type+"Width", context, width);
 793                 if (keyName.length() > 0) {
 794                     keys.add(keyName + "," + context + "," + width);
 795                 }
 796             }
 797         }
 798     }
 799 
 800     private void populateFormatLengthAlias(String type, Set<String> keys) {
 801         for (String length: LENGTHS) {
 802             String keyName = toJDKKey(type+"FormatLength", currentContext, length);
 803             if (keyName.length() > 0) {
 804                 keys.add(keyName + "," + currentContext + "," + length);
 805             }
 806         }
 807     }
 808 
 809     private Set<String> populateAliasKeys(String qName, String context, String width) {
 810         HashSet<String> ret = new HashSet<>();
 811         String keyName = qName;
 812 
 813         switch (qName) {
 814         case "monthWidth":
 815         case "dayWidth":
 816         case "quarterWidth":
 817         case "dayPeriodWidth":
 818         case "dateFormatLength":
 819         case "timeFormatLength":
 820         case "dateTimeFormatLength":
 821         case "eraNames":
 822         case "eraAbbr":
 823         case "eraNarrow":
 824             ret.add(toJDKKey(qName, context, width) + "," + context + "," + width);
 825             break;
 826         case "days":
 827             populateWidthAlias("day", ret);
 828             break;
 829         case "months":
 830             populateWidthAlias("month", ret);
 831             break;
 832         case "quarters":
 833             populateWidthAlias("quarter", ret);
 834             break;
 835         case "dayPeriods":
 836             populateWidthAlias("dayPeriod", ret);
 837             break;
 838         case "eras":
 839             ret.add(toJDKKey("eraNames", context, width) + "," + context + "," + width);
 840             ret.add(toJDKKey("eraAbbr", context, width) + "," + context + "," + width);
 841             ret.add(toJDKKey("eraNarrow", context, width) + "," + context + "," + width);
 842             break;
 843         case "dateFormats":
 844             populateFormatLengthAlias("date", ret);
 845             break;
 846         case "timeFormats":
 847             populateFormatLengthAlias("time", ret);
 848             break;
 849         default:
 850             break;
 851         }
 852         return ret;
 853     }
 854 
 855     private String translateWidthAlias(String qName, String context, String width) {
 856         String keyName = qName;
 857         String type = Character.toUpperCase(qName.charAt(0)) + qName.substring(1, qName.indexOf("Width"));
 858 
 859         switch (width) {
 860         case "wide":
 861             keyName = type + "Names/" + context;
 862             break;
 863         case "abbreviated":
 864             keyName = type + "Abbreviations/" + context;
 865             break;
 866         case "narrow":
 867             keyName = type + "Narrows/" + context;
 868             break;
 869         default:
 870             assert false;
 871         }
 872 
 873         return keyName;
 874     }
 875 
 876     private String toJDKKey(String containerqName, String context, String type) {
 877         String keyName = containerqName;
 878 
 879         switch (containerqName) {
 880         case "monthWidth":
 881         case "dayWidth":
 882         case "quarterWidth":
 883             keyName = translateWidthAlias(keyName, context, type);
 884             break;
 885         case "dayPeriodWidth":
 886             switch (type) {
 887             case "wide":
 888                 keyName = "AmPmMarkers/" + context;
 889                 break;
 890             case "narrow":
 891                 keyName = "narrow.AmPmMarkers/" + context;
 892                 break;
 893             case "abbreviated":
 894                 keyName = "abbreviated.AmPmMarkers/" + context;
 895                 break;
 896             }
 897             break;
 898         case "dateFormatLength":
 899         case "timeFormatLength":
 900         case "dateTimeFormatLength":
 901             keyName = "DateTimePatterns/" +
 902                 type + "-" +
 903                 keyName.substring(0, keyName.indexOf("FormatLength"));
 904             break;
 905         case "eraNames":
 906             keyName = "long.Eras";
 907             break;
 908         case "eraAbbr":
 909             keyName = "Eras";
 910             break;
 911         case "eraNarrow":
 912             keyName = "narrow.Eras";
 913             break;
 914         case "dateFormats":
 915         case "timeFormats":
 916         case "days":
 917         case "months":
 918         case "quarters":
 919         case "dayPeriods":
 920         case "eras":
 921             break;
 922         case "decimalFormatLength": // used for compact number formatting patterns
 923             keyName = type + ".CompactNumberPatterns";
 924             break;
 925         default:
 926             keyName = "";
 927             break;
 928         }
 929 
 930         return keyName;
 931     }
 932 
 933     private String getTarget(String path, String calType, String context, String width) {
 934         // Target qName
 935         int lastSlash = path.lastIndexOf('/');
 936         String qName = path.substring(lastSlash+1);
 937         int bracket = qName.indexOf('[');
 938         if (bracket != -1) {
 939             qName = qName.substring(0, bracket);
 940         }
 941 
 942         // calType
 943         String typeKey = "/calendar[@type='";
 944         int start = path.indexOf(typeKey);
 945         if (start != -1) {
 946             calType = path.substring(start+typeKey.length(), path.indexOf("']", start));
 947         }
 948 
 949         // context
 950         typeKey = "Context[@type='";
 951         start = path.indexOf(typeKey);
 952         if (start != -1) {
 953             context = (path.substring(start+typeKey.length(), path.indexOf("']", start)));
 954         }
 955 
 956         // width
 957         typeKey = "Width[@type='";
 958         start = path.indexOf(typeKey);
 959         if (start != -1) {
 960             width = path.substring(start+typeKey.length(), path.indexOf("']", start));
 961         }
 962 
 963         // used for compact number formatting patterns aliases
 964         typeKey = "decimalFormatLength[@type='";
 965         start = path.indexOf(typeKey);
 966         if (start != -1) {
 967             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
 968             return toJDKKey(qName, "", style);
 969         }
 970 
 971         return calType + "." + toJDKKey(qName, context, width);
 972     }
 973 
 974     @Override
 975     public void endElement(String uri, String localName, String qName) throws SAXException {
 976         assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName;
 977         switch (qName) {
 978         case "calendar":
 979             assert !(currentContainer instanceof Entry);
 980             currentCalendarType = null;
 981             break;
 982 
 983         case "defaultNumberingSystem":
 984             if (currentContainer instanceof StringEntry) {
 985                 defaultNumberingSystem = ((StringEntry) currentContainer).getValue();
 986                 assert defaultNumberingSystem != null;
 987                 put(((StringEntry) currentContainer).getKey(), defaultNumberingSystem);
 988             } else {
 989                 defaultNumberingSystem = null;
 990             }
 991             break;
 992 
 993         case "timeZoneNames":
 994             zonePrefix = null;
 995             break;
 996 
 997         case "generic":
 998         case "standard":
 999         case "daylight":
1000         case "exemplarCity":
1001             if (zonePrefix != null && (currentContainer instanceof Entry)) {
1002                 @SuppressWarnings("unchecked")
1003                 Map<String, String> valmap = (Map<String, String>) get(zonePrefix + getContainerKey());
1004                 Entry<?> entry = (Entry<?>) currentContainer;
1005                 if (qName.equals("exemplarCity")) {
1006                     put(CLDRConverter.EXEMPLAR_CITY_PREFIX + getContainerKey(), (String) entry.getValue());
1007                 } else {
1008                     valmap.put(entry.getKey(), (String) entry.getValue());
1009                 }
1010             }
1011             break;
1012 
1013         case "monthWidth":
1014         case "dayWidth":
1015         case "dayPeriodWidth":
1016         case "quarterWidth":
1017             currentWidth = "";
1018             putIfEntry();
1019             break;
1020 
1021         case "monthContext":
1022         case "dayContext":
1023         case "dayPeriodContext":
1024         case "quarterContext":
1025             currentContext = "";
1026             putIfEntry();
1027             break;
1028         case "decimalFormatLength":
1029             currentStyle = "";
1030             compactCount = "";
1031             putIfEntry();
1032             break;
1033         case "currencyFormats":
1034         case "decimalFormats":
1035         case "percentFormats":
1036         case "symbols":
1037             currentNumberingSystem = "";
1038             putIfEntry();
1039             break;
1040         default:
1041             putIfEntry();
1042         }
1043         currentContainer = currentContainer.getParent();
1044     }
1045 
1046     private void putIfEntry() {
1047         if (currentContainer instanceof AliasEntry) {
1048             Entry<?> entry = (Entry<?>) currentContainer;
1049             String containerqName = entry.getParent().getqName();
1050             if (containerqName.equals("decimalFormatLength")) {
1051                 String srcKey = toJDKKey(containerqName, "", currentStyle);
1052                 String targetKey = getTarget(entry.getKey(), "", "", "");
1053                 CLDRConverter.aliases.put(srcKey, targetKey);
1054             } else {
1055                 Set<String> keyNames = populateAliasKeys(containerqName, currentContext, currentWidth);
1056                 if (!keyNames.isEmpty()) {
1057                     for (String keyName : keyNames) {
1058                         String[] tmp = keyName.split(",", 3);
1059                         String calType = currentCalendarType.lname();
1060                         String src = calType+"."+tmp[0];
1061                         String target = getTarget(
1062                                     entry.getKey(),
1063                                     calType,
1064                                     tmp[1].length()>0 ? tmp[1] : currentContext,
1065                                     tmp[2].length()>0 ? tmp[2] : currentWidth);
1066                         if (target.substring(target.lastIndexOf('.')+1).equals(containerqName)) {
1067                             target = target.substring(0, target.indexOf('.'))+"."+tmp[0];
1068                         }
1069                         CLDRConverter.aliases.put(src.replaceFirst("^gregorian.", ""),
1070                                                   target.replaceFirst("^gregorian.", ""));
1071                     }
1072                 }
1073             }
1074         } else if (currentContainer instanceof Entry) {
1075             Entry<?> entry = (Entry<?>) currentContainer;
1076             Object value = entry.getValue();
1077             if (value != null) {
1078                 String key = entry.getKey();
1079                 // Tweak for MonthNames for the root locale, Needed for
1080                 // SimpleDateFormat.format()/parse() roundtrip.
1081                 if (id.equals("root") && key.startsWith("MonthNames")) {
1082                     value = new DateFormatSymbols(Locale.US).getShortMonths();
1083                 }
1084                 put(entry.getKey(), value);
1085             }
1086         }
1087     }
1088 
1089     public String convertOldKeyName(String key) {
1090         // Explicitly obtained from "alias" attribute in each "key" element.
1091         switch (key) {
1092             case "calendar":
1093                 return "ca";
1094             case "currency":
1095                 return "cu";
1096             case "collation":
1097                 return "co";
1098             case "numbers":
1099                 return "nu";
1100             case "timezone":
1101                 return "tz";
1102             default:
1103                 return key;
1104         }
1105     }
1106 
1107     private void addNumberingScript(String script) {
1108         @SuppressWarnings("unchecked")
1109         List<String> numberingScripts = (List<String>) get("numberingScripts");
1110         if (numberingScripts == null) {
1111             numberingScripts = new ArrayList<>();
1112             put("numberingScripts", numberingScripts);
1113         }
1114         if (!numberingScripts.contains(script)) {
1115             numberingScripts.add(script);
1116         }
1117     }
1118 }