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