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.MessageFormat; 46 import java.text.NumberFormat; 47 import java.util.Calendar; 48 import java.util.HashSet; 49 import java.util.LinkedHashSet; 50 import java.util.Locale; 51 import java.util.Map; 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.calendar.ZoneInfo; 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 (tznb.containsKey(key)) { 275 if (key.startsWith(TZNB_EXCITY_PREFIX)) { 276 val = tznb.getString(key); 277 assert val instanceof String; 278 trace("tznb: %s key: %s, val: %s\n", tznb, key, val); 279 } else { 280 String[] names = tznb.getStringArray(key); 281 trace("tznb: %s key: %s, names: %s, %s, %s, %s, %s, %s, %s\n", tznb, key, 282 names[0], names[1], names[2], names[3], names[4], names[5], names[6]); 283 val = names; 284 } 285 cache.put(cacheKey, 286 new ResourceReference(cacheKey, val, referenceQueue)); 287 } 288 } 289 290 return val; 291 } 292 293 @SuppressWarnings("unchecked") 294 Set<String> getZoneIDs() { 295 Set<String> zoneIDs = null; 296 297 removeEmptyReferences(); 298 ResourceReference data = cache.get(ZONE_IDS_CACHEKEY); 299 if (data == null || ((zoneIDs = (Set<String>) data.get()) == null)) { 300 TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale); 301 zoneIDs = rb.keySet(); 302 cache.put(ZONE_IDS_CACHEKEY, 303 new ResourceReference(ZONE_IDS_CACHEKEY, (Object) zoneIDs, referenceQueue)); 304 } 305 306 return zoneIDs; 307 } 308 309 // zoneStrings are cached separately in TimeZoneNameUtility. 310 String[][] getZoneStrings() { 311 TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale); 312 Set<String> keyset = getZoneIDs(); 313 // Use a LinkedHashSet to preseve the order 314 Set<String[]> value = new LinkedHashSet<>(); 315 Set<String> tzIds = new HashSet<>(Set.of(TimeZone.getAvailableIDs())); 316 for (String key : keyset) { 317 if (!key.startsWith(TZNB_EXCITY_PREFIX)) { 318 value.add(rb.getStringArray(key)); 319 tzIds.remove(key); 320 } 321 } 322 323 if (type == LocaleProviderAdapter.Type.CLDR) { 324 // Add aliases data for CLDR 325 Map<String, String> aliases = ZoneInfo.getAliasTable(); 326 // Note: TimeZoneNamesBundle creates a String[] on each getStringArray call. 327 328 // Add timezones which are not present in this keyset, 329 // so that their fallback names will be generated at runtime. 330 tzIds.stream().filter(i -> (!i.startsWith("Etc/GMT") 331 && !i.startsWith("GMT") 332 && !i.startsWith("SystemV"))) 333 .forEach(tzid -> { 334 String[] val = new String[7]; 335 if (keyset.contains(tzid)) { 336 val = rb.getStringArray(tzid); 337 } else { 338 String tz = aliases.get(tzid); 339 if (keyset.contains(tz)) { 340 val = rb.getStringArray(tz); 341 } 342 } 343 val[0] = tzid; 344 value.add(val); 345 }); 346 } 347 return value.toArray(new String[0][]); 348 } 349 350 String[] getCalendarNames(String key) { 351 String[] names = null; 352 String cacheKey = CALENDAR_NAMES + key; 353 354 removeEmptyReferences(); 355 ResourceReference data = cache.get(cacheKey); 356 357 if (data == null || ((names = (String[]) data.get()) == null)) { 358 ResourceBundle rb = localeData.getDateFormatData(locale); 359 if (rb.containsKey(key)) { 360 names = rb.getStringArray(key); 361 cache.put(cacheKey, 362 new ResourceReference(cacheKey, (Object) names, referenceQueue)); 363 } 364 } 365 366 return names; 367 } 368 369 String[] getJavaTimeNames(String key) { 370 String[] names = null; 371 String cacheKey = CALENDAR_NAMES + key; 372 373 removeEmptyReferences(); 374 ResourceReference data = cache.get(cacheKey); 375 376 if (data == null || ((names = (String[]) data.get()) == null)) { 377 ResourceBundle rb = getJavaTimeFormatData(); 378 if (rb.containsKey(key)) { 379 names = rb.getStringArray(key); 380 cache.put(cacheKey, 381 new ResourceReference(cacheKey, (Object) names, referenceQueue)); 382 } 383 } 384 385 return names; 386 } 387 388 public String getDateTimePattern(int timeStyle, int dateStyle, Calendar cal) { 389 if (cal == null) { 390 cal = Calendar.getInstance(locale); 391 } 392 return getDateTimePattern(null, timeStyle, dateStyle, cal.getCalendarType()); 393 } 394 395 /** 396 * Returns a date-time format pattern 397 * @param timeStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat, 398 * or -1 if not required 399 * @param dateStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat, 400 * or -1 if not required 401 * @param calType the calendar type for the pattern 402 * @return the pattern string 403 */ 404 public String getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType) { 405 calType = CalendarDataUtility.normalizeCalendarType(calType); 406 String pattern; 407 pattern = getDateTimePattern("java.time.", timeStyle, dateStyle, calType); 408 if (pattern == null) { 409 pattern = getDateTimePattern(null, timeStyle, dateStyle, calType); 410 } 411 return pattern; 412 } 413 414 private String getDateTimePattern(String prefix, int timeStyle, int dateStyle, String calType) { 415 String pattern; 416 String timePattern = null; 417 String datePattern = null; 418 419 if (timeStyle >= 0) { 420 if (prefix != null) { 421 timePattern = getDateTimePattern(prefix, "TimePatterns", timeStyle, calType); 422 } 423 if (timePattern == null) { 424 timePattern = getDateTimePattern(null, "TimePatterns", timeStyle, calType); 425 } 426 } 427 if (dateStyle >= 0) { 428 if (prefix != null) { 429 datePattern = getDateTimePattern(prefix, "DatePatterns", dateStyle, calType); 430 } 431 if (datePattern == null) { 432 datePattern = getDateTimePattern(null, "DatePatterns", dateStyle, calType); 433 } 434 } 435 if (timeStyle >= 0) { 436 if (dateStyle >= 0) { 437 String dateTimePattern = null; 438 int dateTimeStyle = Math.max(dateStyle, timeStyle); 439 if (prefix != null) { 440 dateTimePattern = getDateTimePattern(prefix, "DateTimePatterns", dateTimeStyle, calType); 441 } 442 if (dateTimePattern == null) { 443 dateTimePattern = getDateTimePattern(null, "DateTimePatterns", dateTimeStyle, calType); 444 } 445 switch (dateTimePattern) { 446 case "{1} {0}": 447 pattern = datePattern + " " + timePattern; 448 break; 449 case "{0} {1}": 450 pattern = timePattern + " " + datePattern; 451 break; 452 default: 453 pattern = MessageFormat.format(dateTimePattern.replaceAll("'", "''"), timePattern, datePattern); 454 break; 455 } 456 } else { 457 pattern = timePattern; 458 } 459 } else if (dateStyle >= 0) { 460 pattern = datePattern; 461 } else { 462 throw new IllegalArgumentException("No date or time style specified"); 463 } 464 return pattern; 465 } 466 467 public String[] getNumberPatterns() { 468 String[] numberPatterns = null; 469 470 removeEmptyReferences(); 471 ResourceReference data = cache.get(NUMBER_PATTERNS_CACHEKEY); 472 473 if (data == null || ((numberPatterns = (String[]) data.get()) == null)) { 474 ResourceBundle resource = localeData.getNumberFormatData(locale); 475 numberPatterns = resource.getStringArray("NumberPatterns"); 476 cache.put(NUMBER_PATTERNS_CACHEKEY, 477 new ResourceReference(NUMBER_PATTERNS_CACHEKEY, (Object) numberPatterns, referenceQueue)); 478 } 479 480 return numberPatterns; 481 } 482 483 /** 484 * Returns the compact number format patterns. 485 * @param formatStyle the style for formatting a number 486 * @return an array of compact number patterns 487 */ 488 @SuppressWarnings("unchecked") 489 public String[] getCNPatterns(NumberFormat.Style formatStyle) { 490 491 Objects.requireNonNull(formatStyle); 492 String[] compactNumberPatterns = null; 493 removeEmptyReferences(); 494 String width = (formatStyle == NumberFormat.Style.LONG) ? "long" : "short"; 495 String cacheKey = width + "." + COMPACT_NUMBER_PATTERNS_CACHEKEY; 496 ResourceReference data = cache.get(cacheKey); 497 if (data == null || ((compactNumberPatterns 498 = (String[]) data.get()) == null)) { 499 ResourceBundle resource = localeData.getNumberFormatData(locale); 500 compactNumberPatterns = (String[]) resource 501 .getObject(width + ".CompactNumberPatterns"); 502 cache.put(cacheKey, new ResourceReference(cacheKey, 503 (Object) compactNumberPatterns, referenceQueue)); 504 } 505 return compactNumberPatterns; 506 } 507 508 509 /** 510 * Returns the FormatData resource bundle of this LocaleResources. 511 * The FormatData should be used only for accessing extra 512 * resources required by JSR 310. 513 */ 514 public ResourceBundle getJavaTimeFormatData() { 515 ResourceBundle rb = localeData.getDateFormatData(locale); 516 if (rb instanceof ParallelListResourceBundle) { 517 localeData.setSupplementary((ParallelListResourceBundle) rb); 518 } 519 return rb; 520 } 521 522 private String getDateTimePattern(String prefix, String key, int styleIndex, String calendarType) { 523 StringBuilder sb = new StringBuilder(); 524 if (prefix != null) { 525 sb.append(prefix); 526 } 527 if (!"gregory".equals(calendarType)) { 528 sb.append(calendarType).append('.'); 529 } 530 sb.append(key); 531 String resourceKey = sb.toString(); 532 String cacheKey = sb.insert(0, DATE_TIME_PATTERN).toString(); 533 534 removeEmptyReferences(); 535 ResourceReference data = cache.get(cacheKey); 536 Object value = NULLOBJECT; 537 538 if (data == null || ((value = data.get()) == null)) { 539 ResourceBundle r = (prefix != null) ? getJavaTimeFormatData() : localeData.getDateFormatData(locale); 540 if (r.containsKey(resourceKey)) { 541 value = r.getStringArray(resourceKey); 542 } else { 543 assert !resourceKey.equals(key); 544 if (r.containsKey(key)) { 545 value = r.getStringArray(key); 546 } 547 } 548 cache.put(cacheKey, 549 new ResourceReference(cacheKey, value, referenceQueue)); 550 } 551 if (value == NULLOBJECT) { 552 assert prefix != null; 553 return null; 554 } 555 556 // for DateTimePatterns. CLDR has multiple styles, while JRE has one. 557 String[] styles = (String[])value; 558 return (styles.length > 1 ? styles[styleIndex] : styles[0]); 559 } 560 561 private static class ResourceReference extends SoftReference<Object> { 562 private final String cacheKey; 563 564 ResourceReference(String cacheKey, Object o, ReferenceQueue<Object> q) { 565 super(o, q); 566 this.cacheKey = cacheKey; 567 } 568 569 String getCacheKey() { 570 return cacheKey; 571 } 572 } 573 574 private static final boolean TRACE_ON = Boolean.valueOf( 575 GetPropertyAction.privilegedGetProperty("locale.resources.debug", "false")); 576 577 public static void trace(String format, Object... params) { 578 if (TRACE_ON) { 579 System.out.format(format, params); 580 } 581 } 582 }