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