1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package libcore.icu;
18 
19 import com.android.icu.util.ExtendedCalendar;
20 import java.text.DateFormat;
21 import java.text.SimpleDateFormat;
22 import java.util.Locale;
23 import java.util.Objects;
24 import java.util.concurrent.ConcurrentHashMap;
25 
26 import sun.util.locale.LanguageTag;
27 
28 /**
29  * Pattern cache for {@link SimpleDateFormat}
30  *
31  * @hide
32  */
33 public class SimpleDateFormatData {
34 
35     // TODO(http://b/217881004): Replace this with a LRU cache.
36     private static final ConcurrentHashMap<String, SimpleDateFormatData> CACHE =
37             new ConcurrentHashMap<>(/* initialCapacity */ 3);
38 
39 
40     private final Locale locale;
41     /** See {@link #isBug266731719Locale} for details */
42     private final boolean usesAsciiSpace;
43 
44     private final String fullTimeFormat;
45     private final String longTimeFormat;
46     private final String mediumTimeFormat;
47     private final String shortTimeFormat;
48 
49     private final String fullDateFormat;
50     private final String longDateFormat;
51     private final String mediumDateFormat;
52     private final String shortDateFormat;
53 
SimpleDateFormatData(Locale locale)54     private SimpleDateFormatData(Locale locale) {
55         this.locale = locale;
56         this.usesAsciiSpace = isBug266731719Locale(locale);
57         // libcore's java.text supports Gregorian calendar only.
58         ExtendedCalendar calendar = ICU.getExtendedCalendar(locale, "gregorian");
59 
60         String tmpFullTimeFormat = getDateTimePattern(calendar,
61                 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.FULL);
62 
63         // Fix up a couple of patterns.
64         if (tmpFullTimeFormat != null) {
65             // There are some full time format patterns in ICU that use the pattern character 'v'.
66             // Java doesn't accept this, so we replace it with 'z' which has about the same result
67             // as 'v', the timezone name.
68             // 'v' -> "PT", 'z' -> "PST", v is the generic timezone and z the standard tz
69             // "vvvv" -> "Pacific Time", "zzzz" -> "Pacific Standard Time"
70             tmpFullTimeFormat = tmpFullTimeFormat.replace('v', 'z');
71         }
72         fullTimeFormat = tmpFullTimeFormat;
73 
74         longTimeFormat = getDateTimePattern(calendar,
75                 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.LONG);
76         mediumTimeFormat = getDateTimePattern(calendar,
77                 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.MEDIUM);
78         shortTimeFormat = getDateTimePattern(calendar,
79                 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.SHORT);
80         fullDateFormat = getDateTimePattern(calendar,
81                 android.icu.text.DateFormat.FULL, android.icu.text.DateFormat.NONE);
82         longDateFormat = getDateTimePattern(calendar,
83                 android.icu.text.DateFormat.LONG, android.icu.text.DateFormat.NONE);
84         mediumDateFormat = getDateTimePattern(calendar,
85                 android.icu.text.DateFormat.MEDIUM, android.icu.text.DateFormat.NONE);
86         shortDateFormat = getDateTimePattern(calendar,
87                 android.icu.text.DateFormat.SHORT, android.icu.text.DateFormat.NONE);
88     }
89 
90     /**
91      * Returns an instance.
92      *
93      * @param locale can't be null
94      * @throws NullPointerException if {@code locale} is null
95      * @return a {@link SimpleDateFormatData} instance
96      */
getInstance(Locale locale)97     public static SimpleDateFormatData getInstance(Locale locale) {
98         Objects.requireNonNull(locale, "locale can't be null");
99 
100         locale = LocaleData.getCompatibleLocaleForBug159514442(locale);
101 
102         final String languageTag = locale.toLanguageTag();
103 
104         SimpleDateFormatData data = CACHE.get(languageTag);
105         if (data != null) {
106             return data;
107         }
108 
109         data = new SimpleDateFormatData(locale);
110         SimpleDateFormatData prev = CACHE.putIfAbsent(languageTag, data);
111         if (prev != null) {
112             return prev;
113         }
114         return data;
115     }
116 
117     /**
118      * Ensure that we pull in the locale data for the root locale, en_US, and the user's default
119      * locale. All devices must support the root locale and en_US, and they're used for various
120      * system things. Pre-populating the cache is especially useful on Android because
121      * we'll share this via the Zygote.
122      */
initializeCacheInZygote()123     public static void initializeCacheInZygote() {
124         getInstance(Locale.ROOT);
125         getInstance(Locale.US);
126         getInstance(Locale.getDefault());
127     }
128 
129     /**
130      * @throws AssertionError if style is not one of the 4 styles specified in {@link DateFormat}
131      * @return a date pattern string
132      */
getDateFormat(int style)133     public String getDateFormat(int style) {
134         switch (style) {
135             case DateFormat.SHORT:
136                 return shortDateFormat;
137             case DateFormat.MEDIUM:
138                 return mediumDateFormat;
139             case DateFormat.LONG:
140                 return longDateFormat;
141             case DateFormat.FULL:
142                 return fullDateFormat;
143         }
144         // TODO: fix this legacy behavior of throwing AssertionError introduced in
145         //  the commit 6ca85c4.
146         throw new AssertionError();
147     }
148 
149     /**
150      * @throws AssertionError if style is not one of the 4 styles specified in {@link DateFormat}
151      * @return a time pattern string
152      */
getTimeFormat(int style)153     public String getTimeFormat(int style) {
154         // Do not cache ICU.getTimePattern() return value in the LocaleData instance
155         // because most users do not enable this setting, hurts performance in critical path,
156         // e.g. b/161846393, and ICU.getBestDateTimePattern will cache it in  ICU.CACHED_PATTERNS
157         // on demand.
158         switch (style) {
159             case DateFormat.SHORT:
160                 if (DateFormat.is24Hour == null) {
161                     return shortTimeFormat;
162                 } else {
163                     return getTimePattern(DateFormat.is24Hour, false);
164                 }
165             case DateFormat.MEDIUM:
166                 if (DateFormat.is24Hour == null) {
167                     return mediumTimeFormat;
168                 } else {
169                     return getTimePattern(DateFormat.is24Hour, true);
170                 }
171             case DateFormat.LONG:
172                 // CLDR doesn't really have anything we can use to obey the 12-/24-hour preference.
173                 return longTimeFormat;
174             case DateFormat.FULL:
175                 // CLDR doesn't really have anything we can use to obey the 12-/24-hour preference.
176                 return fullTimeFormat;
177         }
178         // TODO: fix this legacy behavior of throwing AssertionError introduced in
179         //  the commit 6ca85c4.
180         throw new AssertionError();
181     }
182 
183     /**
184      * Returns the date and/or time pattern.
185      *
186      * @param dateStyle {@link android.icu.text.DateFormat} date style
187      * @param timeStyle {@link android.icu.text.DateFormat} time style
188      */
getDateTimePattern(ExtendedCalendar calendar, int dateStyle, int timeStyle)189     private String getDateTimePattern(ExtendedCalendar calendar, int dateStyle, int timeStyle) {
190         String pattern = ICU.transformIcuDateTimePattern_forJavaText(
191                 calendar.getDateTimePattern(dateStyle, timeStyle));
192 
193         return postProcessPattern(pattern);
194     }
195 
getTimePattern(boolean is24Hour, boolean withSecond)196     private String getTimePattern(boolean is24Hour, boolean withSecond) {
197         String pattern = ICU.getTimePattern(locale, is24Hour, withSecond);
198 
199         return postProcessPattern(pattern);
200     }
201 
postProcessPattern(String pattern)202     private String postProcessPattern(String pattern) {
203         if (pattern == null || !usesAsciiSpace) {
204             return pattern;
205         }
206 
207         return pattern.replace('\u202f', ' ');
208     }
209 
210     /**
211      * Returns {@code true} if the locale is "en" or "en-US" or "en-US-*" or
212      * "en-<unknown_region>-*"
213      *
214      * The first 2 locales, i.e. {@link Locale#ENGLISH} and {@link Locale#US}, are commonly
215      * hard-coded by developers for serialization and deserialization as shown in
216      * the bug http://b/266731719. The date time formats in these locales are basically frozen
217      * because they should be both backward and forward-compatible.
218      *
219      * We change both formatting and parsing behavior because the serialized string could be
220      * parsed via a network request and thus breaking Android apps. We could consider changing
221      * the formatted date / time, but the parser has to be compatible with both the old and new
222      * formatted string.
223      *
224      * This method returns true for "en-US-*" and "en-<unknown_region>-*" because the behavior
225      * should be consistent with the locale "en-US" and "en". The other English locales are not
226      * expected to be stable in this bug.
227      */
isBug266731719Locale(Locale locale)228     private static boolean isBug266731719Locale(Locale locale) {
229         if (locale == null) {
230             return false;
231         }
232 
233         String language = locale.getLanguage();
234         if (language == null) {
235             return false;
236         }
237         // Use LanguageTag.canonicalizeLanguage(s) instead of String.toUpperCase(s) to avoid
238         // non-ASCII character conversion.
239         language = LanguageTag.canonicalizeLanguage(language);
240         if (!("en".equals(language))) {
241             return false;
242         }
243 
244         String region = locale.getCountry();
245         if (region == null || region.isEmpty()) {
246             return true;
247         }
248         region = LanguageTag.canonicalizeRegion(region);
249         return "US".equals(region);
250     }
251 }
252