Path: blob/master/src/java.base/share/classes/java/time/format/DateTimeTextProvider.java
41159 views
/*1* Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425/*26* This file is available under and governed by the GNU General Public27* License version 2 only, as published by the Free Software Foundation.28* However, the following notice accompanied the original version of this29* file:30*31* Copyright (c) 2011-2012, Stephen Colebourne & Michael Nascimento Santos32*33* All rights reserved.34*35* Redistribution and use in source and binary forms, with or without36* modification, are permitted provided that the following conditions are met:37*38* * Redistributions of source code must retain the above copyright notice,39* this list of conditions and the following disclaimer.40*41* * Redistributions in binary form must reproduce the above copyright notice,42* this list of conditions and the following disclaimer in the documentation43* and/or other materials provided with the distribution.44*45* * Neither the name of JSR-310 nor the names of its contributors46* may be used to endorse or promote products derived from this software47* without specific prior written permission.48*49* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS50* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT51* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR52* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR53* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,54* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,55* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR56* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF57* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING58* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS59* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.60*/61package java.time.format;6263import static java.time.temporal.ChronoField.AMPM_OF_DAY;64import static java.time.temporal.ChronoField.DAY_OF_WEEK;65import static java.time.temporal.ChronoField.ERA;66import static java.time.temporal.ChronoField.MONTH_OF_YEAR;6768import java.time.chrono.Chronology;69import java.time.chrono.IsoChronology;70import java.time.chrono.JapaneseChronology;71import java.time.temporal.ChronoField;72import java.time.temporal.IsoFields;73import java.time.temporal.TemporalField;74import java.util.AbstractMap.SimpleImmutableEntry;75import java.util.ArrayList;76import java.util.Calendar;77import java.util.Collections;78import java.util.Comparator;79import java.util.HashMap;80import java.util.Iterator;81import java.util.List;82import java.util.Locale;83import java.util.Map;84import java.util.Map.Entry;85import java.util.ResourceBundle;86import java.util.concurrent.ConcurrentHashMap;87import java.util.concurrent.ConcurrentMap;8889import sun.util.locale.provider.CalendarDataUtility;90import sun.util.locale.provider.LocaleProviderAdapter;91import sun.util.locale.provider.LocaleResources;9293/**94* A provider to obtain the textual form of a date-time field.95*96* @implSpec97* Implementations must be thread-safe.98* Implementations should cache the textual information.99*100* @since 1.8101*/102class DateTimeTextProvider {103104/** Cache. */105private static final ConcurrentMap<Entry<TemporalField, Locale>, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2);106/** Comparator. */107private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() {108@Override109public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) {110return obj2.getKey().length() - obj1.getKey().length(); // longest to shortest111}112};113114// Singleton instance115private static final DateTimeTextProvider INSTANCE = new DateTimeTextProvider();116117DateTimeTextProvider() {}118119/**120* Gets the provider of text.121*122* @return the provider, not null123*/124static DateTimeTextProvider getInstance() {125return INSTANCE;126}127128/**129* Gets the text for the specified field, locale and style130* for the purpose of formatting.131* <p>132* The text associated with the value is returned.133* The null return value should be used if there is no applicable text, or134* if the text would be a numeric representation of the value.135*136* @param field the field to get text for, not null137* @param value the field value to get text for, not null138* @param style the style to get text for, not null139* @param locale the locale to get text for, not null140* @return the text for the field value, null if no text found141*/142public String getText(TemporalField field, long value, TextStyle style, Locale locale) {143Object store = findStore(field, locale);144if (store instanceof LocaleStore) {145return ((LocaleStore) store).getText(value, style);146}147return null;148}149150/**151* Gets the text for the specified chrono, field, locale and style152* for the purpose of formatting.153* <p>154* The text associated with the value is returned.155* The null return value should be used if there is no applicable text, or156* if the text would be a numeric representation of the value.157*158* @param chrono the Chronology to get text for, not null159* @param field the field to get text for, not null160* @param value the field value to get text for, not null161* @param style the style to get text for, not null162* @param locale the locale to get text for, not null163* @return the text for the field value, null if no text found164*/165public String getText(Chronology chrono, TemporalField field, long value,166TextStyle style, Locale locale) {167if (chrono == IsoChronology.INSTANCE168|| !(field instanceof ChronoField)) {169return getText(field, value, style, locale);170}171172int fieldIndex;173int fieldValue;174if (field == ERA) {175fieldIndex = Calendar.ERA;176if (chrono == JapaneseChronology.INSTANCE) {177if (value == -999) {178fieldValue = 0;179} else {180fieldValue = (int) value + 2;181}182} else {183fieldValue = (int) value;184}185} else if (field == MONTH_OF_YEAR) {186fieldIndex = Calendar.MONTH;187fieldValue = (int) value - 1;188} else if (field == DAY_OF_WEEK) {189fieldIndex = Calendar.DAY_OF_WEEK;190fieldValue = (int) value + 1;191if (fieldValue > 7) {192fieldValue = Calendar.SUNDAY;193}194} else if (field == AMPM_OF_DAY) {195fieldIndex = Calendar.AM_PM;196fieldValue = (int) value;197} else {198return null;199}200return CalendarDataUtility.retrieveJavaTimeFieldValueName(201chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale);202}203204/**205* Gets an iterator of text to field for the specified field, locale and style206* for the purpose of parsing.207* <p>208* The iterator must be returned in order from the longest text to the shortest.209* <p>210* The null return value should be used if there is no applicable parsable text, or211* if the text would be a numeric representation of the value.212* Text can only be parsed if all the values for that field-style-locale combination are unique.213*214* @param field the field to get text for, not null215* @param style the style to get text for, null for all parsable text216* @param locale the locale to get text for, not null217* @return the iterator of text to field pairs, in order from longest text to shortest text,218* null if the field or style is not parsable219*/220public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) {221Object store = findStore(field, locale);222if (store instanceof LocaleStore) {223return ((LocaleStore) store).getTextIterator(style);224}225return null;226}227228/**229* Gets an iterator of text to field for the specified chrono, field, locale and style230* for the purpose of parsing.231* <p>232* The iterator must be returned in order from the longest text to the shortest.233* <p>234* The null return value should be used if there is no applicable parsable text, or235* if the text would be a numeric representation of the value.236* Text can only be parsed if all the values for that field-style-locale combination are unique.237*238* @param chrono the Chronology to get text for, not null239* @param field the field to get text for, not null240* @param style the style to get text for, null for all parsable text241* @param locale the locale to get text for, not null242* @return the iterator of text to field pairs, in order from longest text to shortest text,243* null if the field or style is not parsable244*/245public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, TemporalField field,246TextStyle style, Locale locale) {247if (chrono == IsoChronology.INSTANCE248|| !(field instanceof ChronoField)) {249return getTextIterator(field, style, locale);250}251252int fieldIndex;253switch ((ChronoField)field) {254case ERA:255fieldIndex = Calendar.ERA;256break;257case MONTH_OF_YEAR:258fieldIndex = Calendar.MONTH;259break;260case DAY_OF_WEEK:261fieldIndex = Calendar.DAY_OF_WEEK;262break;263case AMPM_OF_DAY:264fieldIndex = Calendar.AM_PM;265break;266default:267return null;268}269270int calendarStyle = (style == null) ? Calendar.ALL_STYLES : style.toCalendarStyle();271Map<String, Integer> map = CalendarDataUtility.retrieveJavaTimeFieldValueNames(272chrono.getCalendarType(), fieldIndex, calendarStyle, locale);273if (map == null) {274return null;275}276List<Entry<String, Long>> list = new ArrayList<>(map.size());277switch (fieldIndex) {278case Calendar.ERA:279for (Map.Entry<String, Integer> entry : map.entrySet()) {280int era = entry.getValue();281if (chrono == JapaneseChronology.INSTANCE) {282if (era == 0) {283era = -999;284} else {285era -= 2;286}287}288list.add(createEntry(entry.getKey(), (long)era));289}290break;291case Calendar.MONTH:292for (Map.Entry<String, Integer> entry : map.entrySet()) {293list.add(createEntry(entry.getKey(), (long)(entry.getValue() + 1)));294}295break;296case Calendar.DAY_OF_WEEK:297for (Map.Entry<String, Integer> entry : map.entrySet()) {298list.add(createEntry(entry.getKey(), (long)toWeekDay(entry.getValue())));299}300break;301default:302for (Map.Entry<String, Integer> entry : map.entrySet()) {303list.add(createEntry(entry.getKey(), (long)entry.getValue()));304}305break;306}307return list.iterator();308}309310private Object findStore(TemporalField field, Locale locale) {311Entry<TemporalField, Locale> key = createEntry(field, locale);312Object store = CACHE.get(key);313if (store == null) {314store = createStore(field, locale);315CACHE.putIfAbsent(key, store);316store = CACHE.get(key);317}318return store;319}320321private static int toWeekDay(int calWeekDay) {322if (calWeekDay == Calendar.SUNDAY) {323return 7;324} else {325return calWeekDay - 1;326}327}328329private Object createStore(TemporalField field, Locale locale) {330Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>();331if (field == ERA) {332for (TextStyle textStyle : TextStyle.values()) {333if (textStyle.isStandalone()) {334// Stand-alone isn't applicable to era names.335continue;336}337Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(338"gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale);339if (displayNames != null) {340Map<Long, String> map = new HashMap<>();341for (Entry<String, Integer> entry : displayNames.entrySet()) {342map.put((long) entry.getValue(), entry.getKey());343}344if (!map.isEmpty()) {345styleMap.put(textStyle, map);346}347}348}349return new LocaleStore(styleMap);350}351352if (field == MONTH_OF_YEAR) {353for (TextStyle textStyle : TextStyle.values()) {354Map<Long, String> map = new HashMap<>();355// Narrow names may have duplicated names, such as "J" for January, June, July.356// Get names one by one in that case.357if ((textStyle.equals(TextStyle.NARROW) ||358textStyle.equals(TextStyle.NARROW_STANDALONE))) {359for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) {360String name;361name = CalendarDataUtility.retrieveJavaTimeFieldValueName(362"gregory", Calendar.MONTH,363month, textStyle.toCalendarStyle(), locale);364if (name == null) {365break;366}367map.put((month + 1L), name);368}369} else {370Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(371"gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale);372if (displayNames != null) {373for (Entry<String, Integer> entry : displayNames.entrySet()) {374map.put((long)(entry.getValue() + 1), entry.getKey());375}376} else {377// Although probability is very less, but if other styles have duplicate names.378// Get names one by one in that case.379for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) {380String name;381name = CalendarDataUtility.retrieveJavaTimeFieldValueName(382"gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale);383if (name == null) {384break;385}386map.put((month + 1L), name);387}388}389}390if (!map.isEmpty()) {391styleMap.put(textStyle, map);392}393}394return new LocaleStore(styleMap);395}396397if (field == DAY_OF_WEEK) {398for (TextStyle textStyle : TextStyle.values()) {399Map<Long, String> map = new HashMap<>();400// Narrow names may have duplicated names, such as "S" for Sunday and Saturday.401// Get names one by one in that case.402if ((textStyle.equals(TextStyle.NARROW) ||403textStyle.equals(TextStyle.NARROW_STANDALONE))) {404for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) {405String name;406name = CalendarDataUtility.retrieveJavaTimeFieldValueName(407"gregory", Calendar.DAY_OF_WEEK,408wday, textStyle.toCalendarStyle(), locale);409if (name == null) {410break;411}412map.put((long)toWeekDay(wday), name);413}414} else {415Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(416"gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale);417if (displayNames != null) {418for (Entry<String, Integer> entry : displayNames.entrySet()) {419map.put((long)toWeekDay(entry.getValue()), entry.getKey());420}421} else {422// Although probability is very less, but if other styles have duplicate names.423// Get names one by one in that case.424for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) {425String name;426name = CalendarDataUtility.retrieveJavaTimeFieldValueName(427"gregory", Calendar.DAY_OF_WEEK, wday, textStyle.toCalendarStyle(), locale);428if (name == null) {429break;430}431map.put((long)toWeekDay(wday), name);432}433}434}435if (!map.isEmpty()) {436styleMap.put(textStyle, map);437}438}439return new LocaleStore(styleMap);440}441442if (field == AMPM_OF_DAY) {443for (TextStyle textStyle : TextStyle.values()) {444if (textStyle.isStandalone()) {445// Stand-alone isn't applicable to AM/PM.446continue;447}448Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames(449"gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale);450if (displayNames != null) {451Map<Long, String> map = new HashMap<>();452for (Entry<String, Integer> entry : displayNames.entrySet()) {453map.put((long) entry.getValue(), entry.getKey());454}455if (!map.isEmpty()) {456styleMap.put(textStyle, map);457}458}459}460return new LocaleStore(styleMap);461}462463if (field == IsoFields.QUARTER_OF_YEAR) {464// The order of keys must correspond to the TextStyle.values() order.465final String[] keys = {466"QuarterNames",467"standalone.QuarterNames",468"QuarterAbbreviations",469"standalone.QuarterAbbreviations",470"QuarterNarrows",471"standalone.QuarterNarrows",472};473for (int i = 0; i < keys.length; i++) {474String[] names = getLocalizedResource(keys[i], locale);475if (names != null) {476Map<Long, String> map = new HashMap<>();477for (int q = 0; q < names.length; q++) {478map.put((long) (q + 1), names[q]);479}480styleMap.put(TextStyle.values()[i], map);481}482}483return new LocaleStore(styleMap);484}485486return ""; // null marker for map487}488489/**490* Helper method to create an immutable entry.491*492* @param text the text, not null493* @param field the field, not null494* @return the entry, not null495*/496private static <A, B> Entry<A, B> createEntry(A text, B field) {497return new SimpleImmutableEntry<>(text, field);498}499500/**501* Returns the localized resource of the given key and locale, or null502* if no localized resource is available.503*504* @param key the key of the localized resource, not null505* @param locale the locale, not null506* @return the localized resource, or null if not available507* @throws NullPointerException if key or locale is null508*/509@SuppressWarnings("unchecked")510static <T> T getLocalizedResource(String key, Locale locale) {511LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased()512.getLocaleResources(513CalendarDataUtility.findRegionOverride(locale));514ResourceBundle rb = lr.getJavaTimeFormatData();515return rb.containsKey(key) ? (T) rb.getObject(key) : null;516}517518/**519* Stores the text for a single locale.520* <p>521* Some fields have a textual representation, such as day-of-week or month-of-year.522* These textual representations can be captured in this class for printing523* and parsing.524* <p>525* This class is immutable and thread-safe.526*/527static final class LocaleStore {528/**529* Map of value to text.530*/531private final Map<TextStyle, Map<Long, String>> valueTextMap;532/**533* Parsable data.534*/535private final Map<TextStyle, List<Entry<String, Long>>> parsable;536537/**538* Constructor.539*540* @param valueTextMap the map of values to text to store, assigned and not altered, not null541*/542LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) {543this.valueTextMap = valueTextMap;544Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>();545List<Entry<String, Long>> allList = new ArrayList<>();546for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) {547Map<String, Entry<String, Long>> reverse = new HashMap<>();548for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) {549if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) {550// TODO: BUG: this has no effect551continue; // not parsable, try next style552}553}554List<Entry<String, Long>> list = new ArrayList<>(reverse.values());555Collections.sort(list, COMPARATOR);556map.put(vtmEntry.getKey(), list);557allList.addAll(list);558map.put(null, allList);559}560Collections.sort(allList, COMPARATOR);561this.parsable = map;562}563564/**565* Gets the text for the specified field value, locale and style566* for the purpose of printing.567*568* @param value the value to get text for, not null569* @param style the style to get text for, not null570* @return the text for the field value, null if no text found571*/572String getText(long value, TextStyle style) {573Map<Long, String> map = valueTextMap.get(style);574return map != null ? map.get(value) : null;575}576577/**578* Gets an iterator of text to field for the specified style for the purpose of parsing.579* <p>580* The iterator must be returned in order from the longest text to the shortest.581*582* @param style the style to get text for, null for all parsable text583* @return the iterator of text to field pairs, in order from longest text to shortest text,584* null if the style is not parsable585*/586Iterator<Entry<String, Long>> getTextIterator(TextStyle style) {587List<Entry<String, Long>> list = parsable.get(style);588return list != null ? list.iterator() : null;589}590}591}592593594