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             default:
 294                 pushIgnoredContainer(qName);
 295                 break;
 296             }
 297             break;
 298         case "dayPeriod":
 299             // for FormatData
 300             // add to string array entry of AmPmMarkers element
 301             if (attributes.getValue("alt") == null) {
 302                 switch (attributes.getValue("type")) {
 303                 case "am":
 304                     pushStringArrayElement(qName, attributes, 0);
 305                     break;
 306                 case "pm":
 307                     pushStringArrayElement(qName, attributes, 1);
 308                     break;
 309                 default:
 310                     pushIgnoredContainer(qName);
 311                     break;
 312                 }
 313             } else {
 314                 // discard alt values
 315                 pushIgnoredContainer(qName);
 316             }
 317             break;
 318         case "eraNames":
 319             // CLDR era names are inconsistent in terms of their lengths. For example,
 320             // the full names of Japanese imperial eras are eraAbbr, while the full names
 321             // of the Julian eras are eraNames.
 322             if (currentCalendarType == null) {
 323                 assert currentContainer instanceof IgnoredContainer;
 324                 pushIgnoredContainer(qName);
 325             } else {
 326                 String key = currentCalendarType.keyElementName() + "long.Eras"; // for now
 327                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
 328             }
 329             break;
 330         case "eraAbbr":
 331             // for FormatData
 332             // create string array entry
 333             if (currentCalendarType == null) {
 334                 assert currentContainer instanceof IgnoredContainer;
 335                 pushIgnoredContainer(qName);
 336             } else {
 337                 String key = currentCalendarType.keyElementName() + "Eras";
 338                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
 339             }
 340             break;
 341         case "eraNarrow":
 342             // mainly used for the Japanese imperial calendar
 343             if (currentCalendarType == null) {
 344                 assert currentContainer instanceof IgnoredContainer;
 345                 pushIgnoredContainer(qName);
 346             } else {
 347                 String key = currentCalendarType.keyElementName() + "narrow.Eras";
 348                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
 349             }
 350             break;
 351         case "era":
 352             // for FormatData
 353             // add to string array entry of eraAbbr element
 354             if (currentCalendarType == null) {
 355                 assert currentContainer instanceof IgnoredContainer;
 356                 pushIgnoredContainer(qName);
 357             } else {
 358                 int index = Integer.parseInt(attributes.getValue("type"));
 359                 index = currentCalendarType.normalizeEraIndex(index);
 360                 if (index >= 0) {
 361                     pushStringArrayElement(qName, attributes, index);
 362                 } else {
 363                     pushIgnoredContainer(qName);
 364                 }
 365                 if (currentContainer.getParent() == null) {
 366                     throw new InternalError("currentContainer: null parent");
 367                 }
 368             }
 369             break;
 370         case "quarterContext":
 371             {
 372                 // for FormatData
 373                 // need to keep stand-alone and format, to allow for inheritance in CLDR
 374                 String type = attributes.getValue("type");
 375                 if ("stand-alone".equals(type) || "format".equals(type)) {
 376                     currentContext = type;
 377                     pushKeyContainer(qName, attributes, type);
 378                 } else {
 379                     pushIgnoredContainer(qName);
 380                 }
 381             }
 382             break;
 383         case "quarterWidth":
 384             {
 385                 // for FormatData
 386                 // keep info about the context type so we can sort out inheritance later
 387                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 388                 currentWidth = attributes.getValue("type");
 389                 switch (currentWidth) {
 390                 case "wide":
 391                     pushStringArrayEntry(qName, attributes, prefix + "QuarterNames/" + getContainerKey(), 4);
 392                     break;
 393                 case "abbreviated":
 394                     pushStringArrayEntry(qName, attributes, prefix + "QuarterAbbreviations/" + getContainerKey(), 4);
 395                     break;
 396                 case "narrow":
 397                     pushStringArrayEntry(qName, attributes, prefix + "QuarterNarrows/" + getContainerKey(), 4);
 398                     break;
 399                 default:
 400                     pushIgnoredContainer(qName);
 401                     break;
 402                 }
 403             }
 404             break;
 405         case "quarter":
 406             // for FormatData
 407             // add to string array entry of quarterWidth element
 408             pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1);
 409             break;
 410 
 411         //
 412         // Time zone names
 413         //
 414         case "timeZoneNames":
 415             pushContainer(qName, attributes);
 416             break;
 417         case "zone":
 418             {
 419                 String tzid = attributes.getValue("type"); // Olson tz id
 420                 zonePrefix = CLDRConverter.TIMEZONE_ID_PREFIX;
 421                 put(zonePrefix + tzid, new HashMap<String, String>());
 422                 pushKeyContainer(qName, attributes, tzid);
 423             }
 424             break;
 425         case "metazone":
 426             {
 427                 String zone = attributes.getValue("type"); // LDML meta zone id
 428                 zonePrefix = CLDRConverter.METAZONE_ID_PREFIX;
 429                 put(zonePrefix + zone, new HashMap<String, String>());
 430                 pushKeyContainer(qName, attributes, zone);
 431             }
 432             break;
 433         case "long":
 434             zoneNameStyle = "long";
 435             pushContainer(qName, attributes);
 436             break;
 437         case "short":
 438             zoneNameStyle = "short";
 439             pushContainer(qName, attributes);
 440             break;
 441         case "generic":  // generic name
 442         case "standard": // standard time name
 443         case "daylight": // daylight saving (summer) time name
 444             pushStringEntry(qName, attributes, CLDRConverter.ZONE_NAME_PREFIX + qName + "." + zoneNameStyle);
 445             break;
 446         case "exemplarCity":  // not used in JDK
 447             pushIgnoredContainer(qName);
 448             break;
 449 
 450         //
 451         // Number format information
 452         //
 453         case "decimalFormatLength":
 454             if (attributes.getValue("type") == null) {
 455                 // skipping type="short" data
 456                 // for FormatData
 457                 // copy string for later assembly into NumberPatterns
 458                 pushStringEntry(qName, attributes, "NumberPatterns/decimal");
 459             } else {
 460                 pushIgnoredContainer(qName);
 461             }
 462             break;
 463         case "currencyFormat":
 464             // for FormatData
 465             // copy string for later assembly into NumberPatterns
 466             if (attributes.getValue("type").equals("standard")) {
 467             pushStringEntry(qName, attributes, "NumberPatterns/currency");
 468             } else {
 469                 pushIgnoredContainer(qName);
 470             }
 471             break;
 472         case "percentFormat":
 473             // for FormatData
 474             // copy string for later assembly into NumberPatterns
 475             if (attributes.getValue("type").equals("standard")) {
 476             pushStringEntry(qName, attributes, "NumberPatterns/percent");
 477             } else {
 478                 pushIgnoredContainer(qName);
 479             }
 480             break;
 481         case "defaultNumberingSystem":
 482             // default numbering system if multiple numbering systems are used.
 483             pushStringEntry(qName, attributes, "DefaultNumberingSystem");
 484             break;
 485         case "symbols":
 486             // for FormatData
 487             // look up numberingSystems
 488             symbols: {
 489                 String script = attributes.getValue("numberSystem");
 490                 if (script == null) {
 491                     // Has no script. Just ignore.
 492                     pushIgnoredContainer(qName);
 493                     break;
 494                 }
 495 
 496                 // Use keys as <script>."NumberElements/<symbol>"
 497                 currentNumberingSystem = script + ".";
 498                 String digits = CLDRConverter.handlerNumbering.get(script);
 499                 if (digits == null) {
 500                     throw new InternalError("null digits for " + script);
 501                 }
 502                 if (Character.isSurrogate(digits.charAt(0))) {
 503                     // DecimalFormatSymbols doesn't support supplementary characters as digit zero.
 504                     pushIgnoredContainer(qName);
 505                     break;
 506                 }
 507                 // in case digits are in the reversed order, reverse back the order.
 508                 if (digits.charAt(0) > digits.charAt(digits.length() - 1)) {
 509                     StringBuilder sb = new StringBuilder(digits);
 510                     digits = sb.reverse().toString();
 511                 }
 512                 // Check if the order is sequential.
 513                 char c0 = digits.charAt(0);
 514                 for (int i = 1; i < digits.length(); i++) {
 515                     if (digits.charAt(i) != c0 + i) {
 516                         pushIgnoredContainer(qName);
 517                         break symbols;
 518                     }
 519                 }
 520                 @SuppressWarnings("unchecked")
 521                 List<String> numberingScripts = (List<String>) get("numberingScripts");
 522                 if (numberingScripts == null) {
 523                     numberingScripts = new ArrayList<>();
 524                     put("numberingScripts", numberingScripts);
 525                 }
 526                 numberingScripts.add(script);
 527                 put(currentNumberingSystem + "NumberElements/zero", digits.substring(0, 1));
 528                 pushContainer(qName, attributes);
 529             }
 530             break;
 531         case "decimal":
 532             // for FormatData
 533             // copy string for later assembly into NumberElements
 534             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/decimal");
 535             break;
 536         case "group":
 537             // for FormatData
 538             // copy string for later assembly into NumberElements
 539             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/group");
 540             break;
 541         case "list":
 542             // for FormatData
 543             // copy string for later assembly into NumberElements
 544             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/list");
 545             break;
 546         case "percentSign":
 547             // for FormatData
 548             // copy string for later assembly into NumberElements
 549             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/percent");
 550             break;
 551         case "nativeZeroDigit":
 552             // for FormatData
 553             // copy string for later assembly into NumberElements
 554             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/zero");
 555             break;
 556         case "patternDigit":
 557             // for FormatData
 558             // copy string for later assembly into NumberElements
 559             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/pattern");
 560             break;
 561         case "plusSign":
 562             // TODO: DecimalFormatSymbols doesn't support plusSign
 563             pushIgnoredContainer(qName);
 564             break;
 565         case "minusSign":
 566             // for FormatData
 567             // copy string for later assembly into NumberElements
 568             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/minus");
 569             break;
 570         case "exponential":
 571             // for FormatData
 572             // copy string for later assembly into NumberElements
 573             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/exponential");
 574             break;
 575         case "perMille":
 576             // for FormatData
 577             // copy string for later assembly into NumberElements
 578             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/permille");
 579             break;
 580         case "infinity":
 581             // for FormatData
 582             // copy string for later assembly into NumberElements
 583             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/infinity");
 584             break;
 585         case "nan":
 586             // for FormatData
 587             // copy string for later assembly into NumberElements
 588             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/nan");
 589             break;
 590         case "timeFormatLength":
 591             {
 592                 // for FormatData
 593                 // copy string for later assembly into DateTimePatterns
 594                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 595                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-time");
 596             }
 597             break;
 598         case "dateFormatLength":
 599             {
 600                 // for FormatData
 601                 // copy string for later assembly into DateTimePatterns
 602                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 603                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-date");
 604             }
 605             break;
 606         case "dateTimeFormatLength":
 607             {
 608                 // for FormatData
 609                 // copy string for later assembly into DateTimePatterns
 610                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 611                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime");
 612             }
 613             break;
 614         case "localizedPatternChars":
 615             {
 616                 // for FormatData
 617                 // copy string for later adaptation to JRE use
 618                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
 619                 pushStringEntry(qName, attributes, prefix + "DateTimePatternChars");
 620             }
 621             break;
 622 
 623         // "alias" for root
 624         case "alias":
 625             {
 626                 if (id.equals("root") &&
 627                         !isIgnored(attributes) &&
 628                         currentCalendarType != null &&
 629                         !currentCalendarType.lname().startsWith("islamic-")) { // ignore Islamic variants
 630                     pushAliasEntry(qName, attributes, attributes.getValue("path"));
 631                 } else {
 632                     pushIgnoredContainer(qName);
 633                 }
 634             }
 635             break;
 636 
 637         default:
 638             // treat anything else as a container
 639             pushContainer(qName, attributes);
 640             break;
 641         }
 642     }
 643 
 644     private static final String[] CONTEXTS = {"stand-alone", "format"};
 645     private static final String[] WIDTHS = {"wide", "narrow", "abbreviated"};
 646     private static final String[] LENGTHS = {"full", "long", "medium", "short"};
 647 
 648     private void populateWidthAlias(String type, Set<String> keys) {
 649         for (String context : CONTEXTS) {
 650             for (String width : WIDTHS) {
 651                 String keyName = toJDKKey(type+"Width", context, width);
 652                 if (keyName.length() > 0) {
 653                     keys.add(keyName + "," + context + "," + width);
 654                 }
 655             }
 656         }
 657     }
 658 
 659     private void populateFormatLengthAlias(String type, Set<String> keys) {
 660         for (String length: LENGTHS) {
 661             String keyName = toJDKKey(type+"FormatLength", currentContext, length);
 662             if (keyName.length() > 0) {
 663                 keys.add(keyName + "," + currentContext + "," + length);
 664             }
 665         }
 666     }
 667 
 668     private Set<String> populateAliasKeys(String qName, String context, String width) {
 669         HashSet<String> ret = new HashSet<>();
 670         String keyName = qName;
 671 
 672         switch (qName) {
 673         case "monthWidth":
 674         case "dayWidth":
 675         case "quarterWidth":
 676         case "dayPeriodWidth":
 677         case "dateFormatLength":
 678         case "timeFormatLength":
 679         case "dateTimeFormatLength":
 680         case "eraNames":
 681         case "eraAbbr":
 682         case "eraNarrow":
 683             ret.add(toJDKKey(qName, context, width) + "," + context + "," + width);
 684             break;
 685         case "days":
 686             populateWidthAlias("day", ret);
 687             break;
 688         case "months":
 689             populateWidthAlias("month", ret);
 690             break;
 691         case "quarters":
 692             populateWidthAlias("quarter", ret);
 693             break;
 694         case "dayPeriods":
 695             populateWidthAlias("dayPeriod", ret);
 696             break;
 697         case "eras":
 698             ret.add(toJDKKey("eraNames", context, width) + "," + context + "," + width);
 699             ret.add(toJDKKey("eraAbbr", context, width) + "," + context + "," + width);
 700             ret.add(toJDKKey("eraNarrow", context, width) + "," + context + "," + width);
 701             break;
 702         case "dateFormats":
 703             populateFormatLengthAlias("date", ret);
 704             break;
 705         case "timeFormats":
 706             populateFormatLengthAlias("time", ret);
 707             break;
 708         default:
 709             break;
 710         }
 711         return ret;
 712     }
 713 
 714     private String translateWidthAlias(String qName, String context, String width) {
 715         String keyName = qName;
 716         String type = Character.toUpperCase(qName.charAt(0)) + qName.substring(1, qName.indexOf("Width"));
 717 
 718         switch (width) {
 719         case "wide":
 720             keyName = type + "Names/" + context;
 721             break;
 722         case "abbreviated":
 723             keyName = type + "Abbreviations/" + context;
 724             break;
 725         case "narrow":
 726             keyName = type + "Narrows/" + context;
 727             break;
 728         default:
 729             assert false;
 730         }
 731 
 732         return keyName;
 733     }
 734 
 735     private String toJDKKey(String containerqName, String context, String type) {
 736         String keyName = containerqName;
 737 
 738         switch (containerqName) {
 739         case "monthWidth":
 740         case "dayWidth":
 741         case "quarterWidth":
 742             keyName = translateWidthAlias(keyName, context, type);
 743             break;
 744         case "dayPeriodWidth":
 745             switch (type) {
 746             case "wide":
 747                 keyName = "AmPmMarkers/" + context;
 748                 break;
 749             case "narrow":
 750                 keyName = "narrow.AmPmMarkers/" + context;
 751                 break;
 752             case "abbreviated":
 753                 keyName = "";
 754                 break;
 755             }
 756             break;
 757         case "dateFormatLength":
 758         case "timeFormatLength":
 759         case "dateTimeFormatLength":
 760             keyName = "DateTimePatterns/" +
 761                 type + "-" +
 762                 keyName.substring(0, keyName.indexOf("FormatLength"));
 763             break;
 764         case "eraNames":
 765             keyName = "long.Eras";
 766             break;
 767         case "eraAbbr":
 768             keyName = "Eras";
 769             break;
 770         case "eraNarrow":
 771             keyName = "narrow.Eras";
 772             break;
 773         case "dateFormats":
 774         case "timeFormats":
 775         case "days":
 776         case "months":
 777         case "quarters":
 778         case "dayPeriods":
 779         case "eras":
 780             break;
 781         default:
 782             keyName = "";
 783             break;
 784         }
 785 
 786         return keyName;
 787     }
 788 
 789     private String getTarget(String path, String calType, String context, String width) {
 790         // Target qName
 791         int lastSlash = path.lastIndexOf('/');
 792         String qName = path.substring(lastSlash+1);
 793         int bracket = qName.indexOf('[');
 794         if (bracket != -1) {
 795             qName = qName.substring(0, bracket);
 796         }
 797 
 798         // calType
 799         String typeKey = "/calendar[@type='";
 800         int start = path.indexOf(typeKey);
 801         if (start != -1) {
 802             calType = path.substring(start+typeKey.length(), path.indexOf("']", start));
 803         }
 804 
 805         // context
 806         typeKey = "Context[@type='";
 807         start = path.indexOf(typeKey);
 808         if (start != -1) {
 809             context = (path.substring(start+typeKey.length(), path.indexOf("']", start)));
 810         }
 811 
 812         // width
 813         typeKey = "Width[@type='";
 814         start = path.indexOf(typeKey);
 815         if (start != -1) {
 816             width = path.substring(start+typeKey.length(), path.indexOf("']", start));
 817         }
 818 
 819         return calType + "." + toJDKKey(qName, context, width);
 820     }
 821 
 822     @Override
 823     public void endElement(String uri, String localName, String qName) throws SAXException {
 824         assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName;
 825         switch (qName) {
 826         case "calendar":
 827             assert !(currentContainer instanceof Entry);
 828             currentCalendarType = null;
 829             break;
 830 
 831         case "defaultNumberingSystem":
 832             if (currentContainer instanceof StringEntry) {
 833                 defaultNumberingSystem = ((StringEntry) currentContainer).getValue();
 834                 assert defaultNumberingSystem != null;
 835                 put(((StringEntry) currentContainer).getKey(), defaultNumberingSystem);
 836             } else {
 837                 defaultNumberingSystem = null;
 838             }
 839             break;
 840 
 841         case "timeZoneNames":
 842             zonePrefix = null;
 843             break;
 844 
 845         case "generic":
 846         case "standard":
 847         case "daylight":
 848             if (zonePrefix != null && (currentContainer instanceof Entry)) {
 849                 @SuppressWarnings("unchecked")
 850                 Map<String, String> valmap = (Map<String, String>) get(zonePrefix + getContainerKey());
 851                 Entry<?> entry = (Entry<?>) currentContainer;
 852                 valmap.put(entry.getKey(), (String) entry.getValue());
 853             }
 854             break;
 855 
 856         case "monthWidth":
 857         case "dayWidth":
 858         case "dayPeriodWidth":
 859         case "quarterWidth":
 860             currentWidth = "";
 861             putIfEntry();
 862             break;
 863 
 864         case "monthContext":
 865         case "dayContext":
 866         case "dayPeriodContext":
 867         case "quarterContext":
 868             currentContext = "";
 869             putIfEntry();
 870             break;
 871 
 872         default:
 873             putIfEntry();
 874         }
 875         currentContainer = currentContainer.getParent();
 876     }
 877 
 878     private void putIfEntry() {
 879         if (currentContainer instanceof AliasEntry) {
 880             Entry<?> entry = (Entry<?>) currentContainer;
 881             String containerqName = entry.getParent().getqName();
 882             Set<String> keyNames = populateAliasKeys(containerqName, currentContext, currentWidth);
 883             if (!keyNames.isEmpty()) {
 884                 for (String keyName : keyNames) {
 885                     String[] tmp = keyName.split(",", 3);
 886                     String calType = currentCalendarType.lname();
 887                     String src = calType+"."+tmp[0];
 888                     String target = getTarget(
 889                                 entry.getKey(),
 890                                 calType,
 891                                 tmp[1].length()>0 ? tmp[1] : currentContext,
 892                                 tmp[2].length()>0 ? tmp[2] : currentWidth);
 893                     if (target.substring(target.lastIndexOf('.')+1).equals(containerqName)) {
 894                         target = target.substring(0, target.indexOf('.'))+"."+tmp[0];
 895                     }
 896                     CLDRConverter.aliases.put(src.replaceFirst("^gregorian.", ""),
 897                                               target.replaceFirst("^gregorian.", ""));
 898                 }
 899             }
 900         } else if (currentContainer instanceof Entry) {
 901                 Entry<?> entry = (Entry<?>) currentContainer;
 902                 Object value = entry.getValue();
 903                 if (value != null) {
 904                     String key = entry.getKey();
 905                     // Tweak for MonthNames for the root locale, Needed for
 906                     // SimpleDateFormat.format()/parse() roundtrip.
 907                     if (id.equals("root") && key.startsWith("MonthNames")) {
 908                         value = new DateFormatSymbols(Locale.US).getShortMonths();
 909                     }
 910                     put(entry.getKey(), value);
 911                 }
 912             }
 913         }
 914     }