| /* | |
| * Copyright (c) 2012, 2024, 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.ListFormat; | |
| 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 jdk.internal.util.StaticProperty; | |
| 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 = "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."; | |
| private static final String LIST_PATTERN = "LP."; | |
| // 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 cacheKey = COLLATION_DATA; | |
| String coldata = ""; | |
| try { | |
| var type = locale.getUnicodeLocaleType("co"); | |
| if (type != null && !type.isEmpty() && !type.equalsIgnoreCase("standard")) { | |
| key += "." + type; | |
| cacheKey += type; | |
| } | |
| } catch (IllegalArgumentException ignore) {} | |
| removeEmptyReferences(); | |
| ResourceReference data = cache.get(cacheKey); | |
| if (data == null || ((coldata = (String) data.get()) == null)) { | |
| ResourceBundle rb = localeData.getCollationData(locale); | |
| if (rb.containsKey(key)) { | |
| coldata = rb.getString(key); | |
| } | |
| cache.put(cacheKey, new ResourceReference(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) { | |
| // Get names for old ISO codes with new ISO code resources | |
| if (StaticProperty.javaLocaleUseOldISOCodes().equalsIgnoreCase("true")) { | |
| key = switch (key) { | |
| case "iw" -> "he"; | |
| case "in" -> "id"; | |
| case "ji" -> "yi"; | |
| default -> 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; | |
| } | |
| /** | |
| * {@return the list patterns for the locale} | |
| * | |
| * @param type a {@link ListFormat.Type} | |
| * @param style a {@link ListFormat.Style} | |
| */ | |
| public String[] getListPatterns(ListFormat.Type type, ListFormat.Style style) { | |
| String typeStr = type.toString().toLowerCase(Locale.ROOT); | |
| String styleStr = style.toString().toLowerCase(Locale.ROOT); | |
| String[] lpArray; | |
| String cacheKey = LIST_PATTERN + typeStr + styleStr; | |
| removeEmptyReferences(); | |
| ResourceReference data = cache.get(cacheKey); | |
| if (data == null || ((lpArray = (String[]) data.get()) == null)) { | |
| var rbKey = "ListPatterns_" + typeStr + (style == ListFormat.Style.FULL ? "" : "-" + styleStr); | |
| lpArray = localeData.getDateFormatData(locale).getStringArray(rbKey); | |
| if (lpArray[0].isEmpty() || lpArray[1].isEmpty() || lpArray[2].isEmpty()) { | |
| if (LocaleProviderAdapter.forType(LocaleProviderAdapter.Type.CLDR) | |
| instanceof ResourceBundleBasedAdapter rbba) { | |
| var candList = rbba.getCandidateLocales("", locale); | |
| if (!candList.isEmpty()) { | |
| for (var p : candList.subList(1, candList.size())) { | |
| var parentPatterns = localeData.getDateFormatData(p).getStringArray(rbKey); | |
| for (int i = 0; i < 3; i++) { // exclude optional ones, ie, "two"/"three" | |
| if (lpArray[i].isEmpty()) { | |
| lpArray[i] = parentPatterns[i]; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| cache.put(cacheKey, new ResourceReference(cacheKey, lpArray, referenceQueue)); | |
| } | |
| return lpArray; | |
| } | |
| 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( | |
| System.getProperty("locale.resources.debug", "false")); | |
| public static void trace(String format, Object... params) { | |
| if (TRACE_ON) { | |
| System.out.format(format, params); | |
| } | |
| } | |
| } |