1 /*
   2  * Copyright (c) 2012, 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 package sun.util.locale.provider;
  26 
  27 import java.lang.ref.SoftReference;
  28 import java.text.DateFormat;
  29 import java.text.DateFormatSymbols;
  30 import java.text.DecimalFormat;
  31 import java.text.DecimalFormatSymbols;
  32 import java.text.NumberFormat;
  33 import java.text.SimpleDateFormat;
  34 import java.text.spi.DateFormatProvider;
  35 import java.text.spi.DateFormatSymbolsProvider;
  36 import java.text.spi.DecimalFormatSymbolsProvider;
  37 import java.text.spi.NumberFormatProvider;
  38 import java.util.Collections;
  39 import java.util.HashSet;
  40 import java.util.Locale;
  41 import java.util.Map;
  42 import java.util.ResourceBundle.Control;
  43 import java.util.Set;
  44 import java.util.concurrent.ConcurrentHashMap;
  45 import java.util.concurrent.ConcurrentMap;
  46 import java.util.concurrent.atomic.AtomicReferenceArray;
  47 import java.util.spi.CalendarDataProvider;
  48 import java.util.spi.CalendarNameProvider;
  49 
  50 /**
  51  * LocaleProviderdapter implementation for the Windows locale data.
  52  *
  53  * @author Naoto Sato
  54  */
  55 public class HostLocaleProviderAdapterImpl {
  56 
  57     // locale categories
  58     private static final int CAT_DISPLAY = 0;
  59     private static final int CAT_FORMAT  = 1;
  60 
  61     // NumberFormat styles
  62     private static final int NF_NUMBER   = 0;
  63     private static final int NF_CURRENCY = 1;
  64     private static final int NF_PERCENT  = 2;
  65     private static final int NF_INTEGER  = 3;
  66     private static final int NF_MAX = NF_INTEGER;
  67 
  68     // CalendarData value types
  69     private static final int CD_FIRSTDAYOFWEEK = 0;
  70     private static final int CD_MINIMALDAYSINFIRSTWEEK = 1;
  71 
  72     // Native Calendar ID to LDML calendar type map
  73     private static final String[] calIDToLDML = {
  74         "",
  75         "gregory",
  76         "gregory_en-US",
  77         "japanese",
  78         "roc",
  79         "",          // No appropriate type for CAL_KOREA
  80         "islamic",
  81         "buddhist",
  82         "hebrew",
  83         "gregory_fr",
  84         "gregory_ar",
  85         "gregory_en",
  86         "gregory_fr",
  87     };
  88 
  89     // Caches
  90     private static ConcurrentMap<Locale, SoftReference<AtomicReferenceArray<String>>> dateFormatCache = new ConcurrentHashMap<>();
  91     private static ConcurrentMap<Locale, SoftReference<DateFormatSymbols>> dateFormatSymbolsCache = new ConcurrentHashMap<>();
  92     private static ConcurrentMap<Locale, SoftReference<AtomicReferenceArray<String>>> numberFormatCache = new ConcurrentHashMap<>();
  93     private static ConcurrentMap<Locale, SoftReference<DecimalFormatSymbols>> decimalFormatSymbolsCache = new ConcurrentHashMap<>();
  94 
  95     private static final Set<Locale> supportedLocaleSet;
  96     static {
  97         Set<Locale> tmpSet = new HashSet<>();
  98         if (initialize()) {
  99             // Assuming the default locales do not include any extensions, so
 100             // no stripping is needed here.
 101             Locale l = Locale.forLanguageTag(getDefaultLocale(CAT_FORMAT).replaceAll("_","-"));
 102             tmpSet.addAll(Control.getNoFallbackControl(Control.FORMAT_DEFAULT).getCandidateLocales("", l));
 103             l = Locale.forLanguageTag(getDefaultLocale(CAT_DISPLAY).replaceAll("_","-"));
 104             tmpSet.addAll(Control.getNoFallbackControl(Control.FORMAT_DEFAULT).getCandidateLocales("", l));
 105         }
 106         supportedLocaleSet = Collections.unmodifiableSet(tmpSet);
 107     }
 108     private final static Locale[] supportedLocale = supportedLocaleSet.toArray(new Locale[0]);
 109 
 110     public static DateFormatProvider getDateFormatProvider() {
 111         return new DateFormatProvider() {
 112             @Override
 113             public Locale[] getAvailableLocales() {
 114                 return getSupportedCalendarLocales();
 115             }
 116 
 117             @Override
 118             public boolean isSupportedLocale(Locale locale) {
 119                 return isSupportedCalendarLocale(locale);
 120             }
 121 
 122             @Override
 123             public DateFormat getDateInstance(int style, Locale locale) {
 124                 AtomicReferenceArray<String> patterns = getDateTimePatterns(locale);
 125                 return new SimpleDateFormat(patterns.get(style/2),
 126                                             getCalendarLocale(locale));
 127             }
 128 
 129             @Override
 130             public DateFormat getTimeInstance(int style, Locale locale) {
 131                 AtomicReferenceArray<String> patterns = getDateTimePatterns(locale);
 132                 return new SimpleDateFormat(patterns.get(style/2+2),
 133                                             getCalendarLocale(locale));
 134             }
 135 
 136             @Override
 137             public DateFormat getDateTimeInstance(int dateStyle,
 138                     int timeStyle, Locale locale) {
 139                 AtomicReferenceArray<String> patterns = getDateTimePatterns(locale);
 140                 String pattern = new StringBuilder(patterns.get(dateStyle/2))
 141                                        .append(" ")
 142                                        .append(patterns.get(timeStyle/2+2))
 143                                        .toString();
 144                 return new SimpleDateFormat(pattern, getCalendarLocale(locale));
 145             }
 146 
 147             private AtomicReferenceArray<String> getDateTimePatterns(Locale locale) {
 148                 AtomicReferenceArray<String> patterns;
 149                 SoftReference<AtomicReferenceArray<String>> ref = dateFormatCache.get(locale);
 150 
 151                 if (ref == null || (patterns = ref.get()) == null) {
 152                     String langtag = removeExtensions(locale).toLanguageTag();
 153                     patterns = new AtomicReferenceArray<>(4);
 154                     patterns.compareAndSet(0, null, convertDateTimePattern(
 155                         getDateTimePattern(DateFormat.LONG, -1, langtag)));
 156                     patterns.compareAndSet(1, null, convertDateTimePattern(
 157                         getDateTimePattern(DateFormat.SHORT, -1, langtag)));
 158                     patterns.compareAndSet(2, null, convertDateTimePattern(
 159                         getDateTimePattern(-1, DateFormat.LONG, langtag)));
 160                     patterns.compareAndSet(3, null, convertDateTimePattern(
 161                         getDateTimePattern(-1, DateFormat.SHORT, langtag)));
 162                     ref = new SoftReference<>(patterns);
 163                     dateFormatCache.put(locale, ref);
 164                 }
 165 
 166                 return patterns;
 167             }
 168         };
 169     }
 170 
 171     public static DateFormatSymbolsProvider getDateFormatSymbolsProvider() {
 172         return new DateFormatSymbolsProvider() {
 173 
 174             @Override
 175             public Locale[] getAvailableLocales() {
 176                 if (isSupportedLocale(Locale.getDefault(Locale.Category.FORMAT))) {
 177                     return supportedLocale;
 178                 }
 179 
 180                 return new Locale[0];
 181             }
 182 
 183             @Override
 184             public boolean isSupportedLocale(Locale locale) {
 185                 // Only supports the locale with Gregorian calendar
 186                 if (supportedLocale.length != 0) {
 187                     int calid = getCalendarID(locale.toLanguageTag());
 188                     if (calid > 0 && calid < calIDToLDML.length) {
 189                         return calIDToLDML[calid].startsWith("gregory");
 190                     }
 191                 }
 192 
 193                 return false;
 194             }
 195 
 196             @Override
 197             public DateFormatSymbols getInstance(Locale locale) {
 198                 DateFormatSymbols dfs;
 199                 SoftReference<DateFormatSymbols> ref =
 200                     dateFormatSymbolsCache.get(locale);
 201 
 202                 if (ref == null || (dfs = ref.get()) == null) {
 203                     dfs = new DateFormatSymbols(locale);
 204                     String langTag = removeExtensions(locale).toLanguageTag();
 205 
 206                     dfs.setAmPmStrings(getAmPmStrings(langTag, dfs.getAmPmStrings()));
 207                     dfs.setEras(getEras(langTag, dfs.getEras()));
 208                     dfs.setMonths(getMonths(langTag, dfs.getMonths()));
 209                     dfs.setShortMonths(getShortMonths(langTag, dfs.getShortMonths()));
 210                     dfs.setWeekdays(getWeekdays(langTag, dfs.getWeekdays()));
 211                     dfs.setShortWeekdays(getShortWeekdays(langTag, dfs.getShortWeekdays()));
 212                     ref = new SoftReference<>(dfs);
 213                     dateFormatSymbolsCache.put(locale, ref);
 214                 }
 215                 return (DateFormatSymbols)dfs.clone();
 216             }
 217         };
 218     }
 219 
 220     public static NumberFormatProvider getNumberFormatProvider() {
 221         return new NumberFormatProvider() {
 222 
 223             @Override
 224             public Locale[] getAvailableLocales() {
 225                 return getSupportedNativeDigitLocales();
 226             }
 227 
 228             @Override
 229             public boolean isSupportedLocale(Locale locale) {
 230                 return isSupportedNativeDigitLocale(locale);
 231             }
 232 
 233             @Override
 234             public NumberFormat getCurrencyInstance(Locale locale) {
 235                 AtomicReferenceArray<String> patterns = getNumberPatterns(locale);
 236                 return new DecimalFormat(patterns.get(NF_CURRENCY),
 237                     DecimalFormatSymbols.getInstance(locale));
 238             }
 239 
 240             @Override
 241             public NumberFormat getIntegerInstance(Locale locale) {
 242                 AtomicReferenceArray<String> patterns = getNumberPatterns(locale);
 243                 return new DecimalFormat(patterns.get(NF_INTEGER),
 244                     DecimalFormatSymbols.getInstance(locale));
 245             }
 246 
 247             @Override
 248             public NumberFormat getNumberInstance(Locale locale) {
 249                 AtomicReferenceArray<String> patterns = getNumberPatterns(locale);
 250                 return new DecimalFormat(patterns.get(NF_NUMBER),
 251                     DecimalFormatSymbols.getInstance(locale));
 252             }
 253 
 254             @Override
 255             public NumberFormat getPercentInstance(Locale locale) {
 256                 AtomicReferenceArray<String> patterns = getNumberPatterns(locale);
 257                 return new DecimalFormat(patterns.get(NF_PERCENT),
 258                     DecimalFormatSymbols.getInstance(locale));
 259             }
 260 
 261             private AtomicReferenceArray<String> getNumberPatterns(Locale locale) {
 262                 AtomicReferenceArray<String> patterns;
 263                 SoftReference<AtomicReferenceArray<String>> ref = numberFormatCache.get(locale);
 264 
 265                 if (ref == null || (patterns = ref.get()) == null) {
 266                     String langtag = locale.toLanguageTag();
 267                     patterns = new AtomicReferenceArray<>(NF_MAX+1);
 268                     for (int i = 0; i <= NF_MAX; i++) {
 269                         patterns.compareAndSet(i, null, getNumberPattern(i, langtag));
 270                     }
 271                     ref = new SoftReference<>(patterns);
 272                     numberFormatCache.put(locale, ref);
 273                 }
 274                 return patterns;
 275             }
 276         };
 277     }
 278 
 279     public static DecimalFormatSymbolsProvider getDecimalFormatSymbolsProvider() {
 280         return new DecimalFormatSymbolsProvider() {
 281 
 282             @Override
 283             public Locale[] getAvailableLocales() {
 284                 return getSupportedNativeDigitLocales();
 285             }
 286 
 287             @Override
 288             public boolean isSupportedLocale(Locale locale) {
 289                 return isSupportedNativeDigitLocale(locale);
 290             }
 291 
 292             @Override
 293             public DecimalFormatSymbols getInstance(Locale locale) {
 294                 DecimalFormatSymbols dfs;
 295                 SoftReference<DecimalFormatSymbols> ref =
 296                     decimalFormatSymbolsCache.get(locale);
 297 
 298                 if (ref == null || (dfs = ref.get()) == null) {
 299                     dfs = new DecimalFormatSymbols(getNumberLocale(locale));
 300                     String langTag = removeExtensions(locale).toLanguageTag();
 301 
 302                     // DecimalFormatSymbols.setInternationalCurrencySymbol() has
 303                     // a side effect of setting the currency symbol as well. So
 304                     // the calling order is relevant here.
 305                     dfs.setInternationalCurrencySymbol(getInternationalCurrencySymbol(langTag, dfs.getInternationalCurrencySymbol()));
 306                     dfs.setCurrencySymbol(getCurrencySymbol(langTag, dfs.getCurrencySymbol()));
 307                     dfs.setDecimalSeparator(getDecimalSeparator(langTag, dfs.getDecimalSeparator()));
 308                     dfs.setGroupingSeparator(getGroupingSeparator(langTag, dfs.getGroupingSeparator()));
 309                     dfs.setInfinity(getInfinity(langTag, dfs.getInfinity()));
 310                     dfs.setMinusSign(getMinusSign(langTag, dfs.getMinusSign()));
 311                     dfs.setMonetaryDecimalSeparator(getMonetaryDecimalSeparator(langTag, dfs.getMonetaryDecimalSeparator()));
 312                     dfs.setNaN(getNaN(langTag, dfs.getNaN()));
 313                     dfs.setPercent(getPercent(langTag, dfs.getPercent()));
 314                     dfs.setPerMill(getPerMill(langTag, dfs.getPerMill()));
 315                     if (isNativeDigit(langTag)) {
 316                         dfs.setZeroDigit(getZeroDigit(langTag, dfs.getZeroDigit()));
 317                     }
 318                     ref = new SoftReference<>(dfs);
 319                     decimalFormatSymbolsCache.put(locale, ref);
 320                 }
 321                 return (DecimalFormatSymbols)dfs.clone();
 322             }
 323         };
 324     }
 325 
 326     public static CalendarDataProvider getCalendarDataProvider() {
 327         return new CalendarDataProvider() {
 328             @Override
 329             public Locale[] getAvailableLocales() {
 330                 return getSupportedCalendarLocales();
 331             }
 332 
 333             @Override
 334             public boolean isSupportedLocale(Locale locale) {
 335                 return isSupportedCalendarLocale(locale);
 336             }
 337 
 338             @Override
 339             public int getFirstDayOfWeek(Locale locale) {
 340                 int first = getCalendarDataValue(
 341                                  removeExtensions(locale).toLanguageTag(),
 342                                  CD_FIRSTDAYOFWEEK);
 343                 if (first != -1) {
 344                     return (first + 1) % 7 + 1;
 345                 } else {
 346                     return 0;
 347                 }
 348             }
 349 
 350             @Override
 351             public int getMinimalDaysInFirstWeek(Locale locale) {
 352                 return 0;
 353             }
 354         };
 355     }
 356 
 357     public static CalendarNameProvider getCalendarNameProvider() {
 358         return new CalendarNameProvider() {
 359             @Override
 360             public Locale[] getAvailableLocales() {
 361                 return getSupportedCalendarLocales();
 362             }
 363 
 364             @Override
 365             public boolean isSupportedLocale(Locale locale) {
 366                 return isSupportedCalendarLocale(locale);
 367             }
 368 
 369             @Override
 370             public String getDisplayName(String calType, int field, int value,
 371                                          int style, Locale locale) {
 372                 return null;
 373             }
 374 
 375             @Override
 376             public Map<String, Integer> getDisplayNames(String calType,
 377                                          int field, int style, Locale locale) {
 378                 return null;
 379             }
 380         };
 381     }
 382 
 383     private static String convertDateTimePattern(String winPattern) {
 384         String ret = winPattern.replaceAll("dddd", "EEEE");
 385         ret = ret.replaceAll("ddd", "EEE");
 386         ret = ret.replaceAll("tt", "aa");
 387         ret = ret.replaceAll("g", "GG");
 388         return ret;
 389     }
 390 
 391     private static Locale[] getSupportedCalendarLocales() {
 392         if (supportedLocale.length != 0 &&
 393             supportedLocaleSet.contains(Locale.JAPAN) &&
 394             isJapaneseCalendar()) {
 395             Locale[] sup = new Locale[supportedLocale.length+1];
 396             sup[0] = JRELocaleConstants.JA_JP_JP;
 397             System.arraycopy(supportedLocale, 0, sup, 1, supportedLocale.length);
 398             return sup;
 399         }
 400         return supportedLocale;
 401     }
 402 
 403     private static boolean isSupportedCalendarLocale(Locale locale) {
 404         // special case for ja_JP_JP
 405         if (JRELocaleConstants.JA_JP_JP.equals(locale)) {
 406             return isJapaneseCalendar();
 407         }
 408 
 409         Locale base = locale.stripExtensions();
 410         if (!supportedLocaleSet.contains(base)) {
 411             return false;
 412         }
 413 
 414         String caltype = locale.getUnicodeLocaleType("ca");
 415         if (caltype == null) {
 416             return true;
 417         }
 418 
 419         return caltype.equals(
 420             calIDToLDML[getCalendarID(locale.toLanguageTag())]
 421             .replaceFirst("_.*", ""));
 422     }
 423 
 424     private static Locale[] getSupportedNativeDigitLocales() {
 425         if (supportedLocale.length != 0 &&
 426             supportedLocaleSet.contains(JRELocaleConstants.TH_TH) &&
 427             isNativeDigit("th-TH")) {
 428             Locale[] sup = new Locale[supportedLocale.length+1];
 429             sup[0] = JRELocaleConstants.TH_TH_TH;
 430             System.arraycopy(supportedLocale, 0, sup, 1, supportedLocale.length);
 431             return sup;
 432         }
 433         return supportedLocale;
 434     }
 435 
 436     private static boolean isSupportedNativeDigitLocale(Locale locale) {
 437         // special case for th_TH_TH
 438         if (JRELocaleConstants.TH_TH_TH.equals(locale)) {
 439             return isNativeDigit("th-TH");
 440         }
 441 
 442         String numtype = null;
 443         Locale base = locale;
 444         if (locale.hasExtensions()) {
 445             numtype = locale.getUnicodeLocaleType("nu");
 446             base = locale.stripExtensions();
 447         }
 448 
 449         if (supportedLocaleSet.contains(base)) {
 450             // Only supports Latin or Thai (in thai locales) digits.
 451             if (numtype == null || numtype.equals("latn")) {
 452                 return true;
 453             } else if (locale.getLanguage().equals("th")) {
 454                 return "thai".equals(numtype) &&
 455                        isNativeDigit(locale.toLanguageTag());
 456             }
 457         }
 458 
 459         return false;
 460     }
 461 
 462     private static Locale removeExtensions(Locale src) {
 463         return new Locale.Builder().setLocale(src).clearExtensions().build();
 464     }
 465 
 466     private static boolean isJapaneseCalendar() {
 467         return getCalendarID("ja-JP") == 3; // 3: CAL_JAPAN
 468     }
 469 
 470     private static Locale getCalendarLocale(Locale locale) {
 471         int calid = getCalendarID(locale.toLanguageTag());
 472         if (calid > 0 && calid < calIDToLDML.length) {
 473             Locale.Builder lb = new Locale.Builder();
 474             String[] caltype = calIDToLDML[calid].split("_");
 475             if (caltype.length > 1) {
 476                 lb.setLocale(Locale.forLanguageTag(caltype[1]));
 477             } else {
 478                 lb.setLocale(locale);
 479             }
 480             lb.setUnicodeLocaleKeyword("ca", caltype[0]);
 481             return lb.build();
 482         }
 483 
 484         return locale;
 485     }
 486 
 487     private static Locale getNumberLocale(Locale src) {
 488         if (JRELocaleConstants.TH_TH.equals(src)) {
 489             if (isNativeDigit("th-TH")) {
 490                 Locale.Builder lb = new Locale.Builder().setLocale(src);
 491                 lb.setUnicodeLocaleKeyword("nu", "thai");
 492                 return lb.build();
 493             }
 494         }
 495 
 496         return src;
 497     }
 498 
 499     // native methods
 500 
 501     // initialize
 502     private static native boolean initialize();
 503     private static native String getDefaultLocale(int cat);
 504 
 505     // For DateFormatProvider
 506     private static native String getDateTimePattern(int dateStyle, int timeStyle, String langTag);
 507     private static native int getCalendarID(String langTag);
 508 
 509     // For DateFormatSymbolsProvider
 510     private static native String[] getAmPmStrings(String langTag, String[] ampm);
 511     private static native String[] getEras(String langTag, String[] eras);
 512     private static native String[] getMonths(String langTag, String[] months);
 513     private static native String[] getShortMonths(String langTag, String[] smonths);
 514     private static native String[] getWeekdays(String langTag, String[] wdays);
 515     private static native String[] getShortWeekdays(String langTag, String[] swdays);
 516 
 517     // For NumberFormatProvider
 518     private static native String getNumberPattern(int numberStyle, String langTag);
 519     private static native boolean isNativeDigit(String langTag);
 520 
 521     // For DecimalFormatSymbolsProvider
 522     private static native String getCurrencySymbol(String langTag, String currencySymbol);
 523     private static native char getDecimalSeparator(String langTag, char decimalSeparator);
 524     private static native char getGroupingSeparator(String langTag, char groupingSeparator);
 525     private static native String getInfinity(String langTag, String infinity);
 526     private static native String getInternationalCurrencySymbol(String langTag, String internationalCurrencySymbol);
 527     private static native char getMinusSign(String langTag, char minusSign);
 528     private static native char getMonetaryDecimalSeparator(String langTag, char monetaryDecimalSeparator);
 529     private static native String getNaN(String langTag, String nan);
 530     private static native char getPercent(String langTag, char percent);
 531     private static native char getPerMill(String langTag, char perMill);
 532     private static native char getZeroDigit(String langTag, char zeroDigit);
 533 
 534     // For CalendarDataProvider
 535     private static native int getCalendarDataValue(String langTag, int type);
 536 }