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