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