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 
  26 package sun.util.locale.provider;
  27 
  28 import java.lang.ref.SoftReference;
  29 import java.text.*;
  30 import java.text.spi.DateFormatProvider;
  31 import java.text.spi.DateFormatSymbolsProvider;
  32 import java.text.spi.DecimalFormatSymbolsProvider;
  33 import java.text.spi.NumberFormatProvider;
  34 import java.util.Collections;
  35 import java.util.Calendar;
  36 import java.util.HashSet;
  37 import java.util.Locale;
  38 import java.util.Map;
  39 import java.util.ResourceBundle.Control;
  40 import java.util.Set;
  41 import java.util.TimeZone;
  42 import java.util.concurrent.ConcurrentHashMap;
  43 import java.util.concurrent.ConcurrentMap;
  44 import java.util.concurrent.atomic.AtomicReferenceArray;
  45 import java.util.spi.CalendarDataProvider;
  46 import java.util.spi.CalendarNameProvider;
  47 import java.util.spi.CurrencyNameProvider;
  48 import java.util.spi.LocaleNameProvider;
  49 import java.util.spi.TimeZoneNameProvider;
  50 import sun.util.spi.CalendarProvider;
  51 
  52 /**
  53  * LocaleProviderAdapter implementation for the Mac OS X locale data
  54  *
  55  * @author Naoto Sato
  56  */
  57 public class HostLocaleProviderAdapterImpl {
  58 
  59     // per supported locale instances
  60     private static ConcurrentMap<Locale, SoftReference<AtomicReferenceArray<String>>> dateFormatPatternsMap =
  61         new ConcurrentHashMap<>(2);
  62     private static ConcurrentMap<Locale, SoftReference<AtomicReferenceArray<String>>> numberFormatPatternsMap =
  63         new ConcurrentHashMap<>(2);
  64     private static ConcurrentMap<Locale, SoftReference<DateFormatSymbols>> dateFormatSymbolsMap =
  65         new ConcurrentHashMap<>(2);
  66     private static ConcurrentMap<Locale, SoftReference<DecimalFormatSymbols>> decimalFormatSymbolsMap =
  67         new ConcurrentHashMap<>(2);
  68 
  69     // locale categories
  70     private static final int CAT_DISPLAY = 0;
  71     private static final int CAT_FORMAT  = 1;
  72 
  73     // NumberFormat styles
  74     private static final int NF_NUMBER   = 0;
  75     private static final int NF_CURRENCY = 1;
  76     private static final int NF_PERCENT  = 2;
  77     private static final int NF_INTEGER  = 3;
  78     private static final int NF_MAX = NF_INTEGER;
  79 
  80     // CalendarData value types
  81     private static final int CD_FIRSTDAYOFWEEK = 0;
  82     private static final int CD_MINIMALDAYSINFIRSTWEEK = 1;
  83 
  84     // Locale/Currency display name types
  85     private static final int DN_LOCALE_LANGUAGE = 0;
  86     private static final int DN_LOCALE_SCRIPT   = 1;
  87     private static final int DN_LOCALE_REGION   = 2;
  88     private static final int DN_LOCALE_VARIANT  = 3;
  89     private static final int DN_CURRENCY_CODE   = 4;
  90     private static final int DN_CURRENCY_SYMBOL = 5;
  91 
  92     // TimeZone display name types
  93     private static final int DN_TZ_SHORT_STANDARD = 0;
  94     private static final int DN_TZ_SHORT_DST      = 1;
  95     private static final int DN_TZ_LONG_STANDARD  = 2;
  96     private static final int DN_TZ_LONG_DST       = 3;
  97 
  98     private static final Set<Locale> supportedLocaleSet;
  99     static {
 100         Set<Locale> tmpSet = new HashSet<>();
 101         // Assuming the default locales do not include any extensions, so
 102         // no stripping is needed here.
 103         Locale l = convertPosixLocaleToJavaLocale(getDefaultLocale(CAT_FORMAT));
 104         tmpSet.addAll(Control.getNoFallbackControl(Control.FORMAT_DEFAULT).getCandidateLocales("", l));
 105         l = convertPosixLocaleToJavaLocale(getDefaultLocale(CAT_DISPLAY));
 106         tmpSet.addAll(Control.getNoFallbackControl(Control.FORMAT_DEFAULT).getCandidateLocales("", l));
 107         supportedLocaleSet = Collections.unmodifiableSet(tmpSet);
 108     }
 109     private final static Locale[] supportedLocale = supportedLocaleSet.toArray(new Locale[0]);
 110 
 111     @SuppressWarnings("fallthrough")
 112     private static Locale convertPosixLocaleToJavaLocale(String posix) {
 113         // MacOSX may return ICU notation, here is the quote from CFLocale doc:
 114         // "The corresponding value is a CFString containing the POSIX locale
 115         // identifier as used by ICU, such as "ja_JP". If you have a variant
 116         // locale or a different currency or calendar, it can be as complex as
 117         // "en_US_POSIX@calendar=japanese;currency=EUR" or
 118         // "az_Cyrl_AZ@calendar=buddhist;currency=JPY".
 119         String[] tmp = posix.split("@");
 120         String langTag = tmp[0].replaceAll("_","-");
 121         if (tmp.length > 1) {
 122             String[] ext = tmp[1].split(";");
 123             for (String keyval : ext) {
 124                 // We are only interested in "calendar" value for now.
 125                 if (keyval.startsWith("calendar=")) {
 126                     String calid = keyval.substring(keyval.indexOf('=')+1);
 127                     switch (calid) {
 128                         case "gregorian":
 129                             langTag += "-u-ca-gregory";
 130                             break;
 131                         case "japanese":
 132                             // Tweak for ja_JP_JP
 133                             if (tmp[0].equals("ja_JP")) {
 134                                 return JRELocaleConstants.JA_JP_JP;
 135                             }
 136 
 137                             // fall through
 138 
 139                         default:
 140                             langTag += "-u-ca-" + calid;
 141                             break;
 142                     }
 143                 }
 144             }
 145         }
 146 
 147         return Locale.forLanguageTag(langTag);
 148     }
 149 
 150     public static DateFormatProvider getDateFormatProvider() {
 151         return new DateFormatProvider() {
 152 
 153             @Override
 154             public Locale[] getAvailableLocales() {
 155                 return getSupportedCalendarLocales();
 156             }
 157 
 158             @Override
 159             public boolean isSupportedLocale(Locale locale) {
 160                 return isSupportedCalendarLocale(locale);
 161             }
 162 
 163             @Override
 164             public DateFormat getDateInstance(int style, Locale locale) {
 165                 return new SimpleDateFormat(getDateTimePattern(style, -1, locale),
 166                                             getCalendarLocale(locale));
 167             }
 168 
 169             @Override
 170             public DateFormat getTimeInstance(int style, Locale locale) {
 171                 return new SimpleDateFormat(getDateTimePattern(-1, style, locale),
 172                                             getCalendarLocale(locale));
 173             }
 174 
 175             @Override
 176             public DateFormat getDateTimeInstance(int dateStyle,
 177                     int timeStyle, Locale locale) {
 178                 return new SimpleDateFormat(getDateTimePattern(dateStyle, timeStyle, locale),
 179                                             getCalendarLocale(locale));
 180             }
 181 
 182             private String getDateTimePattern(int dateStyle, int timeStyle, Locale locale) {
 183                 AtomicReferenceArray<String> dateFormatPatterns;
 184                 SoftReference<AtomicReferenceArray<String>> ref = dateFormatPatternsMap.get(locale);
 185 
 186                 if (ref == null || (dateFormatPatterns = ref.get()) == null) {
 187                     dateFormatPatterns = new AtomicReferenceArray<>(5 * 5);
 188                     ref = new SoftReference<>(dateFormatPatterns);
 189                     dateFormatPatternsMap.put(locale, ref);
 190                 }
 191 
 192                 int index = (dateStyle + 1) * 5 + timeStyle + 1;
 193                 String pattern = dateFormatPatterns.get(index);
 194                 if (pattern == null) {
 195                     String langTag = locale.toLanguageTag();
 196                     pattern = translateDateFormatLetters(getCalendarID(langTag),
 197                             getDateTimePatternNative(dateStyle, timeStyle, langTag));
 198                     if (!dateFormatPatterns.compareAndSet(index, null, pattern)) {
 199                         pattern = dateFormatPatterns.get(index);
 200                     }
 201                 }
 202 
 203                 return pattern;
 204             }
 205         };
 206     }
 207 
 208     public static DateFormatSymbolsProvider getDateFormatSymbolsProvider() {
 209         return new DateFormatSymbolsProvider() {
 210             @Override
 211             public Locale[] getAvailableLocales() {
 212                 if (isSupportedLocale(Locale.getDefault(Locale.Category.FORMAT))) {
 213                     return supportedLocale;
 214                 }
 215                 return new Locale[0];
 216             }
 217 
 218             @Override
 219             public boolean isSupportedLocale(Locale locale) {
 220                 // Only supports the locale with Gregorian calendar
 221                 Locale base = locale.stripExtensions();
 222                 if (supportedLocaleSet.contains(base)) {
 223                     return getCalendarID(locale.toLanguageTag()).equals("gregorian");
 224                 }
 225                 return false;
 226             }
 227 
 228             @Override
 229             public DateFormatSymbols getInstance(Locale locale) {
 230                 DateFormatSymbols dateFormatSymbols;
 231                 SoftReference<DateFormatSymbols> ref = dateFormatSymbolsMap.get(locale);
 232 
 233                 if (ref == null || (dateFormatSymbols = ref.get()) == null) {
 234                     dateFormatSymbols = new DateFormatSymbols(locale);
 235                     String langTag = locale.toLanguageTag();
 236                     dateFormatSymbols.setAmPmStrings(getAmPmStrings(langTag, dateFormatSymbols.getAmPmStrings()));
 237                     dateFormatSymbols.setEras(getEras(langTag, dateFormatSymbols.getEras()));
 238                     dateFormatSymbols.setMonths(getMonths(langTag, dateFormatSymbols.getMonths()));
 239                     dateFormatSymbols.setShortMonths(getShortMonths(langTag, dateFormatSymbols.getShortMonths()));
 240                     dateFormatSymbols.setWeekdays(getWeekdays(langTag, dateFormatSymbols.getWeekdays()));
 241                     dateFormatSymbols.setShortWeekdays(getShortWeekdays(langTag, dateFormatSymbols.getShortWeekdays()));
 242                     ref = new SoftReference<>(dateFormatSymbols);
 243                     dateFormatSymbolsMap.put(locale, ref);
 244                 }
 245                 return (DateFormatSymbols)dateFormatSymbols.clone();
 246             }
 247         };
 248     }
 249 
 250     public static NumberFormatProvider getNumberFormatProvider() {
 251         return new NumberFormatProvider() {
 252             @Override
 253             public Locale[] getAvailableLocales() {
 254                 return supportedLocale;
 255             }
 256 
 257             @Override
 258             public boolean isSupportedLocale(Locale locale) {
 259                 // Ignore the extensions for now
 260                 return supportedLocaleSet.contains(locale.stripExtensions());
 261             }
 262 
 263             @Override
 264             public NumberFormat getCurrencyInstance(Locale locale) {
 265                 return new DecimalFormat(getNumberPattern(NF_CURRENCY, locale),
 266                     DecimalFormatSymbols.getInstance(locale));
 267             }
 268 
 269             @Override
 270             public NumberFormat getIntegerInstance(Locale locale) {
 271                 return new DecimalFormat(getNumberPattern(NF_INTEGER, locale),
 272                     DecimalFormatSymbols.getInstance(locale));
 273             }
 274 
 275             @Override
 276             public NumberFormat getNumberInstance(Locale locale) {
 277                 return new DecimalFormat(getNumberPattern(NF_NUMBER, locale),
 278                     DecimalFormatSymbols.getInstance(locale));
 279             }
 280 
 281             @Override
 282             public NumberFormat getPercentInstance(Locale locale) {
 283                 return new DecimalFormat(getNumberPattern(NF_PERCENT, locale),
 284                     DecimalFormatSymbols.getInstance(locale));
 285             }
 286 
 287             private String getNumberPattern(int style, Locale locale) {
 288                 AtomicReferenceArray<String> numberFormatPatterns;
 289                 SoftReference<AtomicReferenceArray<String>> ref = numberFormatPatternsMap.get(locale);
 290 
 291                 if (ref == null || (numberFormatPatterns = ref.get()) == null) {
 292                     numberFormatPatterns = new AtomicReferenceArray<>(4);
 293                     ref = new SoftReference<>(numberFormatPatterns);
 294                     numberFormatPatternsMap.put(locale, ref);
 295                 }
 296 
 297                 String pattern = numberFormatPatterns.get(style);
 298                 if (pattern == null) {
 299                     pattern = getNumberPatternNative(style, locale.toLanguageTag());
 300                     if (!numberFormatPatterns.compareAndSet(style, null, pattern)) {
 301                         pattern = numberFormatPatterns.get(style);
 302                     }
 303                 }
 304 
 305                 return pattern;
 306             }
 307         };
 308     }
 309 
 310     public static DecimalFormatSymbolsProvider getDecimalFormatSymbolsProvider() {
 311         return new DecimalFormatSymbolsProvider() {
 312 
 313             @Override
 314             public Locale[] getAvailableLocales() {
 315                 return supportedLocale;
 316             }
 317 
 318             @Override
 319             public boolean isSupportedLocale(Locale locale) {
 320                 // Ignore the extensions for now
 321                 return supportedLocaleSet.contains(locale.stripExtensions());
 322             }
 323 
 324             @Override
 325             public DecimalFormatSymbols getInstance(Locale locale) {
 326                 DecimalFormatSymbols decimalFormatSymbols;
 327                 SoftReference<DecimalFormatSymbols> ref = decimalFormatSymbolsMap.get(locale);
 328 
 329                 if (ref == null || (decimalFormatSymbols = ref.get()) == null) {
 330                     decimalFormatSymbols = new DecimalFormatSymbols(locale);
 331                     String langTag = locale.toLanguageTag();
 332 
 333                     // DecimalFormatSymbols.setInternationalCurrencySymbol() has
 334                     // a side effect of setting the currency symbol as well. So
 335                     // the calling order is relevant here.
 336                     decimalFormatSymbols.setInternationalCurrencySymbol(getInternationalCurrencySymbol(langTag, decimalFormatSymbols.getInternationalCurrencySymbol()));
 337                     decimalFormatSymbols.setCurrencySymbol(getCurrencySymbol(langTag, decimalFormatSymbols.getCurrencySymbol()));
 338                     decimalFormatSymbols.setDecimalSeparator(getDecimalSeparator(langTag, decimalFormatSymbols.getDecimalSeparator()));
 339                     decimalFormatSymbols.setGroupingSeparator(getGroupingSeparator(langTag, decimalFormatSymbols.getGroupingSeparator()));
 340                     decimalFormatSymbols.setInfinity(getInfinity(langTag, decimalFormatSymbols.getInfinity()));
 341                     decimalFormatSymbols.setMinusSign(getMinusSign(langTag, decimalFormatSymbols.getMinusSign()));
 342                     decimalFormatSymbols.setMonetaryDecimalSeparator(getMonetaryDecimalSeparator(langTag, decimalFormatSymbols.getMonetaryDecimalSeparator()));
 343                     decimalFormatSymbols.setNaN(getNaN(langTag, decimalFormatSymbols.getNaN()));
 344                     decimalFormatSymbols.setPercent(getPercent(langTag, decimalFormatSymbols.getPercent()));
 345                     decimalFormatSymbols.setPerMill(getPerMill(langTag, decimalFormatSymbols.getPerMill()));
 346                     decimalFormatSymbols.setZeroDigit(getZeroDigit(langTag, decimalFormatSymbols.getZeroDigit()));
 347                     decimalFormatSymbols.setExponentSeparator(getExponentSeparator(langTag, decimalFormatSymbols.getExponentSeparator()));
 348                     ref = new SoftReference<>(decimalFormatSymbols);
 349                     decimalFormatSymbolsMap.put(locale, ref);
 350                 }
 351                 return (DecimalFormatSymbols)decimalFormatSymbols.clone();
 352             }
 353         };
 354     }
 355 
 356     public static CalendarDataProvider getCalendarDataProvider() {
 357         return new CalendarDataProvider() {
 358             @Override
 359             public Locale[] getAvailableLocales() {
 360                 return getSupportedCalendarLocales();
 361             }
 362 
 363             @Override
 364             public boolean isSupportedLocale(Locale locale) {
 365                 return isSupportedCalendarLocale(locale);
 366             }
 367 
 368             @Override
 369             public int getFirstDayOfWeek(Locale locale) {
 370                 return getCalendarInt(locale.toLanguageTag(), CD_FIRSTDAYOFWEEK);
 371             }
 372 
 373             @Override
 374             public int getMinimalDaysInFirstWeek(Locale locale) {
 375                 return getCalendarInt(locale.toLanguageTag(), CD_MINIMALDAYSINFIRSTWEEK);
 376             }
 377         };
 378     }
 379 
 380     public static CalendarNameProvider getCalendarNameProvider() {
 381         return new CalendarNameProvider() {
 382             @Override
 383             public Locale[] getAvailableLocales() {
 384                 return getSupportedCalendarLocales();
 385             }
 386 
 387             @Override
 388             public boolean isSupportedLocale(Locale locale) {
 389                 return isSupportedCalendarLocale(locale);
 390             }
 391 
 392             @Override
 393             public String getDisplayName(String calType, int field, int value,
 394                                          int style, Locale locale) {
 395                 return null;
 396             }
 397 
 398             @Override
 399             public Map<String, Integer> getDisplayNames(String calType,
 400                                          int field, int style, Locale locale) {
 401                 return null;
 402             }
 403         };
 404     }
 405 
 406     public static CalendarProvider getCalendarProvider() {
 407         return new CalendarProvider() {
 408             @Override
 409             public Locale[] getAvailableLocales() {
 410                 return getSupportedCalendarLocales();
 411             }
 412 
 413             @Override
 414             public boolean isSupportedLocale(Locale locale) {
 415                 return isSupportedCalendarLocale(locale);
 416             }
 417 
 418             @Override
 419             public Calendar getInstance(TimeZone zone, Locale locale) {
 420                 return new Calendar.Builder()
 421                              .setLocale(locale)
 422                              .setCalendarType(getCalendarID(locale.toLanguageTag())
 423                                .replaceFirst("gregorian", "gregory"))
 424                              .setTimeZone(zone)
 425                              .build();
 426             }
 427         };
 428     }
 429 
 430     public static CurrencyNameProvider getCurrencyNameProvider() {
 431         return new CurrencyNameProvider() {
 432             @Override
 433             public Locale[] getAvailableLocales() {
 434                 return supportedLocale;
 435             }
 436 
 437             @Override
 438             public boolean isSupportedLocale(Locale locale) {
 439                 // Ignore the extensions for now
 440                 return supportedLocaleSet.contains(locale.stripExtensions());
 441             }
 442 
 443             @Override
 444             public String getDisplayName(String code, Locale locale) {
 445                 return getDisplayString(locale.toLanguageTag(), DN_CURRENCY_CODE, code);
 446             }
 447 
 448             @Override
 449             public String getSymbol(String code, Locale locale) {
 450                 return getDisplayString(locale.toLanguageTag(), DN_CURRENCY_SYMBOL, code);
 451             }
 452         };
 453     }
 454 
 455     public static LocaleNameProvider getLocaleNameProvider() {
 456         return new LocaleNameProvider() {
 457             @Override
 458             public Locale[] getAvailableLocales() {
 459                 return supportedLocale;
 460             }
 461 
 462             @Override
 463             public boolean isSupportedLocale(Locale locale) {
 464                 // Ignore the extensions for now
 465                 return supportedLocaleSet.contains(locale.stripExtensions());
 466             }
 467 
 468             @Override
 469             public String getDisplayLanguage(String languageCode, Locale locale) {
 470                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_LANGUAGE, languageCode);
 471             }
 472 
 473             @Override
 474             public String getDisplayCountry(String countryCode, Locale locale) {
 475                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_REGION, countryCode);
 476             }
 477 
 478             @Override
 479             public String getDisplayScript(String scriptCode, Locale locale) {
 480                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_SCRIPT, scriptCode);
 481             }
 482 
 483             @Override
 484             public String getDisplayVariant(String variantCode, Locale locale) {
 485                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_VARIANT, variantCode);
 486             }
 487         };
 488     }
 489 
 490     public static TimeZoneNameProvider getTimeZoneNameProvider() {
 491         return new TimeZoneNameProvider() {
 492             @Override
 493             public Locale[] getAvailableLocales() {
 494                 return supportedLocale;
 495             }
 496 
 497             @Override
 498             public boolean isSupportedLocale(Locale locale) {
 499                 // Ignore the extensions for now
 500                 return supportedLocaleSet.contains(locale.stripExtensions());
 501             }
 502 
 503             @Override
 504             public String getDisplayName(String ID, boolean daylight, int style, Locale locale) {
 505                 return getTimeZoneDisplayString(locale.toLanguageTag(), style * 2 + (daylight ? 1 : 0), ID);
 506             }
 507         };
 508     }
 509 
 510     private static Locale[] getSupportedCalendarLocales() {
 511         if (supportedLocale.length != 0 &&
 512             supportedLocaleSet.contains(Locale.JAPAN) &&
 513             isJapaneseCalendar()) {
 514             Locale[] sup = new Locale[supportedLocale.length+1];
 515             sup[0] = JRELocaleConstants.JA_JP_JP;
 516             System.arraycopy(supportedLocale, 0, sup, 1, supportedLocale.length);
 517             return sup;
 518         }
 519         return supportedLocale;
 520     }
 521 
 522     private static boolean isSupportedCalendarLocale(Locale locale) {
 523         Locale base = locale.stripExtensions();
 524         if (!supportedLocaleSet.contains(base)) {
 525             return false;
 526         }
 527 
 528         String requestedCalType = locale.getUnicodeLocaleType("ca");
 529         String nativeCalType =
 530             getCalendarID(locale.toLanguageTag()).replaceFirst("gregorian", "gregory");
 531 
 532         if (requestedCalType == null) {
 533             return Calendar.getAvailableCalendarTypes().contains(nativeCalType);
 534         } else {
 535             return requestedCalType.equals(nativeCalType);
 536         }
 537     }
 538 
 539     private static boolean isJapaneseCalendar() {
 540         return getCalendarID("ja-JP").equals("japanese");
 541     }
 542 
 543     private static Locale getCalendarLocale(Locale locale) {
 544         String nativeCalType = getCalendarID(locale.toLanguageTag())
 545                      .replaceFirst("gregorian", "gregory");
 546         if (Calendar.getAvailableCalendarTypes().contains(nativeCalType)) {
 547             return new Locale.Builder()
 548                            .setLocale(locale)
 549                            .setUnicodeLocaleKeyword("ca", nativeCalType)
 550                            .build();
 551         } else {
 552             return locale;
 553         }
 554     }
 555 
 556     // The following methods are copied from CLDRConverter build tool.
 557     private static String translateDateFormatLetters(String calendarType, String cldrFormat) {
 558         String pattern = cldrFormat;
 559         int length = pattern.length();
 560         boolean inQuote = false;
 561         StringBuilder jrePattern = new StringBuilder(length);
 562         int count = 0;
 563         char lastLetter = 0;
 564 
 565         for (int i = 0; i < length; i++) {
 566             char c = pattern.charAt(i);
 567 
 568             if (c == '\'') {
 569                 // '' is treated as a single quote regardless of being
 570                 // in a quoted section.
 571                 if ((i + 1) < length) {
 572                     char nextc = pattern.charAt(i + 1);
 573                     if (nextc == '\'') {
 574                         i++;
 575                         if (count != 0) {
 576                             convert(calendarType, lastLetter, count, jrePattern);
 577                             lastLetter = 0;
 578                             count = 0;
 579                         }
 580                         jrePattern.append("''");
 581                         continue;
 582                     }
 583                 }
 584                 if (!inQuote) {
 585                     if (count != 0) {
 586                         convert(calendarType, lastLetter, count, jrePattern);
 587                         lastLetter = 0;
 588                         count = 0;
 589                     }
 590                     inQuote = true;
 591                 } else {
 592                     inQuote = false;
 593                 }
 594                 jrePattern.append(c);
 595                 continue;
 596             }
 597             if (inQuote) {
 598                 jrePattern.append(c);
 599                 continue;
 600             }
 601             if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
 602                 if (count != 0) {
 603                     convert(calendarType, lastLetter, count, jrePattern);
 604                     lastLetter = 0;
 605                     count = 0;
 606                 }
 607                 jrePattern.append(c);
 608                 continue;
 609             }
 610 
 611             if (lastLetter == 0 || lastLetter == c) {
 612                 lastLetter = c;
 613                 count++;
 614                 continue;
 615             }
 616             convert(calendarType, lastLetter, count, jrePattern);
 617             lastLetter = c;
 618             count = 1;
 619         }
 620 
 621         if (count != 0) {
 622             convert(calendarType, lastLetter, count, jrePattern);
 623         }
 624         if (cldrFormat.contentEquals(jrePattern)) {
 625             return cldrFormat;
 626         }
 627         return jrePattern.toString();
 628     }
 629 
 630     private static void convert(String calendarType, char cldrLetter, int count, StringBuilder sb) {
 631         switch (cldrLetter) {
 632         case 'G':
 633             if (!calendarType.equals("gregorian")) {
 634                 // Adjust the number of 'G's for JRE SimpleDateFormat
 635                 if (count == 5) {
 636                     // CLDR narrow -> JRE short
 637                     count = 1;
 638                 } else if (count == 1) {
 639                     // CLDR abbr -> JRE long
 640                     count = 4;
 641                 }
 642             }
 643             appendN(cldrLetter, count, sb);
 644             break;
 645 
 646         // TODO: support 'c' and 'e' in JRE SimpleDateFormat
 647         // Use 'u' and 'E' for now.
 648         case 'c':
 649         case 'e':
 650             switch (count) {
 651             case 1:
 652                 sb.append('u');
 653                 break;
 654             case 3:
 655             case 4:
 656                 appendN('E', count, sb);
 657                 break;
 658             case 5:
 659                 appendN('E', 3, sb);
 660                 break;
 661             }
 662             break;
 663 
 664         case 'v':
 665         case 'V':
 666             appendN('z', count, sb);
 667             break;
 668 
 669         case 'Z':
 670             if (count == 4 || count == 5) {
 671                 sb.append("XXX");
 672             }
 673             break;
 674 
 675         case 'u':
 676         case 'U':
 677         case 'q':
 678         case 'Q':
 679         case 'l':
 680         case 'g':
 681         case 'j':
 682         case 'A':
 683             // Unsupported letter. Just append it within quotes
 684             sb.append('\'');
 685             sb.append(cldrLetter);
 686             sb.append('\'');
 687             break;
 688 
 689         default:
 690             appendN(cldrLetter, count, sb);
 691             break;
 692         }
 693     }
 694 
 695     private static void appendN(char c, int n, StringBuilder sb) {
 696         for (int i = 0; i < n; i++) {
 697             sb.append(c);
 698         }
 699     }
 700 
 701     // initialize
 702     private static native String getDefaultLocale(int cat);
 703 
 704     // For DateFormatProvider
 705     private static native String getDateTimePatternNative(int dateStyle, int timeStyle, String langtag);
 706     private static native String getCalendarID(String langTag);
 707 
 708     // For NumberFormatProvider
 709     private static native String getNumberPatternNative(int style, String langtag);
 710 
 711     // For DateFormatSymbolsProvider
 712     private static native String[] getAmPmStrings(String langTag, String[] ampm);
 713     private static native String[] getEras(String langTag, String[] eras);
 714     private static native String[] getMonths(String langTag, String[] months);
 715     private static native String[] getShortMonths(String langTag, String[] smonths);
 716     private static native String[] getWeekdays(String langTag, String[] wdays);
 717     private static native String[] getShortWeekdays(String langTag, String[] swdays);
 718 
 719     // For DecimalFormatSymbolsProvider
 720     private static native String getCurrencySymbol(String langTag, String currencySymbol);
 721     private static native char getDecimalSeparator(String langTag, char decimalSeparator);
 722     private static native char getGroupingSeparator(String langTag, char groupingSeparator);
 723     private static native String getInfinity(String langTag, String infinity);
 724     private static native String getInternationalCurrencySymbol(String langTag, String internationalCurrencySymbol);
 725     private static native char getMinusSign(String langTag, char minusSign);
 726     private static native char getMonetaryDecimalSeparator(String langTag, char monetaryDecimalSeparator);
 727     private static native String getNaN(String langTag, String nan);
 728     private static native char getPercent(String langTag, char percent);
 729     private static native char getPerMill(String langTag, char perMill);
 730     private static native char getZeroDigit(String langTag, char zeroDigit);
 731     private static native String getExponentSeparator(String langTag, String exponent);
 732 
 733     // For CalendarDataProvider
 734     private static native int getCalendarInt(String langTag, int type);
 735 
 736     // For Locale/CurrencyNameProvider
 737     private static native String getDisplayString(String langTag, int key, String value);
 738 
 739     // For TimeZoneNameProvider
 740     private static native String getTimeZoneDisplayString(String langTag, int style, String value);
 741 }