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