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