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 /* 27 * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved 28 * (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved 29 * 30 * The original version of this source code and documentation 31 * is copyrighted and owned by Taligent, Inc., a wholly-owned 32 * subsidiary of IBM. These materials are provided under terms 33 * of a License Agreement between Taligent and Sun. This technology 34 * is protected by multiple US and International patents. 35 * 36 * This notice and attribution to Taligent may not be removed. 37 * Taligent is a registered trademark of Taligent, Inc. 38 * 39 */ 40 41 package sun.util.locale.provider; 42 43 import java.lang.ref.ReferenceQueue; 44 import java.lang.ref.SoftReference; 45 import java.text.MessageFormat; 46 import java.text.NumberFormat; 47 import java.util.Arrays; 48 import java.util.Calendar; 49 import java.util.HashSet; 50 import java.util.LinkedHashSet; 51 import java.util.Locale; 52 import java.util.Objects; 53 import java.util.ResourceBundle; 54 import java.util.Set; 55 import java.util.TimeZone; 56 import java.util.concurrent.ConcurrentHashMap; 57 import java.util.concurrent.ConcurrentMap; 58 import sun.security.action.GetPropertyAction; 59 import sun.util.resources.LocaleData; 60 import sun.util.resources.OpenListResourceBundle; 61 import sun.util.resources.ParallelListResourceBundle; 62 import sun.util.resources.TimeZoneNamesBundle; 63 64 /** 65 * Central accessor to locale-dependent resources for JRE/CLDR provider adapters. 66 * 67 * @author Masayoshi Okutsu 68 * @author Naoto Sato 69 */ 70 public class LocaleResources { 71 72 private final Locale locale; 73 private final LocaleData localeData; 74 private final LocaleProviderAdapter.Type type; 75 76 // Resource cache 77 private final ConcurrentMap<String, ResourceReference> cache = new ConcurrentHashMap<>(); 78 private final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); 79 80 // cache key prefixes 81 private static final String BREAK_ITERATOR_INFO = "BII."; 82 private static final String CALENDAR_DATA = "CALD."; 83 private static final String COLLATION_DATA_CACHEKEY = "COLD"; 84 private static final String DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY = "DFSD"; 85 private static final String CURRENCY_NAMES = "CN."; 86 private static final String LOCALE_NAMES = "LN."; 87 private static final String TIME_ZONE_NAMES = "TZN."; 88 private static final String ZONE_IDS_CACHEKEY = "ZID"; 89 private static final String CALENDAR_NAMES = "CALN."; 90 private static final String NUMBER_PATTERNS_CACHEKEY = "NP"; 91 private static final String COMPACT_NUMBER_PATTERNS_CACHEKEY = "CNP"; 92 private static final String DATE_TIME_PATTERN = "DTP."; 93 private static final String RULES_CACHEKEY = "RULE"; 94 95 // TimeZoneNamesBundle exemplar city prefix 96 private static final String TZNB_EXCITY_PREFIX = "timezone.excity."; 97 98 // null singleton cache value 99 private static final Object NULLOBJECT = new Object(); 100 LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale)101 LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale) { 102 this.locale = locale; 103 this.localeData = adapter.getLocaleData(); 104 type = ((LocaleProviderAdapter)adapter).getAdapterType(); 105 } 106 removeEmptyReferences()107 private void removeEmptyReferences() { 108 Object ref; 109 while ((ref = referenceQueue.poll()) != null) { 110 cache.remove(((ResourceReference)ref).getCacheKey()); 111 } 112 } 113 getBreakIteratorInfo(String key)114 Object getBreakIteratorInfo(String key) { 115 Object biInfo; 116 String cacheKey = BREAK_ITERATOR_INFO + key; 117 118 removeEmptyReferences(); 119 ResourceReference data = cache.get(cacheKey); 120 if (data == null || ((biInfo = data.get()) == null)) { 121 biInfo = localeData.getBreakIteratorInfo(locale).getObject(key); 122 cache.put(cacheKey, new ResourceReference(cacheKey, biInfo, referenceQueue)); 123 } 124 125 return biInfo; 126 } 127 getBreakIteratorResources(String key)128 byte[] getBreakIteratorResources(String key) { 129 return (byte[]) localeData.getBreakIteratorResources(locale).getObject(key); 130 } 131 getCalendarData(String key)132 public String getCalendarData(String key) { 133 String caldata = ""; 134 String cacheKey = CALENDAR_DATA + key; 135 136 removeEmptyReferences(); 137 138 ResourceReference data = cache.get(cacheKey); 139 if (data == null || ((caldata = (String) data.get()) == null)) { 140 ResourceBundle rb = localeData.getCalendarData(locale); 141 if (rb.containsKey(key)) { 142 caldata = rb.getString(key); 143 } 144 145 cache.put(cacheKey, 146 new ResourceReference(cacheKey, caldata, referenceQueue)); 147 } 148 149 return caldata; 150 } 151 getCollationData()152 public String getCollationData() { 153 String key = "Rule"; 154 String coldata = ""; 155 156 removeEmptyReferences(); 157 ResourceReference data = cache.get(COLLATION_DATA_CACHEKEY); 158 if (data == null || ((coldata = (String) data.get()) == null)) { 159 ResourceBundle rb = localeData.getCollationData(locale); 160 if (rb.containsKey(key)) { 161 coldata = rb.getString(key); 162 } 163 cache.put(COLLATION_DATA_CACHEKEY, 164 new ResourceReference(COLLATION_DATA_CACHEKEY, coldata, referenceQueue)); 165 } 166 167 return coldata; 168 } 169 getDecimalFormatSymbolsData()170 public Object[] getDecimalFormatSymbolsData() { 171 Object[] dfsdata; 172 173 removeEmptyReferences(); 174 ResourceReference data = cache.get(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY); 175 if (data == null || ((dfsdata = (Object[]) data.get()) == null)) { 176 // Note that only dfsdata[0] is prepared here in this method. Other 177 // elements are provided by the caller, yet they are cached here. 178 ResourceBundle rb = localeData.getNumberFormatData(locale); 179 dfsdata = new Object[3]; 180 dfsdata[0] = getNumberStrings(rb, "NumberElements"); 181 182 cache.put(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY, 183 new ResourceReference(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY, dfsdata, referenceQueue)); 184 } 185 186 return dfsdata; 187 } 188 getNumberStrings(ResourceBundle rb, String type)189 private String[] getNumberStrings(ResourceBundle rb, String type) { 190 String[] ret = null; 191 String key; 192 String numSys; 193 194 // Number strings look up. First, try the Unicode extension 195 numSys = locale.getUnicodeLocaleType("nu"); 196 if (numSys != null) { 197 key = numSys + "." + type; 198 if (rb.containsKey(key)) { 199 ret = rb.getStringArray(key); 200 } 201 } 202 203 // Next, try DefaultNumberingSystem value 204 if (ret == null && rb.containsKey("DefaultNumberingSystem")) { 205 key = rb.getString("DefaultNumberingSystem") + "." + type; 206 if (rb.containsKey(key)) { 207 ret = rb.getStringArray(key); 208 } 209 } 210 211 // Last resort. No need to check the availability. 212 // Just let it throw MissingResourceException when needed. 213 if (ret == null) { 214 ret = rb.getStringArray(type); 215 } 216 217 return ret; 218 } 219 getCurrencyName(String key)220 public String getCurrencyName(String key) { 221 Object currencyName = null; 222 String cacheKey = CURRENCY_NAMES + key; 223 224 removeEmptyReferences(); 225 ResourceReference data = cache.get(cacheKey); 226 227 if (data != null && ((currencyName = data.get()) != null)) { 228 if (currencyName.equals(NULLOBJECT)) { 229 currencyName = null; 230 } 231 232 return (String) currencyName; 233 } 234 235 OpenListResourceBundle olrb = localeData.getCurrencyNames(locale); 236 237 if (olrb.containsKey(key)) { 238 currencyName = olrb.getObject(key); 239 cache.put(cacheKey, 240 new ResourceReference(cacheKey, currencyName, referenceQueue)); 241 } 242 243 return (String) currencyName; 244 } 245 getLocaleName(String key)246 public String getLocaleName(String key) { 247 Object localeName = null; 248 String cacheKey = LOCALE_NAMES + key; 249 250 removeEmptyReferences(); 251 ResourceReference data = cache.get(cacheKey); 252 253 if (data != null && ((localeName = data.get()) != null)) { 254 if (localeName.equals(NULLOBJECT)) { 255 localeName = null; 256 } 257 258 return (String) localeName; 259 } 260 261 OpenListResourceBundle olrb = localeData.getLocaleNames(locale); 262 263 if (olrb.containsKey(key)) { 264 localeName = olrb.getObject(key); 265 cache.put(cacheKey, 266 new ResourceReference(cacheKey, localeName, referenceQueue)); 267 } 268 269 return (String) localeName; 270 } 271 getTimeZoneNames(String key)272 public Object getTimeZoneNames(String key) { 273 Object val = null; 274 String cacheKey = TIME_ZONE_NAMES + key; 275 276 removeEmptyReferences(); 277 ResourceReference data = cache.get(cacheKey); 278 279 if (Objects.isNull(data) || Objects.isNull(val = data.get())) { 280 TimeZoneNamesBundle tznb = localeData.getTimeZoneNames(locale); 281 if (key.startsWith(TZNB_EXCITY_PREFIX)) { 282 if (tznb.containsKey(key)) { 283 val = tznb.getString(key); 284 assert val instanceof String; 285 trace("tznb: %s key: %s, val: %s\n", tznb, key, val); 286 } 287 } else { 288 String[] names = null; 289 if (tznb.containsKey(key)) { 290 names = tznb.getStringArray(key); 291 } else { 292 var tz = TimeZoneNameUtility.canonicalTZID(key).orElse(key); 293 if (tznb.containsKey(tz)) { 294 names = tznb.getStringArray(tz); 295 } 296 } 297 298 if (names != null) { 299 names[0] = key; 300 trace("tznb: %s key: %s, names: %s, %s, %s, %s, %s, %s, %s\n", tznb, key, 301 names[0], names[1], names[2], names[3], names[4], names[5], names[6]); 302 val = names; 303 } 304 } 305 if (val != null) { 306 cache.put(cacheKey, 307 new ResourceReference(cacheKey, val, referenceQueue)); 308 } 309 } 310 311 return val; 312 } 313 314 @SuppressWarnings("unchecked") getZoneIDs()315 Set<String> getZoneIDs() { 316 Set<String> zoneIDs; 317 318 removeEmptyReferences(); 319 ResourceReference data = cache.get(ZONE_IDS_CACHEKEY); 320 if (data == null || ((zoneIDs = (Set<String>) data.get()) == null)) { 321 TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale); 322 zoneIDs = rb.keySet(); 323 cache.put(ZONE_IDS_CACHEKEY, 324 new ResourceReference(ZONE_IDS_CACHEKEY, zoneIDs, referenceQueue)); 325 } 326 327 return zoneIDs; 328 } 329 330 // zoneStrings are cached separately in TimeZoneNameUtility. getZoneStrings()331 String[][] getZoneStrings() { 332 TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale); 333 Set<String> keyset = getZoneIDs(); 334 // Use a LinkedHashSet to preseve the order 335 Set<String[]> value = new LinkedHashSet<>(); 336 Set<String> tzIds = new HashSet<>(Arrays.asList(TimeZone.getAvailableIDs())); 337 for (String key : keyset) { 338 if (!key.startsWith(TZNB_EXCITY_PREFIX)) { 339 value.add(rb.getStringArray(key)); 340 tzIds.remove(key); 341 } 342 } 343 344 if (type == LocaleProviderAdapter.Type.CLDR) { 345 // Note: TimeZoneNamesBundle creates a String[] on each getStringArray call. 346 347 // Add timezones which are not present in this keyset, 348 // so that their fallback names will be generated at runtime. 349 tzIds.stream().filter(i -> (!i.startsWith("Etc/GMT") 350 && !i.startsWith("GMT") 351 && !i.startsWith("SystemV"))) 352 .forEach(tzid -> { 353 String[] val = new String[7]; 354 if (keyset.contains(tzid)) { 355 val = rb.getStringArray(tzid); 356 } else { 357 var canonID = TimeZoneNameUtility.canonicalTZID(tzid) 358 .orElse(tzid); 359 if (keyset.contains(canonID)) { 360 val = rb.getStringArray(canonID); 361 } 362 } 363 val[0] = tzid; 364 value.add(val); 365 }); 366 } 367 return value.toArray(new String[0][]); 368 } 369 getCalendarNames(String key)370 String[] getCalendarNames(String key) { 371 String[] names = null; 372 String cacheKey = CALENDAR_NAMES + key; 373 374 removeEmptyReferences(); 375 ResourceReference data = cache.get(cacheKey); 376 377 if (data == null || ((names = (String[]) data.get()) == null)) { 378 ResourceBundle rb = localeData.getDateFormatData(locale); 379 if (rb.containsKey(key)) { 380 names = rb.getStringArray(key); 381 cache.put(cacheKey, 382 new ResourceReference(cacheKey, names, referenceQueue)); 383 } 384 } 385 386 return names; 387 } 388 getJavaTimeNames(String key)389 String[] getJavaTimeNames(String key) { 390 String[] names = null; 391 String cacheKey = CALENDAR_NAMES + key; 392 393 removeEmptyReferences(); 394 ResourceReference data = cache.get(cacheKey); 395 396 if (data == null || ((names = (String[]) data.get()) == null)) { 397 ResourceBundle rb = getJavaTimeFormatData(); 398 if (rb.containsKey(key)) { 399 names = rb.getStringArray(key); 400 cache.put(cacheKey, 401 new ResourceReference(cacheKey, names, referenceQueue)); 402 } 403 } 404 405 return names; 406 } 407 getDateTimePattern(int timeStyle, int dateStyle, Calendar cal)408 public String getDateTimePattern(int timeStyle, int dateStyle, Calendar cal) { 409 if (cal == null) { 410 cal = Calendar.getInstance(locale); 411 } 412 return getDateTimePattern(null, timeStyle, dateStyle, cal.getCalendarType()); 413 } 414 415 /** 416 * Returns a date-time format pattern 417 * @param timeStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat, 418 * or -1 if not required 419 * @param dateStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat, 420 * or -1 if not required 421 * @param calType the calendar type for the pattern 422 * @return the pattern string 423 */ getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType)424 public String getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType) { 425 calType = CalendarDataUtility.normalizeCalendarType(calType); 426 String pattern; 427 pattern = getDateTimePattern("java.time.", timeStyle, dateStyle, calType); 428 if (pattern == null) { 429 pattern = getDateTimePattern(null, timeStyle, dateStyle, calType); 430 } 431 return pattern; 432 } 433 getDateTimePattern(String prefix, int timeStyle, int dateStyle, String calType)434 private String getDateTimePattern(String prefix, int timeStyle, int dateStyle, String calType) { 435 String pattern; 436 String timePattern = null; 437 String datePattern = null; 438 439 if (timeStyle >= 0) { 440 if (prefix != null) { 441 timePattern = getDateTimePattern(prefix, "TimePatterns", timeStyle, calType); 442 } 443 if (timePattern == null) { 444 timePattern = getDateTimePattern(null, "TimePatterns", timeStyle, calType); 445 } 446 } 447 if (dateStyle >= 0) { 448 if (prefix != null) { 449 datePattern = getDateTimePattern(prefix, "DatePatterns", dateStyle, calType); 450 } 451 if (datePattern == null) { 452 datePattern = getDateTimePattern(null, "DatePatterns", dateStyle, calType); 453 } 454 } 455 if (timeStyle >= 0) { 456 if (dateStyle >= 0) { 457 String dateTimePattern = null; 458 int dateTimeStyle = Math.max(dateStyle, timeStyle); 459 if (prefix != null) { 460 dateTimePattern = getDateTimePattern(prefix, "DateTimePatterns", dateTimeStyle, calType); 461 } 462 if (dateTimePattern == null) { 463 dateTimePattern = getDateTimePattern(null, "DateTimePatterns", dateTimeStyle, calType); 464 } 465 pattern = switch (Objects.requireNonNull(dateTimePattern)) { 466 case "{1} {0}" -> datePattern + " " + timePattern; 467 case "{0} {1}" -> timePattern + " " + datePattern; 468 default -> MessageFormat.format(dateTimePattern.replaceAll("'", "''"), timePattern, datePattern); 469 }; 470 } else { 471 pattern = timePattern; 472 } 473 } else if (dateStyle >= 0) { 474 pattern = datePattern; 475 } else { 476 throw new IllegalArgumentException("No date or time style specified"); 477 } 478 return pattern; 479 } 480 getNumberPatterns()481 public String[] getNumberPatterns() { 482 String[] numberPatterns; 483 484 removeEmptyReferences(); 485 ResourceReference data = cache.get(NUMBER_PATTERNS_CACHEKEY); 486 487 if (data == null || ((numberPatterns = (String[]) data.get()) == null)) { 488 ResourceBundle resource = localeData.getNumberFormatData(locale); 489 numberPatterns = getNumberStrings(resource, "NumberPatterns"); 490 cache.put(NUMBER_PATTERNS_CACHEKEY, 491 new ResourceReference(NUMBER_PATTERNS_CACHEKEY, numberPatterns, referenceQueue)); 492 } 493 494 return numberPatterns; 495 } 496 497 /** 498 * Returns the compact number format patterns. 499 * @param formatStyle the style for formatting a number 500 * @return an array of compact number patterns 501 */ getCNPatterns(NumberFormat.Style formatStyle)502 public String[] getCNPatterns(NumberFormat.Style formatStyle) { 503 504 Objects.requireNonNull(formatStyle); 505 String[] compactNumberPatterns; 506 removeEmptyReferences(); 507 String width = (formatStyle == NumberFormat.Style.LONG) ? "long" : "short"; 508 String cacheKey = width + "." + COMPACT_NUMBER_PATTERNS_CACHEKEY; 509 ResourceReference data = cache.get(cacheKey); 510 if (data == null || ((compactNumberPatterns 511 = (String[]) data.get()) == null)) { 512 ResourceBundle resource = localeData.getNumberFormatData(locale); 513 compactNumberPatterns = (String[]) resource 514 .getObject(width + ".CompactNumberPatterns"); 515 cache.put(cacheKey, new ResourceReference(cacheKey, compactNumberPatterns, referenceQueue)); 516 } 517 return compactNumberPatterns; 518 } 519 520 521 /** 522 * Returns the FormatData resource bundle of this LocaleResources. 523 * The FormatData should be used only for accessing extra 524 * resources required by JSR 310. 525 */ getJavaTimeFormatData()526 public ResourceBundle getJavaTimeFormatData() { 527 ResourceBundle rb = localeData.getDateFormatData(locale); 528 if (rb instanceof ParallelListResourceBundle) { 529 localeData.setSupplementary((ParallelListResourceBundle) rb); 530 } 531 return rb; 532 } 533 getDateTimePattern(String prefix, String key, int styleIndex, String calendarType)534 private String getDateTimePattern(String prefix, String key, int styleIndex, String calendarType) { 535 StringBuilder sb = new StringBuilder(); 536 if (prefix != null) { 537 sb.append(prefix); 538 } 539 if (!"gregory".equals(calendarType)) { 540 sb.append(calendarType).append('.'); 541 } 542 sb.append(key); 543 String resourceKey = sb.toString(); 544 String cacheKey = sb.insert(0, DATE_TIME_PATTERN).toString(); 545 546 removeEmptyReferences(); 547 ResourceReference data = cache.get(cacheKey); 548 Object value = NULLOBJECT; 549 550 if (data == null || ((value = data.get()) == null)) { 551 ResourceBundle r = (prefix != null) ? getJavaTimeFormatData() : localeData.getDateFormatData(locale); 552 if (r.containsKey(resourceKey)) { 553 value = r.getStringArray(resourceKey); 554 } else { 555 assert !resourceKey.equals(key); 556 if (r.containsKey(key)) { 557 value = r.getStringArray(key); 558 } 559 } 560 cache.put(cacheKey, 561 new ResourceReference(cacheKey, value, referenceQueue)); 562 } 563 if (value == NULLOBJECT) { 564 assert prefix != null; 565 return null; 566 } 567 568 // for DateTimePatterns. CLDR has multiple styles, while JRE has one. 569 String[] styles = (String[])value; 570 return (styles.length > 1 ? styles[styleIndex] : styles[0]); 571 } 572 getRules()573 public String[] getRules() { 574 String[] rules; 575 576 removeEmptyReferences(); 577 ResourceReference data = cache.get(RULES_CACHEKEY); 578 579 if (data == null || ((rules = (String[]) data.get()) == null)) { 580 ResourceBundle rb = localeData.getDateFormatData(locale); 581 rules = new String[2]; 582 rules[0] = rules[1] = ""; 583 if (rb.containsKey("PluralRules")) { 584 rules[0] = rb.getString("PluralRules"); 585 } 586 if (rb.containsKey("DayPeriodRules")) { 587 rules[1] = rb.getString("DayPeriodRules"); 588 } 589 cache.put(RULES_CACHEKEY, new ResourceReference(RULES_CACHEKEY, rules, referenceQueue)); 590 } 591 592 return rules; 593 } 594 595 private static class ResourceReference extends SoftReference<Object> { 596 private final String cacheKey; 597 ResourceReference(String cacheKey, Object o, ReferenceQueue<Object> q)598 ResourceReference(String cacheKey, Object o, ReferenceQueue<Object> q) { 599 super(o, q); 600 this.cacheKey = cacheKey; 601 } 602 getCacheKey()603 String getCacheKey() { 604 return cacheKey; 605 } 606 } 607 608 private static final boolean TRACE_ON = Boolean.valueOf( 609 GetPropertyAction.privilegedGetProperty("locale.resources.debug", "false")); 610 trace(String format, Object... params)611 public static void trace(String format, Object... params) { 612 if (TRACE_ON) { 613 System.out.format(format, params); 614 } 615 } 616 } 617