1 /*
2 * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 /*
27 * This file is available under and governed by the GNU General Public
28 * License version 2 only, as published by the Free Software Foundation.
29 * However, the following notice accompanied the original version of this
30 * file:
31 *
32 * Copyright (c) 2011-2012, Stephen Colebourne & Michael Nascimento Santos
33 *
34 * All rights reserved.
35 *
36 * Redistribution and use in source and binary forms, with or without
37 * modification, are permitted provided that the following conditions are met:
38 *
39 * * Redistributions of source code must retain the above copyright notice,
40 * this list of conditions and the following disclaimer.
41 *
42 * * Redistributions in binary form must reproduce the above copyright notice,
43 * this list of conditions and the following disclaimer in the documentation
44 * and/or other materials provided with the distribution.
45 *
46 * * Neither the name of JSR-310 nor the names of its contributors
47 * may be used to endorse or promote products derived from this software
48 * without specific prior written permission.
49 *
50 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
51 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
52 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
53 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
54 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
55 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
56 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
57 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
58 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
59 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
60 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
61 */
62 package java.time.format;
63
64 import static java.time.temporal.ChronoField.AMPM_OF_DAY;
65 import static java.time.temporal.ChronoField.DAY_OF_WEEK;
66 import static java.time.temporal.ChronoField.ERA;
67 import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
68
69 import java.time.chrono.Chronology;
70 import java.time.chrono.IsoChronology;
71 import java.time.chrono.JapaneseChronology;
72 import java.time.temporal.ChronoField;
73 import java.time.temporal.IsoFields;
74 import java.time.temporal.TemporalField;
75 import java.util.AbstractMap.SimpleImmutableEntry;
76 import java.util.ArrayList;
77 import java.util.Calendar;
78 import java.util.Collections;
79 import java.util.Comparator;
80 import java.util.HashMap;
81 import java.util.Iterator;
82 import java.util.List;
83 import java.util.Locale;
84 import java.util.Map;
85 import java.util.Map.Entry;
86 import java.util.ResourceBundle;
87 import java.util.concurrent.ConcurrentHashMap;
88 import java.util.concurrent.ConcurrentMap;
89
90 import sun.util.locale.provider.CalendarDataUtility;
91 import sun.util.locale.provider.LocaleProviderAdapter;
92 import sun.util.locale.provider.LocaleResources;
93
94 /**
95 * A provider to obtain the textual form of a date-time field.
96 *
97 * @implSpec
98 * Implementations must be thread-safe.
99 * Implementations should cache the textual information.
100 *
101 * @since 1.8
102 */
103 class DateTimeTextProvider {
104
105 /** Cache. */
106 private static final ConcurrentMap<Entry<TemporalField, Locale>, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2);
107 /** Comparator. */
108 private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() {
109 @Override
110 public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) {
111 return obj2.getKey().length() - obj1.getKey().length(); // longest to shortest
112 }
113 };
114
115 // Singleton instance
116 private static final DateTimeTextProvider INSTANCE = new DateTimeTextProvider();
117
118 DateTimeTextProvider() {}
119
120 /**
121 * Gets the provider of text.
122 *
123 * @return the provider, not null
124 */
125 static DateTimeTextProvider getInstance() {
126 return INSTANCE;
127 }
128
129 /**
130 * Gets the text for the specified field, locale and style
131 * for the purpose of formatting.
132 * <p>
133 * The text associated with the value is returned.
134 * The null return value should be used if there is no applicable text, or
135 * if the text would be a numeric representation of the value.
136 *
137 * @param field the field to get text for, not null
138 * @param value the field value to get text for, not null
139 * @param style the style to get text for, not null
140 * @param locale the locale to get text for, not null
141 * @return the text for the field value, null if no text found
142 */
143 public String getText(TemporalField field, long value, TextStyle style, Locale locale) {
144 Object store = findStore(field, locale);
145 if (store instanceof LocaleStore) {
146 return ((LocaleStore) store).getText(value, style);
147 }
148 return null;
149 }
150
151 /**
152 * Gets the text for the specified chrono, field, locale and style
153 * for the purpose of formatting.
154 * <p>
155 * The text associated with the value is returned.
156 * The null return value should be used if there is no applicable text, or
157 * if the text would be a numeric representation of the value.
158 *
159 * @param chrono the Chronology to get text for, not null
160 * @param field the field to get text for, not null
161 * @param value the field value to get text for, not null
162 * @param style the style to get text for, not null
163 * @param locale the locale to get text for, not null
164 * @return the text for the field value, null if no text found
165 */
166 public String getText(Chronology chrono, TemporalField field, long value,
167 TextStyle style, Locale locale) {
168 if (chrono == IsoChronology.INSTANCE
169 || !(field instanceof ChronoField)) {
170 return getText(field, value, style, locale);
171 }
172
173 int fieldIndex;
174 int fieldValue;
175 if (field == ERA) {
176 fieldIndex = Calendar.ERA;
177 if (chrono == JapaneseChronology.INSTANCE) {
178 if (value == -999) {
179 fieldValue = 0;
180 } else {
181 fieldValue = (int) value + 2;
182 }
183 } else {
184 fieldValue = (int) value;
185 }
186 } else if (field == MONTH_OF_YEAR) {
187 fieldIndex = Calendar.MONTH;
188 fieldValue = (int) value - 1;
189 } else if (field == DAY_OF_WEEK) {
190 fieldIndex = Calendar.DAY_OF_WEEK;
191 fieldValue = (int) value + 1;
192 if (fieldValue > 7) {
193 fieldValue = Calendar.SUNDAY;
194 }
195 } else if (field == AMPM_OF_DAY) {
196 fieldIndex = Calendar.AM_PM;
197 fieldValue = (int) value;
198 } else {
199 return null;
200 }
201 return CalendarDataUtility.retrieveJavaTimeFieldValueName(
202 chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale);
203 }
204
205 /**
206 * Gets an iterator of text to field for the specified field, locale and style
207 * for the purpose of parsing.
208 * <p>
209 * The iterator must be returned in order from the longest text to the shortest.
210 * <p>
211 * The null return value should be used if there is no applicable parsable text, or
212 * if the text would be a numeric representation of the value.
213 * Text can only be parsed if all the values for that field-style-locale combination are unique.
214 *
215 * @param field the field to get text for, not null
216 * @param style the style to get text for, null for all parsable text
217 * @param locale the locale to get text for, not null
218 * @return the iterator of text to field pairs, in order from longest text to shortest text,
219 * null if the field or style is not parsable
220 */
221 public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) {
222 Object store = findStore(field, locale);
223 if (store instanceof LocaleStore) {
224 return ((LocaleStore) store).getTextIterator(style);
225 }
226 return null;
227 }
228
229 /**
230 * Gets an iterator of text to field for the specified chrono, field, locale and style
231 * for the purpose of parsing.
232 * <p>
233 * The iterator must be returned in order from the longest text to the shortest.
234 * <p>
235 * The null return value should be used if there is no applicable parsable text, or
236 * if the text would be a numeric representation of the value.
237 * Text can only be parsed if all the values for that field-style-locale combination are unique.
238 *
239 * @param chrono the Chronology to get text for, not null
240 * @param field the field to get text for, not null
241 * @param style the style to get text for, null for all parsable text
242 * @param locale the locale to get text for, not null
243 * @return the iterator of text to field pairs, in order from longest text to shortest text,
244 * null if the field or style is not parsable
245 */
246 public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, TemporalField field,
247 TextStyle style, Locale locale) {
248 if (chrono == IsoChronology.INSTANCE
249 || !(field instanceof ChronoField)) {
250 return getTextIterator(field, style, locale);
251 }
252
253 int fieldIndex;
254 switch ((ChronoField)field) {
255 case ERA:
256 fieldIndex = Calendar.ERA;
257 break;
258 case MONTH_OF_YEAR:
259 fieldIndex = Calendar.MONTH;
260 break;
261 case DAY_OF_WEEK:
262 fieldIndex = Calendar.DAY_OF_WEEK;
263 break;
264 case AMPM_OF_DAY:
265 fieldIndex = Calendar.AM_PM;
266 break;
267 default:
268 return null;
269 }
270
271 int calendarStyle = (style == null) ? Calendar.ALL_STYLES : style.toCalendarStyle();
272 Map<String, Integer> map = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
273 chrono.getCalendarType(), fieldIndex, calendarStyle, locale);
274 if (map == null) {
275 return null;
276 }
277 List<Entry<String, Long>> list = new ArrayList<>(map.size());
278 switch (fieldIndex) {
279 case Calendar.ERA:
280 for (Map.Entry<String, Integer> entry : map.entrySet()) {
281 int era = entry.getValue();
282 if (chrono == JapaneseChronology.INSTANCE) {
283 if (era == 0) {
284 era = -999;
285 } else {
286 era -= 2;
287 }
288 }
289 list.add(createEntry(entry.getKey(), (long)era));
290 }
291 break;
292 case Calendar.MONTH:
293 for (Map.Entry<String, Integer> entry : map.entrySet()) {
294 list.add(createEntry(entry.getKey(), (long)(entry.getValue() + 1)));
295 }
296 break;
297 case Calendar.DAY_OF_WEEK:
298 for (Map.Entry<String, Integer> entry : map.entrySet()) {
299 list.add(createEntry(entry.getKey(), (long)toWeekDay(entry.getValue())));
300 }
301 break;
302 default:
303 for (Map.Entry<String, Integer> entry : map.entrySet()) {
304 list.add(createEntry(entry.getKey(), (long)entry.getValue()));
305 }
306 break;
307 }
308 return list.iterator();
309 }
310
311 private Object findStore(TemporalField field, Locale locale) {
312 Entry<TemporalField, Locale> key = createEntry(field, locale);
313 Object store = CACHE.get(key);
314 if (store == null) {
315 store = createStore(field, locale);
316 CACHE.putIfAbsent(key, store);
317 store = CACHE.get(key);
318 }
319 return store;
320 }
321
322 private static int toWeekDay(int calWeekDay) {
323 if (calWeekDay == Calendar.SUNDAY) {
324 return 7;
325 } else {
326 return calWeekDay - 1;
327 }
328 }
329
330 private Object createStore(TemporalField field, Locale locale) {
331 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>();
332 if (field == ERA) {
333 for (TextStyle textStyle : TextStyle.values()) {
334 if (textStyle.isStandalone()) {
335 // Stand-alone isn't applicable to era names.
336 continue;
337 }
338 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
339 "gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale);
340 if (displayNames != null) {
341 Map<Long, String> map = new HashMap<>();
342 for (Entry<String, Integer> entry : displayNames.entrySet()) {
343 map.put((long) entry.getValue(), entry.getKey());
344 }
345 if (!map.isEmpty()) {
346 styleMap.put(textStyle, map);
347 }
348 }
349 }
350 return new LocaleStore(styleMap);
351 }
352
353 if (field == MONTH_OF_YEAR) {
354 for (TextStyle textStyle : TextStyle.values()) {
355 Map<Long, String> map = new HashMap<>();
356 // Narrow names may have duplicated names, such as "J" for January, June, July.
357 // Get names one by one in that case.
358 if ((textStyle.equals(TextStyle.NARROW) ||
359 textStyle.equals(TextStyle.NARROW_STANDALONE))) {
360 for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) {
361 String name;
362 name = CalendarDataUtility.retrieveJavaTimeFieldValueName(
363 "gregory", Calendar.MONTH,
364 month, textStyle.toCalendarStyle(), locale);
365 if (name == null) {
366 break;
367 }
368 map.put((month + 1L), name);
369 }
370 } else {
371 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
372 "gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale);
373 if (displayNames != null) {
374 for (Entry<String, Integer> entry : displayNames.entrySet()) {
375 map.put((long)(entry.getValue() + 1), entry.getKey());
376 }
377 } else {
378 // Although probability is very less, but if other styles have duplicate names.
379 // Get names one by one in that case.
380 for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) {
381 String name;
382 name = CalendarDataUtility.retrieveJavaTimeFieldValueName(
383 "gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale);
384 if (name == null) {
385 break;
386 }
387 map.put((month + 1L), name);
388 }
389 }
390 }
391 if (!map.isEmpty()) {
392 styleMap.put(textStyle, map);
393 }
394 }
395 return new LocaleStore(styleMap);
396 }
397
398 if (field == DAY_OF_WEEK) {
399 for (TextStyle textStyle : TextStyle.values()) {
400 Map<Long, String> map = new HashMap<>();
401 // Narrow names may have duplicated names, such as "S" for Sunday and Saturday.
402 // Get names one by one in that case.
403 if ((textStyle.equals(TextStyle.NARROW) ||
404 textStyle.equals(TextStyle.NARROW_STANDALONE))) {
405 for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) {
406 String name;
407 name = CalendarDataUtility.retrieveJavaTimeFieldValueName(
408 "gregory", Calendar.DAY_OF_WEEK,
409 wday, textStyle.toCalendarStyle(), locale);
410 if (name == null) {
411 break;
412 }
413 map.put((long)toWeekDay(wday), name);
414 }
415 } else {
416 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
417 "gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale);
418 if (displayNames != null) {
419 for (Entry<String, Integer> entry : displayNames.entrySet()) {
420 map.put((long)toWeekDay(entry.getValue()), entry.getKey());
421 }
422 } else {
423 // Although probability is very less, but if other styles have duplicate names.
424 // Get names one by one in that case.
425 for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) {
426 String name;
427 name = CalendarDataUtility.retrieveJavaTimeFieldValueName(
428 "gregory", Calendar.DAY_OF_WEEK, wday, textStyle.toCalendarStyle(), locale);
429 if (name == null) {
430 break;
431 }
432 map.put((long)toWeekDay(wday), name);
433 }
434 }
435 }
436 if (!map.isEmpty()) {
437 styleMap.put(textStyle, map);
438 }
439 }
440 return new LocaleStore(styleMap);
441 }
442
443 if (field == AMPM_OF_DAY) {
444 for (TextStyle textStyle : TextStyle.values()) {
445 if (textStyle.isStandalone()) {
446 // Stand-alone isn't applicable to AM/PM.
447 continue;
448 }
449 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(
450 "gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale);
451 if (displayNames != null) {
452 Map<Long, String> map = new HashMap<>();
453 for (Entry<String, Integer> entry : displayNames.entrySet()) {
454 map.put((long) entry.getValue(), entry.getKey());
455 }
456 if (!map.isEmpty()) {
457 styleMap.put(textStyle, map);
458 }
459 }
460 }
461 return new LocaleStore(styleMap);
462 }
463
464 if (field == IsoFields.QUARTER_OF_YEAR) {
465 // The order of keys must correspond to the TextStyle.values() order.
466 final String[] keys = {
467 "QuarterNames",
468 "standalone.QuarterNames",
469 "QuarterAbbreviations",
470 "standalone.QuarterAbbreviations",
471 "QuarterNarrows",
472 "standalone.QuarterNarrows",
473 };
474 for (int i = 0; i < keys.length; i++) {
475 String[] names = getLocalizedResource(keys[i], locale);
476 if (names != null) {
477 Map<Long, String> map = new HashMap<>();
478 for (int q = 0; q < names.length; q++) {
479 map.put((long) (q + 1), names[q]);
480 }
481 styleMap.put(TextStyle.values()[i], map);
482 }
483 }
484 return new LocaleStore(styleMap);
485 }
486
487 return ""; // null marker for map
488 }
489
490 /**
491 * Helper method to create an immutable entry.
492 *
493 * @param text the text, not null
494 * @param field the field, not null
495 * @return the entry, not null
496 */
497 private static <A, B> Entry<A, B> createEntry(A text, B field) {
498 return new SimpleImmutableEntry<>(text, field);
499 }
500
501 /**
502 * Returns the localized resource of the given key and locale, or null
503 * if no localized resource is available.
504 *
505 * @param key the key of the localized resource, not null
506 * @param locale the locale, not null
507 * @return the localized resource, or null if not available
508 * @throws NullPointerException if key or locale is null
509 */
510 @SuppressWarnings("unchecked")
511 static <T> T getLocalizedResource(String key, Locale locale) {
512 LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased()
513 .getLocaleResources(
514 CalendarDataUtility.findRegionOverride(locale));
515 ResourceBundle rb = lr.getJavaTimeFormatData();
516 return rb.containsKey(key) ? (T) rb.getObject(key) : null;
517 }
518
519 /**
520 * Stores the text for a single locale.
521 * <p>
522 * Some fields have a textual representation, such as day-of-week or month-of-year.
523 * These textual representations can be captured in this class for printing
524 * and parsing.
525 * <p>
526 * This class is immutable and thread-safe.
527 */
528 static final class LocaleStore {
529 /**
530 * Map of value to text.
531 */
532 private final Map<TextStyle, Map<Long, String>> valueTextMap;
533 /**
534 * Parsable data.
535 */
536 private final Map<TextStyle, List<Entry<String, Long>>> parsable;
537
538 /**
539 * Constructor.
540 *
541 * @param valueTextMap the map of values to text to store, assigned and not altered, not null
542 */
543 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) {
544 this.valueTextMap = valueTextMap;
545 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>();
546 List<Entry<String, Long>> allList = new ArrayList<>();
547 for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) {
548 Map<String, Entry<String, Long>> reverse = new HashMap<>();
549 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) {
550 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) {
551 // TODO: BUG: this has no effect
552 continue; // not parsable, try next style
553 }
554 }
555 List<Entry<String, Long>> list = new ArrayList<>(reverse.values());
556 Collections.sort(list, COMPARATOR);
557 map.put(vtmEntry.getKey(), list);
558 allList.addAll(list);
559 map.put(null, allList);
560 }
561 Collections.sort(allList, COMPARATOR);
562 this.parsable = map;
563 }
564
565 /**
566 * Gets the text for the specified field value, locale and style
567 * for the purpose of printing.
568 *
569 * @param value the value to get text for, not null
570 * @param style the style to get text for, not null
571 * @return the text for the field value, null if no text found
572 */
573 String getText(long value, TextStyle style) {
574 Map<Long, String> map = valueTextMap.get(style);
575 return map != null ? map.get(value) : null;
576 }
577
578 /**
579 * Gets an iterator of text to field for the specified style for the purpose of parsing.
580 * <p>
581 * The iterator must be returned in order from the longest text to the shortest.
582 *
583 * @param style the style to get text for, null for all parsable text
584 * @return the iterator of text to field pairs, in order from longest text to shortest text,
585 * null if the style is not parsable
586 */
587 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) {
588 List<Entry<String, Long>> list = parsable.get(style);
589 return list != null ? list.iterator() : null;
590 }
591 }
592 }
593