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