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