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