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