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 }