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