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