1 /*
   2  * Copyright (c) 2012, 2015, 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.util.ArrayList;
  29 import java.util.Arrays;
  30 import java.util.EnumSet;
  31 import java.util.HashMap;
  32 import java.util.Iterator;
  33 import java.util.List;
  34 import java.util.Map;
  35 import java.util.Objects;
  36 
  37 class Bundle {
  38     static enum Type {
  39         LOCALENAMES, CURRENCYNAMES, TIMEZONENAMES, CALENDARDATA, FORMATDATA;
  40 
  41         static EnumSet<Type> ALL_TYPES = EnumSet.of(LOCALENAMES,
  42                                                     CURRENCYNAMES,
  43                                                     TIMEZONENAMES,
  44                                                     CALENDARDATA,
  45                                                     FORMATDATA);
  46     }
  47 
  48     private final static Map<String, Bundle> bundles = new HashMap<>();
  49 
  50     private final static String[] NUMBER_PATTERN_KEYS = {
  51         "NumberPatterns/decimal",
  52         "NumberPatterns/currency",
  53         "NumberPatterns/percent"
  54     };
  55 
  56     private final static String[] NUMBER_ELEMENT_KEYS = {
  57         "NumberElements/decimal",
  58         "NumberElements/group",
  59         "NumberElements/list",
  60         "NumberElements/percent",
  61         "NumberElements/zero",
  62         "NumberElements/pattern",
  63         "NumberElements/minus",
  64         "NumberElements/exponential",
  65         "NumberElements/permille",
  66         "NumberElements/infinity",
  67         "NumberElements/nan"
  68     };
  69 
  70     private final static String[] TIME_PATTERN_KEYS = {
  71         "DateTimePatterns/full-time",
  72         "DateTimePatterns/long-time",
  73         "DateTimePatterns/medium-time",
  74         "DateTimePatterns/short-time",
  75     };
  76 
  77     private final static String[] DATE_PATTERN_KEYS = {
  78         "DateTimePatterns/full-date",
  79         "DateTimePatterns/long-date",
  80         "DateTimePatterns/medium-date",
  81         "DateTimePatterns/short-date",
  82     };
  83 
  84     private final static String[] DATETIME_PATTERN_KEYS = {
  85         "DateTimePatterns/full-dateTime",
  86         "DateTimePatterns/long-dateTime",
  87         "DateTimePatterns/medium-dateTime",
  88         "DateTimePatterns/short-dateTime",
  89     };
  90 
  91     private final static String[] ERA_KEYS = {
  92         "long.Eras",
  93         "Eras",
  94         "narrow.Eras"
  95     };
  96 
  97     // Keys for individual time zone names
  98     private final static String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long";
  99     private final static String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short";
 100     private final static String TZ_STD_LONG_KEY = "timezone.displayname.standard.long";
 101     private final static String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short";
 102     private final static String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long";
 103     private final static String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short";
 104     private final static String[] ZONE_NAME_KEYS = {
 105         TZ_STD_LONG_KEY,
 106         TZ_STD_SHORT_KEY,
 107         TZ_DST_LONG_KEY,
 108         TZ_DST_SHORT_KEY,
 109         TZ_GEN_LONG_KEY,
 110         TZ_GEN_SHORT_KEY
 111     };
 112 
 113     private final String id;
 114     private final String cldrPath;
 115     private final EnumSet<Type> bundleTypes;
 116     private final String currencies;
 117 
 118     static Bundle getBundle(String id) {
 119         return bundles.get(id);
 120     }
 121 
 122     @SuppressWarnings("ConvertToStringSwitch")
 123     Bundle(String id, String cldrPath, String bundles, String currencies) {
 124         this.id = id;
 125         this.cldrPath = cldrPath;
 126         if ("localenames".equals(bundles)) {
 127             bundleTypes = EnumSet.of(Type.LOCALENAMES);
 128         } else if ("currencynames".equals(bundles)) {
 129             bundleTypes = EnumSet.of(Type.CURRENCYNAMES);
 130         } else {
 131             bundleTypes = Type.ALL_TYPES;
 132         }
 133         if (currencies == null) {
 134             currencies = "local";
 135         }
 136         this.currencies = currencies;
 137         addBundle();
 138     }
 139 
 140     private void addBundle() {
 141         Bundle.bundles.put(id, this);
 142     }
 143 
 144     String getID() {
 145         return id;
 146     }
 147 
 148     boolean isRoot() {
 149         return "root".equals(id);
 150     }
 151 
 152     String getCLDRPath() {
 153         return cldrPath;
 154     }
 155 
 156     EnumSet<Type> getBundleTypes() {
 157         return bundleTypes;
 158     }
 159 
 160     String getCurrencies() {
 161         return currencies;
 162     }
 163 
 164     /**
 165      * Generate a map that contains all the data that should be
 166      * visible for the bundle's locale
 167      */
 168     Map<String, Object> getTargetMap() throws Exception {
 169         String[] cldrBundles = getCLDRPath().split(",");
 170 
 171         // myMap contains resources for id.
 172         Map<String, Object> myMap = new HashMap<>();
 173         int index;
 174         for (index = 0; index < cldrBundles.length; index++) {
 175             if (cldrBundles[index].equals(id)) {
 176                 myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index]));
 177                 CLDRConverter.handleAliases(myMap);
 178                 break;
 179             }
 180         }
 181 
 182         // parentsMap contains resources from id's parents.
 183         Map<String, Object> parentsMap = new HashMap<>();
 184         for (int i = cldrBundles.length - 1; i > index; i--) {
 185             if (!("no".equals(cldrBundles[i]) || cldrBundles[i].startsWith("no_"))) {
 186                 parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i]));
 187                 CLDRConverter.handleAliases(parentsMap);
 188             }
 189         }
 190         // Duplicate myMap as parentsMap for "root" so that the
 191         // fallback works. This is a hack, though.
 192         if ("root".equals(cldrBundles[0])) {
 193             assert parentsMap.isEmpty();
 194             parentsMap.putAll(myMap);
 195         }
 196 
 197         // merge individual strings into arrays
 198 
 199         // if myMap has any of the NumberPatterns members
 200         for (String k : NUMBER_PATTERN_KEYS) {
 201             if (myMap.containsKey(k)) {
 202                 String[] numberPatterns = new String[NUMBER_PATTERN_KEYS.length];
 203                 for (int i = 0; i < NUMBER_PATTERN_KEYS.length; i++) {
 204                     String key = NUMBER_PATTERN_KEYS[i];
 205                     String value = (String) myMap.remove(key);
 206                     if (value == null) {
 207                         value = (String) parentsMap.remove(key);
 208                     }
 209                     if (value.length() == 0) {
 210                         CLDRConverter.warning("empty pattern for " + key);
 211                     }
 212                     numberPatterns[i] = value;
 213                 }
 214                 myMap.put("NumberPatterns", numberPatterns);
 215                 break;
 216             }
 217         }
 218 
 219         // if myMap has any of NUMBER_ELEMENT_KEYS, create a complete NumberElements.
 220         String defaultScript = (String) myMap.get("DefaultNumberingSystem");
 221         @SuppressWarnings("unchecked")
 222         List<String> scripts = (List<String>) myMap.get("numberingScripts");
 223         if (defaultScript == null && scripts != null) {
 224             // Some locale data has no default script for numbering even with mutiple scripts.
 225             // Take the first one as default in that case.
 226             defaultScript = scripts.get(0);
 227             myMap.put("DefaultNumberingSystem", defaultScript);
 228         }
 229         if (scripts != null) {
 230             for (String script : scripts) {
 231                 for (String k : NUMBER_ELEMENT_KEYS) {
 232                     String[] numberElements = new String[NUMBER_ELEMENT_KEYS.length];
 233                     for (int i = 0; i < NUMBER_ELEMENT_KEYS.length; i++) {
 234                         String key = script + "." + NUMBER_ELEMENT_KEYS[i];
 235                         String value = (String) myMap.remove(key);
 236                         if (value == null) {
 237                             if (key.endsWith("/pattern")) {
 238                                 value = "#";
 239                             } else {
 240                                 value = (String) parentsMap.get(key);
 241                                 if (value == null) {
 242                                     // the last resort is "latn"
 243                                     key = "latn." + NUMBER_ELEMENT_KEYS[i];
 244                                     value = (String) parentsMap.get(key);
 245                                     if (value == null) {
 246                                         throw new InternalError("NumberElements: null for " + key);
 247                                     }
 248                                 }
 249                             }
 250                         }
 251                         numberElements[i] = value;
 252                     }
 253                     myMap.put(script + "." + "NumberElements", numberElements);
 254                     break;
 255                 }
 256             }
 257         }
 258 
 259         // another hack: parentsMap is not used for date-time resources.
 260         if ("root".equals(id)) {
 261             parentsMap = null;
 262         }
 263 
 264         for (CalendarType calendarType : CalendarType.values()) {
 265             String calendarPrefix = calendarType.keyElementName();
 266             // handle multiple inheritance for month and day names
 267             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames");
 268             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations");
 269             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNarrows");
 270             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames");
 271             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations");
 272             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNarrows");
 273             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers");
 274             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "narrow.AmPmMarkers");
 275             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNames");
 276             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterAbbreviations");
 277             handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNarrows");
 278 
 279             adjustEraNames(myMap, calendarType);
 280 
 281             handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns");
 282             handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns");
 283             handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns");
 284         }
 285 
 286         // First, weed out any empty timezone or metazone names from myMap.
 287         // Fill in any missing abbreviations if locale is "en".
 288         for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
 289             String key = it.next();
 290             if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)
 291                     || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
 292                 @SuppressWarnings("unchecked")
 293                 Map<String, String> nameMap = (Map<String, String>) myMap.get(key);
 294                 if (nameMap.isEmpty()) {
 295                     // Some zones have only exemplarCity, which become empty.
 296                     // Remove those from the map.
 297                     it.remove();
 298                     continue;
 299                 }
 300 
 301                 if (id.startsWith("en")) {
 302                     fillInAbbrs(key, nameMap);
 303                 }
 304             }
 305         }
 306         for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
 307             String key = it.next();
 308             if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)
 309                     || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
 310                 @SuppressWarnings("unchecked")
 311                 Map<String, String> nameMap = (Map<String, String>) myMap.get(key);
 312                 // Convert key/value pairs to an array.
 313                 String[] names = new String[ZONE_NAME_KEYS.length];
 314                 int ix = 0;
 315                 for (String nameKey : ZONE_NAME_KEYS) {
 316                     String name = nameMap.get(nameKey);
 317                     if (name == null) {
 318                         @SuppressWarnings("unchecked")
 319                         Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key);
 320                         if (parentNames != null) {
 321                             name = parentNames.get(nameKey);
 322                         }
 323                     }
 324                     names[ix++] = name;
 325                 }
 326                 if (hasNulls(names)) {
 327                     String metaKey = toMetaZoneKey(key);
 328                     if (metaKey != null) {
 329                         Object obj = myMap.get(metaKey);
 330                         if (obj instanceof String[]) {
 331                             String[] metaNames = (String[]) obj;
 332                             for (int i = 0; i < names.length; i++) {
 333                                 if (names[i] == null) {
 334                                     names[i] = metaNames[i];
 335                                 }
 336                             }
 337                         } else if (obj instanceof Map) {
 338                             @SuppressWarnings("unchecked")
 339                             Map<String, String> m = (Map<String, String>) obj;
 340                             for (int i = 0; i < names.length; i++) {
 341                                 if (names[i] == null) {
 342                                     names[i] = m.get(ZONE_NAME_KEYS[i]);
 343                                 }
 344                             }
 345                         }
 346                     }
 347                     // If there are still any nulls, try filling in them from en data.
 348                     if (hasNulls(names) && !id.equals("en")) {
 349                         @SuppressWarnings("unchecked")
 350                         String[] enNames = (String[]) Bundle.getBundle("en").getTargetMap().get(key);
 351                         if (enNames == null) {
 352                             if (metaKey != null) {
 353                                 @SuppressWarnings("unchecked")
 354                                 String[] metaNames = (String[]) Bundle.getBundle("en").getTargetMap().get(metaKey);
 355                                 enNames = metaNames;
 356                             }
 357                         }
 358                         if (enNames != null) {
 359                             for (int i = 0; i < names.length; i++) {
 360                                 if (names[i] == null) {
 361                                     names[i] = enNames[i];
 362                                 }
 363                             }
 364                         }
 365                         // If there are still nulls, give up names.
 366                         if (hasNulls(names)) {
 367                             names = null;
 368                         }
 369                     }
 370                 }
 371                 // replace the Map with the array
 372                 if (names != null) {
 373                     myMap.put(key, names);
 374                 } else {
 375                     it.remove();
 376                 }
 377             }
 378         }
 379 
 380         // Remove all duplicates
 381         if (Objects.nonNull(parentsMap)) {
 382             for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
 383                 String key = it.next();
 384                 if (Objects.deepEquals(parentsMap.get(key), myMap.get(key))) {
 385                     it.remove();
 386                 }
 387             }
 388         }
 389 
 390         return myMap;
 391     }
 392 
 393     private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) {
 394         String formatKey = key + "/format";
 395         Object format = map.get(formatKey);
 396         if (format != null) {
 397             map.remove(formatKey);
 398             map.put(key, format);
 399             if (fillInElements(parents, formatKey, format)) {
 400                 map.remove(key);
 401             }
 402         }
 403         String standaloneKey = key + "/stand-alone";
 404         Object standalone = map.get(standaloneKey);
 405         if (standalone != null) {
 406             map.remove(standaloneKey);
 407             String realKey = key;
 408             if (format != null) {
 409                 realKey = "standalone." + key;
 410             }
 411             map.put(realKey, standalone);
 412             if (fillInElements(parents, standaloneKey, standalone)) {
 413                 map.remove(realKey);
 414             }
 415         }
 416     }
 417 
 418     /**
 419      * Fills in any empty elements with its parent element. Returns true if the resulting array is
 420      * identical to its parent array.
 421      *
 422      * @param parents
 423      * @param key
 424      * @param value
 425      * @return true if the resulting array is identical to its parent array.
 426      */
 427     private boolean fillInElements(Map<String, Object> parents, String key, Object value) {
 428         if (parents == null) {
 429             return false;
 430         }
 431         if (value instanceof String[]) {
 432             Object pvalue = parents.get(key);
 433             if (pvalue != null && pvalue instanceof String[]) {
 434                 String[] strings = (String[]) value;
 435                 String[] pstrings = (String[]) pvalue;
 436                 for (int i = 0; i < strings.length; i++) {
 437                     if (strings[i] == null || strings[i].length() == 0) {
 438                         strings[i] = pstrings[i];
 439                     }
 440                 }
 441                 return Arrays.equals(strings, pstrings);
 442             }
 443         }
 444         return false;
 445     }
 446 
 447     /*
 448      * Adjusts String[] for era names because JRE's Calendars use different
 449      * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars.
 450      */
 451     private void adjustEraNames(Map<String, Object> map, CalendarType type) {
 452         String[][] eraNames = new String[ERA_KEYS.length][];
 453         String[] realKeys = new String[ERA_KEYS.length];
 454         int index = 0;
 455         for (String key : ERA_KEYS) {
 456             String realKey = type.keyElementName() + key;
 457             String[] value = (String[]) map.get(realKey);
 458             if (value != null) {
 459                 switch (type) {
 460                 case GREGORIAN:
 461                     break;
 462 
 463                 case JAPANESE:
 464                     {
 465                         String[] newValue = new String[value.length + 1];
 466                         String[] julianEras = (String[]) map.get(key);
 467                         if (julianEras != null && julianEras.length >= 2) {
 468                             newValue[0] = julianEras[1];
 469                         } else {
 470                             newValue[0] = "";
 471                         }
 472                         System.arraycopy(value, 0, newValue, 1, value.length);
 473                         value = newValue;
 474                     }
 475                     break;
 476 
 477                 case BUDDHIST:
 478                     // Replace the value
 479                     value = new String[] {"BC", value[0]};
 480                     break;
 481 
 482                 case ISLAMIC:
 483                     // Replace the value
 484                     value = new String[] {"", value[0]};
 485                     break;
 486                 }
 487                 if (!key.equals(realKey)) {
 488                     map.put(realKey, value);
 489                 }
 490             }
 491             realKeys[index] = realKey;
 492             eraNames[index++] = value;
 493         }
 494         for (int i = 0; i < eraNames.length; i++) {
 495             if (eraNames[i] == null) {
 496                 map.put(realKeys[i], null);
 497             }
 498         }
 499     }
 500 
 501     private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap,
 502                                               CalendarType calendarType, String name) {
 503         String calendarPrefix = calendarType.keyElementName();
 504         for (String k : patternKeys) {
 505             if (myMap.containsKey(calendarPrefix + k)) {
 506                 int len = patternKeys.length;
 507                 List<String> rawPatterns = new ArrayList<>(len);
 508                 List<String> patterns = new ArrayList<>(len);
 509                 for (int i = 0; i < len; i++) {
 510                     String key = calendarPrefix + patternKeys[i];
 511                     String pattern = (String) myMap.remove(key);
 512                     if (pattern == null) {
 513                         pattern = (String) parentsMap.remove(key);
 514                     }
 515                     rawPatterns.add(i, pattern);
 516                     if (pattern != null) {
 517                         patterns.add(i, translateDateFormatLetters(calendarType, pattern));
 518                     } else {
 519                         patterns.add(i, null);
 520                     }
 521                 }
 522                 // If patterns is empty or has any nulls, discard patterns.
 523                 if (patterns.isEmpty()) {
 524                     return;
 525                 }
 526                 String key = calendarPrefix + name;
 527                 if (!rawPatterns.equals(patterns)) {
 528                     myMap.put("java.time." + key, rawPatterns.toArray(new String[len]));
 529                 }
 530                 myMap.put(key, patterns.toArray(new String[len]));
 531                 break;
 532             }
 533         }
 534     }
 535 
 536     private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) {
 537         String pattern = cldrFormat;
 538         int length = pattern.length();
 539         boolean inQuote = false;
 540         StringBuilder jrePattern = new StringBuilder(length);
 541         int count = 0;
 542         char lastLetter = 0;
 543 
 544         for (int i = 0; i < length; i++) {
 545             char c = pattern.charAt(i);
 546 
 547             if (c == '\'') {
 548                 // '' is treated as a single quote regardless of being
 549                 // in a quoted section.
 550                 if ((i + 1) < length) {
 551                     char nextc = pattern.charAt(i + 1);
 552                     if (nextc == '\'') {
 553                         i++;
 554                         if (count != 0) {
 555                             convert(calendarType, lastLetter, count, jrePattern);
 556                             lastLetter = 0;
 557                             count = 0;
 558                         }
 559                         jrePattern.append("''");
 560                         continue;
 561                     }
 562                 }
 563                 if (!inQuote) {
 564                     if (count != 0) {
 565                         convert(calendarType, lastLetter, count, jrePattern);
 566                         lastLetter = 0;
 567                         count = 0;
 568                     }
 569                     inQuote = true;
 570                 } else {
 571                     inQuote = false;
 572                 }
 573                 jrePattern.append(c);
 574                 continue;
 575             }
 576             if (inQuote) {
 577                 jrePattern.append(c);
 578                 continue;
 579             }
 580             if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
 581                 if (count != 0) {
 582                     convert(calendarType, lastLetter, count, jrePattern);
 583                     lastLetter = 0;
 584                     count = 0;
 585                 }
 586                 jrePattern.append(c);
 587                 continue;
 588             }
 589 
 590             if (lastLetter == 0 || lastLetter == c) {
 591                 lastLetter = c;
 592                 count++;
 593                 continue;
 594             }
 595             convert(calendarType, lastLetter, count, jrePattern);
 596             lastLetter = c;
 597             count = 1;
 598         }
 599 
 600         if (inQuote) {
 601             throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat);
 602         }
 603 
 604         if (count != 0) {
 605             convert(calendarType, lastLetter, count, jrePattern);
 606         }
 607         if (cldrFormat.contentEquals(jrePattern)) {
 608             return cldrFormat;
 609         }
 610         return jrePattern.toString();
 611     }
 612 
 613     private String toMetaZoneKey(String tzKey) {
 614         if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) {
 615             String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length());
 616             String meta = CLDRConverter.handlerMetaZones.get(tz);
 617             if (meta != null) {
 618                 return CLDRConverter.METAZONE_ID_PREFIX + meta;
 619             }
 620         }
 621         return null;
 622     }
 623 
 624     private void fillInAbbrs(String key, Map<String, String> map) {
 625         fillInAbbrs(TZ_STD_LONG_KEY, TZ_STD_SHORT_KEY, map);
 626         fillInAbbrs(TZ_DST_LONG_KEY, TZ_DST_SHORT_KEY, map);
 627         fillInAbbrs(TZ_GEN_LONG_KEY, TZ_GEN_SHORT_KEY, map);
 628 
 629         // If the standard std is "Standard Time" and daylight std is "Summer Time",
 630         // replace the standard std with the generic std to avoid using
 631         // the same abbrivation except for Australia time zone names.
 632         String std = map.get(TZ_STD_SHORT_KEY);
 633         String dst = map.get(TZ_DST_SHORT_KEY);
 634         String gen = map.get(TZ_GEN_SHORT_KEY);
 635         if (std != null) {
 636             if (dst == null) {
 637                 // if dst is null, create long and short names from the standard
 638                 // std. ("Something Standard Time" to "Something Daylight Time",
 639                 // or "Something Time" to "Something Summer Time")
 640                 String name = map.get(TZ_STD_LONG_KEY);
 641                 if (name != null) {
 642                     if (name.contains("Standard Time")) {
 643                         name = name.replace("Standard Time", "Daylight Time");
 644                     } else if (name.endsWith("Mean Time")) {
 645                         if (!name.startsWith("Greenwich ")) {
 646                             name = name.replace("Mean Time", "Summer Time");
 647                         }
 648                     } else if (name.endsWith(" Time")) {
 649                         name = name.replace(" Time", " Summer Time");
 650                     }
 651                     map.put(TZ_DST_LONG_KEY, name);
 652                     fillInAbbrs(TZ_DST_LONG_KEY, TZ_DST_SHORT_KEY, map);
 653                 }
 654             }
 655             if (gen  == null) {
 656                 String name = map.get(TZ_STD_LONG_KEY);
 657                 if (name != null) {
 658                     if (name.endsWith("Standard Time")) {
 659                         name = name.replace("Standard Time", "Time");
 660                     } else if (name.endsWith("Mean Time")) {
 661                         if (!name.startsWith("Greenwich ")) {
 662                             name = name.replace("Mean Time", "Time");
 663                         }
 664                     }
 665                     map.put(TZ_GEN_LONG_KEY, name);
 666                     fillInAbbrs(TZ_GEN_LONG_KEY, TZ_GEN_SHORT_KEY, map);
 667                 }
 668             }
 669         }
 670     }
 671 
 672     private void fillInAbbrs(String longKey, String shortKey, Map<String, String> map) {
 673         String abbr = map.get(shortKey);
 674         if (abbr == null) {
 675             String name = map.get(longKey);
 676             if (name != null) {
 677                 abbr = toAbbr(name);
 678                 if (abbr != null) {
 679                     map.put(shortKey, abbr);
 680                 }
 681             }
 682         }
 683     }
 684 
 685     private String toAbbr(String name) {
 686         String[] substrs = name.split("\\s+");
 687         StringBuilder sb = new StringBuilder();
 688         for (String s : substrs) {
 689             char c = s.charAt(0);
 690             if (c >= 'A' && c <= 'Z') {
 691                 sb.append(c);
 692             }
 693         }
 694         return sb.length() > 0 ? sb.toString() : null;
 695     }
 696 
 697     private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) {
 698         switch (cldrLetter) {
 699         case 'G':
 700             if (calendarType != CalendarType.GREGORIAN) {
 701                 // Adjust the number of 'G's for JRE SimpleDateFormat
 702                 if (count == 5) {
 703                     // CLDR narrow -> JRE short
 704                     count = 1;
 705                 } else if (count == 1) {
 706                     // CLDR abbr -> JRE long
 707                     count = 4;
 708                 }
 709             }
 710             appendN(cldrLetter, count, sb);
 711             break;
 712 
 713         // TODO: support 'c' and 'e' in JRE SimpleDateFormat
 714         // Use 'u' and 'E' for now.
 715         case 'c':
 716         case 'e':
 717             switch (count) {
 718             case 1:
 719                 sb.append('u');
 720                 break;
 721             case 3:
 722             case 4:
 723                 appendN('E', count, sb);
 724                 break;
 725             case 5:
 726                 appendN('E', 3, sb);
 727                 break;
 728             }
 729             break;
 730 
 731         case 'l':
 732             // 'l' is deprecated as a pattern character. Should be ignored.
 733             break;
 734 
 735         case 'u':
 736             // Use 'y' for now.
 737             appendN('y', count, sb);
 738             break;
 739 
 740         case 'v':
 741         case 'V':
 742             appendN('z', count, sb);
 743             break;
 744 
 745         case 'Z':
 746             if (count == 4 || count == 5) {
 747                 sb.append("XXX");
 748             }
 749             break;
 750 
 751         case 'U':
 752         case 'q':
 753         case 'Q':
 754         case 'g':
 755         case 'j':
 756         case 'A':
 757             throw new InternalError(String.format("Unsupported letter: '%c', count=%d, id=%s%n",
 758                                                   cldrLetter, count, id));
 759         default:
 760             appendN(cldrLetter, count, sb);
 761             break;
 762         }
 763     }
 764 
 765     private void appendN(char c, int n, StringBuilder sb) {
 766         for (int i = 0; i < n; i++) {
 767             sb.append(c);
 768         }
 769     }
 770 
 771     private static boolean hasNulls(Object[] array) {
 772         for (int i = 0; i < array.length; i++) {
 773             if (array[i] == null) {
 774                 return true;
 775             }
 776         }
 777         return false;
 778     }
 779 }