jdk/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java
Magnus Ihse Bursie fb469fb894 8284893: Fix typos in java.base
Reviewed-by: iris, wetmore, lancea, mullan, naoto
2022-04-19 20:07:57 +00:00

847 lines
32 KiB
Java

/*
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved
* (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved
*
* The original version of this source code and documentation
* is copyrighted and owned by Taligent, Inc., a wholly-owned
* subsidiary of IBM. These materials are provided under terms
* of a License Agreement between Taligent and Sun. This technology
* is protected by multiple US and International patents.
*
* This notice and attribution to Taligent may not be removed.
* Taligent is a registered trademark of Taligent, Inc.
*
*/
package sun.util.locale.provider;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import sun.security.action.GetPropertyAction;
import sun.util.resources.LocaleData;
import sun.util.resources.OpenListResourceBundle;
import sun.util.resources.ParallelListResourceBundle;
import sun.util.resources.TimeZoneNamesBundle;
/**
* Central accessor to locale-dependent resources for JRE/CLDR provider adapters.
*
* @author Masayoshi Okutsu
* @author Naoto Sato
*/
public class LocaleResources {
private final Locale locale;
private final LocaleData localeData;
private final LocaleProviderAdapter.Type type;
// Resource cache
private final ConcurrentMap<String, ResourceReference> cache = new ConcurrentHashMap<>();
private final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// cache key prefixes
private static final String BREAK_ITERATOR_INFO = "BII.";
private static final String CALENDAR_DATA = "CALD.";
private static final String COLLATION_DATA_CACHEKEY = "COLD";
private static final String DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY = "DFSD";
private static final String CURRENCY_NAMES = "CN.";
private static final String LOCALE_NAMES = "LN.";
private static final String TIME_ZONE_NAMES = "TZN.";
private static final String ZONE_IDS_CACHEKEY = "ZID";
private static final String CALENDAR_NAMES = "CALN.";
private static final String NUMBER_PATTERNS_CACHEKEY = "NP";
private static final String COMPACT_NUMBER_PATTERNS_CACHEKEY = "CNP";
private static final String DATE_TIME_PATTERN = "DTP.";
private static final String RULES_CACHEKEY = "RULE";
private static final String SKELETON_PATTERN = "SP.";
// ResourceBundle key names for skeletons
private static final String SKELETON_INPUT_REGIONS_KEY = "DateFormatItemInputRegions";
// TimeZoneNamesBundle exemplar city prefix
private static final String TZNB_EXCITY_PREFIX = "timezone.excity.";
// null singleton cache value
private static final Object NULLOBJECT = new Object();
// RegEx pattern for skeleton validity checking
private static final Pattern VALID_SKELETON_PATTERN = Pattern.compile(
"(?<date>" +
"G{0,5}" + // Era
"y*" + // Year
"Q{0,5}" + // Quarter
"M{0,5}" + // Month
"w*" + // Week of Week Based Year
"E{0,5}" + // Day of Week
"d{0,2})" + // Day of Month
"(?<time>" +
"B{0,5}" + // Period/AmPm of Day
"[hHjC]{0,2}" + // Hour of Day/AmPm
"m{0,2}" + // Minute of Hour
"s{0,2}" + // Second of Minute
"[vz]{0,4})"); // Zone
// Input Skeleton map for "preferred" and "allowed"
// Map<"preferred"/"allowed", Map<"region", "skeleton">>
private static Map<String, Map<String, String>> inputSkeletons;
// Skeletons for "j" and "C" input skeleton symbols for this locale
private String jPattern;
private String CPattern;
LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale) {
this.locale = locale;
this.localeData = adapter.getLocaleData();
type = ((LocaleProviderAdapter)adapter).getAdapterType();
}
private void removeEmptyReferences() {
Object ref;
while ((ref = referenceQueue.poll()) != null) {
cache.remove(((ResourceReference)ref).getCacheKey());
}
}
Object getBreakIteratorInfo(String key) {
Object biInfo;
String cacheKey = BREAK_ITERATOR_INFO + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data == null || ((biInfo = data.get()) == null)) {
biInfo = localeData.getBreakIteratorInfo(locale).getObject(key);
cache.put(cacheKey, new ResourceReference(cacheKey, biInfo, referenceQueue));
}
return biInfo;
}
byte[] getBreakIteratorResources(String key) {
return (byte[]) localeData.getBreakIteratorResources(locale).getObject(key);
}
public String getCalendarData(String key) {
String caldata = "";
String cacheKey = CALENDAR_DATA + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data == null || ((caldata = (String) data.get()) == null)) {
ResourceBundle rb = localeData.getCalendarData(locale);
if (rb.containsKey(key)) {
caldata = rb.getString(key);
}
cache.put(cacheKey,
new ResourceReference(cacheKey, caldata, referenceQueue));
}
return caldata;
}
public String getCollationData() {
String key = "Rule";
String coldata = "";
removeEmptyReferences();
ResourceReference data = cache.get(COLLATION_DATA_CACHEKEY);
if (data == null || ((coldata = (String) data.get()) == null)) {
ResourceBundle rb = localeData.getCollationData(locale);
if (rb.containsKey(key)) {
coldata = rb.getString(key);
}
cache.put(COLLATION_DATA_CACHEKEY,
new ResourceReference(COLLATION_DATA_CACHEKEY, coldata, referenceQueue));
}
return coldata;
}
public Object[] getDecimalFormatSymbolsData() {
Object[] dfsdata;
removeEmptyReferences();
ResourceReference data = cache.get(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY);
if (data == null || ((dfsdata = (Object[]) data.get()) == null)) {
// Note that only dfsdata[0] is prepared here in this method. Other
// elements are provided by the caller, yet they are cached here.
ResourceBundle rb = localeData.getNumberFormatData(locale);
dfsdata = new Object[3];
dfsdata[0] = getNumberStrings(rb, "NumberElements");
cache.put(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY,
new ResourceReference(DECIMAL_FORMAT_SYMBOLS_DATA_CACHEKEY, dfsdata, referenceQueue));
}
return dfsdata;
}
private String[] getNumberStrings(ResourceBundle rb, String type) {
String[] ret = null;
String key;
String numSys;
// Number strings look up. First, try the Unicode extension
numSys = locale.getUnicodeLocaleType("nu");
if (numSys != null) {
key = numSys + "." + type;
if (rb.containsKey(key)) {
ret = rb.getStringArray(key);
}
}
// Next, try DefaultNumberingSystem value
if (ret == null && rb.containsKey("DefaultNumberingSystem")) {
key = rb.getString("DefaultNumberingSystem") + "." + type;
if (rb.containsKey(key)) {
ret = rb.getStringArray(key);
}
}
// Last resort. No need to check the availability.
// Just let it throw MissingResourceException when needed.
if (ret == null) {
ret = rb.getStringArray(type);
}
return ret;
}
public String getCurrencyName(String key) {
Object currencyName = null;
String cacheKey = CURRENCY_NAMES + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data != null && ((currencyName = data.get()) != null)) {
if (currencyName.equals(NULLOBJECT)) {
currencyName = null;
}
return (String) currencyName;
}
OpenListResourceBundle olrb = localeData.getCurrencyNames(locale);
if (olrb.containsKey(key)) {
currencyName = olrb.getObject(key);
cache.put(cacheKey,
new ResourceReference(cacheKey, currencyName, referenceQueue));
}
return (String) currencyName;
}
public String getLocaleName(String key) {
Object localeName = null;
String cacheKey = LOCALE_NAMES + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data != null && ((localeName = data.get()) != null)) {
if (localeName.equals(NULLOBJECT)) {
localeName = null;
}
return (String) localeName;
}
OpenListResourceBundle olrb = localeData.getLocaleNames(locale);
if (olrb.containsKey(key)) {
localeName = olrb.getObject(key);
cache.put(cacheKey,
new ResourceReference(cacheKey, localeName, referenceQueue));
}
return (String) localeName;
}
public Object getTimeZoneNames(String key) {
Object val = null;
String cacheKey = TIME_ZONE_NAMES + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (Objects.isNull(data) || Objects.isNull(val = data.get())) {
TimeZoneNamesBundle tznb = localeData.getTimeZoneNames(locale);
if (key.startsWith(TZNB_EXCITY_PREFIX)) {
if (tznb.containsKey(key)) {
val = tznb.getString(key);
assert val instanceof String;
trace("tznb: %s key: %s, val: %s\n", tznb, key, val);
}
} else {
String[] names = null;
if (tznb.containsKey(key)) {
names = tznb.getStringArray(key);
} else {
var tz = TimeZoneNameUtility.canonicalTZID(key).orElse(key);
if (tznb.containsKey(tz)) {
names = tznb.getStringArray(tz);
}
}
if (names != null) {
names[0] = key;
trace("tznb: %s key: %s, names: %s, %s, %s, %s, %s, %s, %s\n", tznb, key,
names[0], names[1], names[2], names[3], names[4], names[5], names[6]);
val = names;
}
}
if (val != null) {
cache.put(cacheKey,
new ResourceReference(cacheKey, val, referenceQueue));
}
}
return val;
}
@SuppressWarnings("unchecked")
Set<String> getZoneIDs() {
Set<String> zoneIDs;
removeEmptyReferences();
ResourceReference data = cache.get(ZONE_IDS_CACHEKEY);
if (data == null || ((zoneIDs = (Set<String>) data.get()) == null)) {
TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale);
zoneIDs = rb.keySet();
cache.put(ZONE_IDS_CACHEKEY,
new ResourceReference(ZONE_IDS_CACHEKEY, zoneIDs, referenceQueue));
}
return zoneIDs;
}
// zoneStrings are cached separately in TimeZoneNameUtility.
String[][] getZoneStrings() {
TimeZoneNamesBundle rb = localeData.getTimeZoneNames(locale);
Set<String> keyset = getZoneIDs();
// Use a LinkedHashSet to preserve the order
Set<String[]> value = new LinkedHashSet<>();
Set<String> tzIds = new HashSet<>(Arrays.asList(TimeZone.getAvailableIDs()));
for (String key : keyset) {
if (!key.startsWith(TZNB_EXCITY_PREFIX)) {
value.add(rb.getStringArray(key));
tzIds.remove(key);
}
}
if (type == LocaleProviderAdapter.Type.CLDR) {
// Note: TimeZoneNamesBundle creates a String[] on each getStringArray call.
// Add timezones which are not present in this keyset,
// so that their fallback names will be generated at runtime.
tzIds.stream().filter(i -> (!i.startsWith("Etc/GMT")
&& !i.startsWith("GMT")
&& !i.startsWith("SystemV")))
.forEach(tzid -> {
String[] val = new String[7];
if (keyset.contains(tzid)) {
val = rb.getStringArray(tzid);
} else {
var canonID = TimeZoneNameUtility.canonicalTZID(tzid)
.orElse(tzid);
if (keyset.contains(canonID)) {
val = rb.getStringArray(canonID);
}
}
val[0] = tzid;
value.add(val);
});
}
return value.toArray(new String[0][]);
}
String[] getCalendarNames(String key) {
String[] names = null;
String cacheKey = CALENDAR_NAMES + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data == null || ((names = (String[]) data.get()) == null)) {
ResourceBundle rb = localeData.getDateFormatData(locale);
if (rb.containsKey(key)) {
names = rb.getStringArray(key);
cache.put(cacheKey,
new ResourceReference(cacheKey, names, referenceQueue));
}
}
return names;
}
String[] getJavaTimeNames(String key) {
String[] names = null;
String cacheKey = CALENDAR_NAMES + key;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data == null || ((names = (String[]) data.get()) == null)) {
ResourceBundle rb = getJavaTimeFormatData();
if (rb.containsKey(key)) {
names = rb.getStringArray(key);
cache.put(cacheKey,
new ResourceReference(cacheKey, names, referenceQueue));
}
}
return names;
}
public String getDateTimePattern(int timeStyle, int dateStyle, Calendar cal) {
if (cal == null) {
cal = Calendar.getInstance(locale);
}
return getDateTimePattern(null, timeStyle, dateStyle, cal.getCalendarType());
}
/**
* Returns a date-time format pattern
* @param timeStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat,
* or -1 if not required
* @param dateStyle style of time; one of FULL, LONG, MEDIUM, SHORT in DateFormat,
* or -1 if not required
* @param calType the calendar type for the pattern
* @return the pattern string
*/
public String getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType) {
calType = CalendarDataUtility.normalizeCalendarType(calType);
String pattern;
pattern = getDateTimePattern("java.time.", timeStyle, dateStyle, calType);
if (pattern == null) {
pattern = getDateTimePattern(null, timeStyle, dateStyle, calType);
}
return pattern;
}
private String getDateTimePattern(String prefix, int timeStyle, int dateStyle, String calType) {
String pattern;
String timePattern = null;
String datePattern = null;
if (timeStyle >= 0) {
if (prefix != null) {
timePattern = getDateTimePattern(prefix, "TimePatterns", timeStyle, calType);
}
if (timePattern == null) {
timePattern = getDateTimePattern(null, "TimePatterns", timeStyle, calType);
}
}
if (dateStyle >= 0) {
if (prefix != null) {
datePattern = getDateTimePattern(prefix, "DatePatterns", dateStyle, calType);
}
if (datePattern == null) {
datePattern = getDateTimePattern(null, "DatePatterns", dateStyle, calType);
}
}
if (timeStyle >= 0) {
if (dateStyle >= 0) {
String dateTimePattern = null;
int dateTimeStyle = Math.max(dateStyle, timeStyle);
if (prefix != null) {
dateTimePattern = getDateTimePattern(prefix, "DateTimePatterns", dateTimeStyle, calType);
}
if (dateTimePattern == null) {
dateTimePattern = getDateTimePattern(null, "DateTimePatterns", dateTimeStyle, calType);
}
pattern = switch (Objects.requireNonNull(dateTimePattern)) {
case "{1} {0}" -> datePattern + " " + timePattern;
case "{0} {1}" -> timePattern + " " + datePattern;
default -> MessageFormat.format(dateTimePattern.replaceAll("'", "''"), timePattern, datePattern);
};
} else {
pattern = timePattern;
}
} else if (dateStyle >= 0) {
pattern = datePattern;
} else {
throw new IllegalArgumentException("No date or time style specified");
}
return pattern;
}
public String[] getNumberPatterns() {
String[] numberPatterns;
removeEmptyReferences();
ResourceReference data = cache.get(NUMBER_PATTERNS_CACHEKEY);
if (data == null || ((numberPatterns = (String[]) data.get()) == null)) {
ResourceBundle resource = localeData.getNumberFormatData(locale);
numberPatterns = getNumberStrings(resource, "NumberPatterns");
cache.put(NUMBER_PATTERNS_CACHEKEY,
new ResourceReference(NUMBER_PATTERNS_CACHEKEY, numberPatterns, referenceQueue));
}
return numberPatterns;
}
/**
* Returns the compact number format patterns.
* @param formatStyle the style for formatting a number
* @return an array of compact number patterns
*/
public String[] getCNPatterns(NumberFormat.Style formatStyle) {
Objects.requireNonNull(formatStyle);
String[] compactNumberPatterns;
removeEmptyReferences();
String width = (formatStyle == NumberFormat.Style.LONG) ? "long" : "short";
String cacheKey = width + "." + COMPACT_NUMBER_PATTERNS_CACHEKEY;
ResourceReference data = cache.get(cacheKey);
if (data == null || ((compactNumberPatterns
= (String[]) data.get()) == null)) {
ResourceBundle resource = localeData.getNumberFormatData(locale);
compactNumberPatterns = (String[]) resource
.getObject(width + ".CompactNumberPatterns");
cache.put(cacheKey, new ResourceReference(cacheKey, compactNumberPatterns, referenceQueue));
}
return compactNumberPatterns;
}
/**
* Returns the FormatData resource bundle of this LocaleResources.
* The FormatData should be used only for accessing extra
* resources required by JSR 310.
*/
public ResourceBundle getJavaTimeFormatData() {
ResourceBundle rb = localeData.getDateFormatData(locale);
if (rb instanceof ParallelListResourceBundle) {
localeData.setSupplementary((ParallelListResourceBundle) rb);
}
return rb;
}
/**
* Returns the actual format pattern string based on the requested template
* and calendar type for this locale.
*
* @param requestedTemplate requested template
* @param calType calendar type
* @throws IllegalArgumentException if the requested template is invalid
* @return format pattern string for this locale, null if not found
*/
public String getLocalizedPattern(String requestedTemplate, String calType) {
String pattern;
String cacheKey = SKELETON_PATTERN + calType + "." + requestedTemplate;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data == null || ((pattern = (String) data.get()) == null)) {
pattern = getLocalizedPatternImpl(requestedTemplate, calType);
cache.put(cacheKey,
new ResourceReference(cacheKey, pattern != null ? pattern : "", referenceQueue));
} else if ("".equals(pattern)) {
// non-existent pattern
pattern = null;
}
return pattern;
}
private String getLocalizedPatternImpl(String requestedTemplate, String calType) {
initSkeletonIfNeeded();
// input skeleton substitution
var skeleton = substituteInputSkeletons(requestedTemplate);
// validity check
var matcher = VALID_SKELETON_PATTERN.matcher(skeleton);
if (!matcher.matches()) {
throw new IllegalArgumentException("Requested template \"%s\" is invalid".formatted(requestedTemplate) +
(requestedTemplate.equals(skeleton) ? "." : ", which translated into \"%s\"".formatted(skeleton) +
" after the 'j' or 'C' substitution."));
}
// try to match entire requested template first
String matched = matchSkeleton(skeleton, calType);
if (matched == null) {
// 2.6.2.2 Missing Skeleton Fields
var dateMatched = matchSkeleton(matcher.group("date"), calType);
var timeMatched = matchSkeleton(matcher.group("time"), calType);
if (dateMatched != null && timeMatched != null) {
// combine both matches
var style = switch (requestedTemplate.replaceAll("[^M]+", "").length()) {
case 4 -> requestedTemplate.indexOf('E') >= 0 ? 0 : 1;
case 3 -> 2;
default -> 3;
};
var dateTimePattern = getDateTimePattern(null, "DateTimePatterns", style, calType);
matched = MessageFormat.format(dateTimePattern.replaceAll("'", "''"), timeMatched, dateMatched);
}
}
trace("requested: %s, locale: %s, calType: %s, matched: %s\n", requestedTemplate, locale, calType, matched);
return matched;
}
private String matchSkeleton(String skeleton, String calType) {
// Expand it with possible inferred skeleton stream based on its priority
var inferred = possibleInferred(skeleton);
// Search the closest format pattern string from the resource bundle
ResourceBundle r = localeData.getDateFormatData(locale);
return inferred
.map(s -> ("gregory".equals(calType) ? "" : calType + ".") + "DateFormatItem." + s)
.map(key -> r.containsKey(key) ? r.getString(key) : null)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private void initSkeletonIfNeeded() {
// "preferred"/"allowed" input skeleton maps
if (inputSkeletons == null) {
inputSkeletons = new HashMap<>();
Pattern p = Pattern.compile("([^:]+):([^;]+);");
ResourceBundle r = localeData.getDateFormatData(Locale.ROOT);
Stream.of("preferred", "allowed").forEach(type -> {
var inputRegionsKey = SKELETON_INPUT_REGIONS_KEY + "." + type;
Map<String, String> typeMap = new HashMap<>();
if (r.containsKey(inputRegionsKey)) {
p.matcher(r.getString(inputRegionsKey)).results()
.forEach(mr ->
Arrays.stream(mr.group(2).split(" "))
.forEach(region -> typeMap.put(region, mr.group(1))));
}
inputSkeletons.put(type, typeMap);
});
}
// j/C patterns for this locale
if (jPattern == null) {
jPattern = resolveInputSkeleton("preferred");
CPattern = resolveInputSkeleton("allowed");
// hack: "allowed" contains reversed order for hour/period, e.g, "hB" which should be "Bh" as a skeleton
if (CPattern.length() == 2) {
var ba = new byte[2];
ba[0] = (byte)CPattern.charAt(1);
ba[1] = (byte)CPattern.charAt(0);
CPattern = new String(ba);
}
}
}
/**
* Resolve locale specific input skeletons. Fall back method is different from usual
* resource bundle's, as it has to be "lang-region" -> "region" -> "lang-001" -> "001"
* @param type type of the input skeleton
* @return resolved skeletons for this locale, defaults to "h" if none found.
*/
private String resolveInputSkeleton(String type) {
var regionToSkeletonMap = inputSkeletons.get(type);
return regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-" + locale.getCountry(),
regionToSkeletonMap.getOrDefault(locale.getCountry(),
regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-001",
regionToSkeletonMap.getOrDefault("001", "h"))));
}
/**
* Replace 'j' and 'C' input skeletons with locale specific patterns. Note that 'j'
* is guaranteed to be replaced with one char [hkHK], while 'C' may be replaced with
* multiple chars. Repeat each as much as 'C' count.
* @param requestedTemplate requested skeleton
* @return skeleton with j/C substituted with concrete patterns
*/
private String substituteInputSkeletons(String requestedTemplate) {
var cCount = requestedTemplate.chars().filter(c -> c == 'C').count();
return requestedTemplate.replaceAll("j", jPattern)
.replaceFirst("C+", CPattern.replaceAll("([hkHK])", "$1".repeat((int)cCount)));
}
/**
* Returns a stream of possible skeletons, inferring standalone/format (M/L and/or E/c) patterns
* and their styles. (cf. 2.6.2.1 Matching Skeletons)
*
* @param skeleton original skeleton
* @return inferred Stream of skeletons in its priority order
*/
private Stream<String> possibleInferred(String skeleton) {
return priorityList(skeleton, "M", "L").stream()
.flatMap(s -> priorityList(s, "E", "c").stream())
.distinct();
}
/**
* Inferring the possible format styles in priority order, based on the original
* skeleton length.
*
* @param skeleton skeleton
* @param pChar pattern character string
* @param subChar substitute character string
* @return list of skeletons
*/
private List<String> priorityList(String skeleton, String pChar, String subChar) {
int first = skeleton.indexOf(pChar);
int last = skeleton.lastIndexOf(pChar);
if (first >= 0) {
var prefix = skeleton.substring(0, first);
var suffix = skeleton.substring(last + 1);
// Priority are based on this chart. First column is the original count of `pChar`,
// then it is followed by inferred skeletons base on priority.
//
// 1->2->3->4 (number form (1-digit) -> number form (2-digit) -> Abbr. form -> Full form)
// 2->1->3->4
// 3->4->2->1
// 4->3->2->1
var o1 = prefix + pChar + suffix;
var o2 = prefix + pChar.repeat(2) + suffix;
var o3 = prefix + pChar.repeat(3) + suffix;
var o4 = prefix + pChar.repeat(4) + suffix;
var s1 = prefix + subChar + suffix;
var s2 = prefix + subChar.repeat(2) + suffix;
var s3 = prefix + subChar.repeat(3) + suffix;
var s4 = prefix + subChar.repeat(4) + suffix;
return switch (last - first) {
case 1 -> List.of(skeleton, o1, o2, o3, o4, s1, s2, s3, s4);
case 2 -> List.of(skeleton, o2, o1, o3, o4, s2, s1, s3, s4);
case 3 -> List.of(skeleton, o3, o4, o2, o1, s3, s4, s2, s1);
default -> List.of(skeleton, o4, o3, o2, o1, s4, s3, s2, s1);
};
} else {
return List.of(skeleton);
}
}
private String getDateTimePattern(String prefix, String key, int styleIndex, String calendarType) {
StringBuilder sb = new StringBuilder();
if (prefix != null) {
sb.append(prefix);
}
if (!"gregory".equals(calendarType)) {
sb.append(calendarType).append('.');
}
sb.append(key);
String resourceKey = sb.toString();
String cacheKey = sb.insert(0, DATE_TIME_PATTERN).toString();
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
Object value = NULLOBJECT;
if (data == null || ((value = data.get()) == null)) {
ResourceBundle r = (prefix != null) ? getJavaTimeFormatData() : localeData.getDateFormatData(locale);
if (r.containsKey(resourceKey)) {
value = r.getStringArray(resourceKey);
} else {
assert !resourceKey.equals(key);
if (r.containsKey(key)) {
value = r.getStringArray(key);
}
}
cache.put(cacheKey,
new ResourceReference(cacheKey, value, referenceQueue));
}
if (value == NULLOBJECT) {
assert prefix != null;
return null;
}
// for DateTimePatterns. CLDR has multiple styles, while JRE has one.
String[] styles = (String[])value;
return (styles.length > 1 ? styles[styleIndex] : styles[0]);
}
public String[] getRules() {
String[] rules;
removeEmptyReferences();
ResourceReference data = cache.get(RULES_CACHEKEY);
if (data == null || ((rules = (String[]) data.get()) == null)) {
ResourceBundle rb = localeData.getDateFormatData(locale);
rules = new String[2];
rules[0] = rules[1] = "";
if (rb.containsKey("PluralRules")) {
rules[0] = rb.getString("PluralRules");
}
if (rb.containsKey("DayPeriodRules")) {
rules[1] = rb.getString("DayPeriodRules");
}
cache.put(RULES_CACHEKEY, new ResourceReference(RULES_CACHEKEY, rules, referenceQueue));
}
return rules;
}
private static class ResourceReference extends SoftReference<Object> {
private final String cacheKey;
ResourceReference(String cacheKey, Object o, ReferenceQueue<Object> q) {
super(o, q);
this.cacheKey = cacheKey;
}
String getCacheKey() {
return cacheKey;
}
}
private static final boolean TRACE_ON = Boolean.parseBoolean(
GetPropertyAction.privilegedGetProperty("locale.resources.debug", "false"));
public static void trace(String format, Object... params) {
if (TRACE_ON) {
System.out.format(format, params);
}
}
}