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