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