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                              .setInstant(System.currentTimeMillis())
 426                              .build();
 427             }
 428         };
 429     }
 430 
 431     public static CurrencyNameProvider getCurrencyNameProvider() {
 432         return new CurrencyNameProvider() {
 433             @Override
 434             public Locale[] getAvailableLocales() {
 435                 return supportedLocale;
 436             }
 437 
 438             @Override
 439             public boolean isSupportedLocale(Locale locale) {
 440                 // Ignore the extensions for now
 441                 return supportedLocaleSet.contains(locale.stripExtensions());
 442             }
 443 
 444             @Override
 445             public String getDisplayName(String code, Locale locale) {
 446                 return getDisplayString(locale.toLanguageTag(), DN_CURRENCY_CODE, code);
 447             }
 448 
 449             @Override
 450             public String getSymbol(String code, Locale locale) {
 451                 return getDisplayString(locale.toLanguageTag(), DN_CURRENCY_SYMBOL, code);
 452             }
 453         };
 454     }
 455 
 456     public static LocaleNameProvider getLocaleNameProvider() {
 457         return new LocaleNameProvider() {
 458             @Override
 459             public Locale[] getAvailableLocales() {
 460                 return supportedLocale;
 461             }
 462 
 463             @Override
 464             public boolean isSupportedLocale(Locale locale) {
 465                 // Ignore the extensions for now
 466                 return supportedLocaleSet.contains(locale.stripExtensions());
 467             }
 468 
 469             @Override
 470             public String getDisplayLanguage(String languageCode, Locale locale) {
 471                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_LANGUAGE, languageCode);
 472             }
 473 
 474             @Override
 475             public String getDisplayCountry(String countryCode, Locale locale) {
 476                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_REGION, countryCode);
 477             }
 478 
 479             @Override
 480             public String getDisplayScript(String scriptCode, Locale locale) {
 481                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_SCRIPT, scriptCode);
 482             }
 483 
 484             @Override
 485             public String getDisplayVariant(String variantCode, Locale locale) {
 486                 return getDisplayString(locale.toLanguageTag(), DN_LOCALE_VARIANT, variantCode);
 487             }
 488         };
 489     }
 490 
 491     public static TimeZoneNameProvider getTimeZoneNameProvider() {
 492         return new TimeZoneNameProvider() {
 493             @Override
 494             public Locale[] getAvailableLocales() {
 495                 return supportedLocale;
 496             }
 497 
 498             @Override
 499             public boolean isSupportedLocale(Locale locale) {
 500                 // Ignore the extensions for now
 501                 return supportedLocaleSet.contains(locale.stripExtensions());
 502             }
 503 
 504             @Override
 505             public String getDisplayName(String ID, boolean daylight, int style, Locale locale) {
 506                 return getTimeZoneDisplayString(locale.toLanguageTag(), style * 2 + (daylight ? 1 : 0), ID);
 507             }
 508         };
 509     }
 510 
 511     private static Locale[] getSupportedCalendarLocales() {
 512         if (supportedLocale.length != 0 &&
 513             supportedLocaleSet.contains(Locale.JAPAN) &&
 514             isJapaneseCalendar()) {
 515             Locale[] sup = new Locale[supportedLocale.length+1];
 516             sup[0] = JRELocaleConstants.JA_JP_JP;
 517             System.arraycopy(supportedLocale, 0, sup, 1, supportedLocale.length);
 518             return sup;
 519         }
 520         return supportedLocale;
 521     }
 522 
 523     private static boolean isSupportedCalendarLocale(Locale locale) {
 524         Locale base = locale.stripExtensions();
 525         if (!supportedLocaleSet.contains(base)) {
 526             return false;
 527         }
 528 
 529         String requestedCalType = locale.getUnicodeLocaleType("ca");
 530         String nativeCalType =
 531             getCalendarID(locale.toLanguageTag()).replaceFirst("gregorian", "gregory");
 532 
 533         if (requestedCalType == null) {
 534             return Calendar.getAvailableCalendarTypes().contains(nativeCalType);
 535         } else {
 536             return requestedCalType.equals(nativeCalType);
 537         }
 538     }
 539 
 540     private static boolean isJapaneseCalendar() {
 541         return getCalendarID("ja-JP").equals("japanese");
 542     }
 543 
 544     private static Locale getCalendarLocale(Locale locale) {
 545         String nativeCalType = getCalendarID(locale.toLanguageTag())
 546                      .replaceFirst("gregorian", "gregory");
 547         if (Calendar.getAvailableCalendarTypes().contains(nativeCalType)) {
 548             return new Locale.Builder()
 549                            .setLocale(locale)
 550                            .setUnicodeLocaleKeyword("ca", nativeCalType)
 551                            .build();
 552         } else {
 553             return locale;
 554         }
 555     }
 556 
 557     // The following methods are copied from CLDRConverter build tool.
 558     private static String translateDateFormatLetters(String calendarType, String cldrFormat) {
 559         String pattern = cldrFormat;
 560         int length = pattern.length();
 561         boolean inQuote = false;
 562         StringBuilder jrePattern = new StringBuilder(length);
 563         int count = 0;
 564         char lastLetter = 0;
 565 
 566         for (int i = 0; i < length; i++) {
 567             char c = pattern.charAt(i);
 568 
 569             if (c == '\'') {
 570                 // '' is treated as a single quote regardless of being
 571                 // in a quoted section.
 572                 if ((i + 1) < length) {
 573                     char nextc = pattern.charAt(i + 1);
 574                     if (nextc == '\'') {
 575                         i++;
 576                         if (count != 0) {
 577                             convert(calendarType, lastLetter, count, jrePattern);
 578                             lastLetter = 0;
 579                             count = 0;
 580                         }
 581                         jrePattern.append("''");
 582                         continue;
 583                     }
 584                 }
 585                 if (!inQuote) {
 586                     if (count != 0) {
 587                         convert(calendarType, lastLetter, count, jrePattern);
 588                         lastLetter = 0;
 589                         count = 0;
 590                     }
 591                     inQuote = true;
 592                 } else {
 593                     inQuote = false;
 594                 }
 595                 jrePattern.append(c);
 596                 continue;
 597             }
 598             if (inQuote) {
 599                 jrePattern.append(c);
 600                 continue;
 601             }
 602             if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
 603                 if (count != 0) {
 604                     convert(calendarType, lastLetter, count, jrePattern);
 605                     lastLetter = 0;
 606                     count = 0;
 607                 }
 608                 jrePattern.append(c);
 609                 continue;
 610             }
 611 
 612             if (lastLetter == 0 || lastLetter == c) {
 613                 lastLetter = c;
 614                 count++;
 615                 continue;
 616             }
 617             convert(calendarType, lastLetter, count, jrePattern);
 618             lastLetter = c;
 619             count = 1;
 620         }
 621 
 622         if (count != 0) {
 623             convert(calendarType, lastLetter, count, jrePattern);
 624         }
 625         if (cldrFormat.contentEquals(jrePattern)) {
 626             return cldrFormat;
 627         }
 628         return jrePattern.toString();
 629     }
 630 
 631     private static void convert(String calendarType, char cldrLetter, int count, StringBuilder sb) {
 632         switch (cldrLetter) {
 633         case 'G':
 634             if (!calendarType.equals("gregorian")) {
 635                 // Adjust the number of 'G's for JRE SimpleDateFormat
 636                 if (count == 5) {
 637                     // CLDR narrow -> JRE short
 638                     count = 1;
 639                 } else if (count == 1) {
 640                     // CLDR abbr -> JRE long
 641                     count = 4;
 642                 }
 643             }
 644             appendN(cldrLetter, count, sb);
 645             break;
 646 
 647         // TODO: support 'c' and 'e' in JRE SimpleDateFormat
 648         // Use 'u' and 'E' for now.
 649         case 'c':
 650         case 'e':
 651             switch (count) {
 652             case 1:
 653                 sb.append('u');
 654                 break;
 655             case 3:
 656             case 4:
 657                 appendN('E', count, sb);
 658                 break;
 659             case 5:
 660                 appendN('E', 3, sb);
 661                 break;
 662             }
 663             break;
 664 
 665         case 'v':
 666         case 'V':
 667             appendN('z', count, sb);
 668             break;
 669 
 670         case 'Z':
 671             if (count == 4 || count == 5) {
 672                 sb.append("XXX");
 673             }
 674             break;
 675 
 676         case 'u':
 677         case 'U':
 678         case 'q':
 679         case 'Q':
 680         case 'l':
 681         case 'g':
 682         case 'j':
 683         case 'A':
 684             // Unsupported letter. Just append it within quotes
 685             sb.append('\'');
 686             sb.append(cldrLetter);
 687             sb.append('\'');
 688             break;
 689 
 690         default:
 691             appendN(cldrLetter, count, sb);
 692             break;
 693         }
 694     }
 695 
 696     private static void appendN(char c, int n, StringBuilder sb) {
 697         for (int i = 0; i < n; i++) {
 698             sb.append(c);
 699         }
 700     }
 701 
 702     // initialize
 703     private static native String getDefaultLocale(int cat);
 704 
 705     // For DateFormatProvider
 706     private static native String getDateTimePatternNative(int dateStyle, int timeStyle, String langtag);
 707     private static native String getCalendarID(String langTag);
 708 
 709     // For NumberFormatProvider
 710     private static native String getNumberPatternNative(int style, String langtag);
 711 
 712     // For DateFormatSymbolsProvider
 713     private static native String[] getAmPmStrings(String langTag, String[] ampm);
 714     private static native String[] getEras(String langTag, String[] eras);
 715     private static native String[] getMonths(String langTag, String[] months);
 716     private static native String[] getShortMonths(String langTag, String[] smonths);
 717     private static native String[] getWeekdays(String langTag, String[] wdays);
 718     private static native String[] getShortWeekdays(String langTag, String[] swdays);
 719 
 720     // For DecimalFormatSymbolsProvider
 721     private static native String getCurrencySymbol(String langTag, String currencySymbol);
 722     private static native char getDecimalSeparator(String langTag, char decimalSeparator);
 723     private static native char getGroupingSeparator(String langTag, char groupingSeparator);
 724     private static native String getInfinity(String langTag, String infinity);
 725     private static native String getInternationalCurrencySymbol(String langTag, String internationalCurrencySymbol);
 726     private static native char getMinusSign(String langTag, char minusSign);
 727     private static native char getMonetaryDecimalSeparator(String langTag, char monetaryDecimalSeparator);
 728     private static native String getNaN(String langTag, String nan);
 729     private static native char getPercent(String langTag, char percent);
 730     private static native char getPerMill(String langTag, char perMill);
 731     private static native char getZeroDigit(String langTag, char zeroDigit);
 732     private static native String getExponentSeparator(String langTag, String exponent);
 733 
 734     // For CalendarDataProvider
 735     private static native int getCalendarInt(String langTag, int type);
 736 
 737     // For Locale/CurrencyNameProvider
 738     private static native String getDisplayString(String langTag, int key, String value);
 739 
 740     // For TimeZoneNameProvider
 741     private static native String getTimeZoneDisplayString(String langTag, int style, String value);
 742 }