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