| // © 2016 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html |
| /* |
| ******************************************************************************* |
| * Copyright (C) 2011-2016, International Business Machines Corporation and |
| * others. All Rights Reserved. |
| ******************************************************************************* |
| */ |
| package com.ibm.icu.impl; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.Serializable; |
| import java.lang.ref.WeakReference; |
| import java.text.MessageFormat; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.MissingResourceException; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import com.ibm.icu.impl.TextTrieMap.ResultHandler; |
| import com.ibm.icu.text.LocaleDisplayNames; |
| import com.ibm.icu.text.TimeZoneFormat.TimeType; |
| import com.ibm.icu.text.TimeZoneNames; |
| import com.ibm.icu.text.TimeZoneNames.MatchInfo; |
| import com.ibm.icu.text.TimeZoneNames.NameType; |
| import com.ibm.icu.util.BasicTimeZone; |
| import com.ibm.icu.util.Freezable; |
| import com.ibm.icu.util.Output; |
| import com.ibm.icu.util.TimeZone; |
| import com.ibm.icu.util.TimeZone.SystemTimeZoneType; |
| import com.ibm.icu.util.TimeZoneTransition; |
| import com.ibm.icu.util.ULocale; |
| |
| /** |
| * This class interact with TimeZoneNames and LocaleDisplayNames |
| * to format and parse time zone's generic display names. |
| * It is not recommended to use this class directly, instead |
| * use com.ibm.icu.text.TimeZoneFormat. |
| */ |
| public class TimeZoneGenericNames implements Serializable, Freezable<TimeZoneGenericNames> { |
| |
| // Note: This class implements Serializable, but we no longer serialize instance of |
| // TimeZoneGenericNames in ICU 49. ICU 4.8 com.ibm.icu.text.TimeZoneFormat used to |
| // serialize TimeZoneGenericNames field. TimeZoneFormat no longer read TimeZoneGenericNames |
| // field, we have to keep TimeZoneGenericNames Serializable. Otherwise it fails to read |
| // (unused) TimeZoneGenericNames serialized data. |
| |
| private static final long serialVersionUID = 2729910342063468417L; |
| |
| /** |
| * Generic name type enum |
| */ |
| public enum GenericNameType { |
| LOCATION ("LONG", "SHORT"), |
| LONG (), |
| SHORT (); |
| |
| String[] _fallbackTypeOf; |
| GenericNameType(String... fallbackTypeOf) { |
| _fallbackTypeOf = fallbackTypeOf; |
| } |
| |
| public boolean isFallbackTypeOf(GenericNameType type) { |
| String typeStr = type.toString(); |
| for (String t : _fallbackTypeOf) { |
| if (t.equals(typeStr)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Format pattern enum used for composing location and partial location names |
| */ |
| public enum Pattern { |
| // The format pattern such as "{0} Time", where {0} is the country or city. |
| REGION_FORMAT("regionFormat", "({0})"), |
| |
| // Note: FALLBACK_REGION_FORMAT is no longer used since ICU 50/CLDR 22.1 |
| // The format pattern such as "{1} Time ({0})", where {1} is the country and {0} is a city. |
| //FALLBACK_REGION_FORMAT("fallbackRegionFormat", "{1} ({0})"), |
| |
| // The format pattern such as "{1} ({0})", where {1} is the metazone, and {0} is the country or city. |
| FALLBACK_FORMAT("fallbackFormat", "{1} ({0})"); |
| |
| String _key; |
| String _defaultVal; |
| |
| Pattern(String key, String defaultVal) { |
| _key = key; |
| _defaultVal = defaultVal; |
| } |
| |
| String key() { |
| return _key; |
| } |
| |
| String defaultValue() { |
| return _defaultVal; |
| } |
| } |
| |
| private final ULocale _locale; |
| private TimeZoneNames _tznames; |
| |
| private transient volatile boolean _frozen; |
| private transient String _region; |
| private transient WeakReference<LocaleDisplayNames> _localeDisplayNamesRef; |
| private transient MessageFormat[] _patternFormatters; |
| |
| private transient ConcurrentHashMap<String, String> _genericLocationNamesMap; |
| private transient ConcurrentHashMap<String, String> _genericPartialLocationNamesMap; |
| private transient TextTrieMap<NameInfo> _gnamesTrie; |
| private transient boolean _gnamesTrieFullyLoaded; |
| |
| private static Cache GENERIC_NAMES_CACHE = new Cache(); |
| |
| // Window size used for DST check for a zone in a metazone (about a half year) |
| private static final long DST_CHECK_RANGE = 184L*(24*60*60*1000); |
| |
| private static final NameType[] GENERIC_NON_LOCATION_TYPES = |
| {NameType.LONG_GENERIC, NameType.SHORT_GENERIC}; |
| |
| |
| /** |
| * Constructs a <code>TimeZoneGenericNames</code> with the given locale |
| * and the <code>TimeZoneNames</code>. |
| * @param locale the locale |
| * @param tznames the TimeZoneNames |
| */ |
| public TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames) { |
| _locale = locale; |
| _tznames = tznames; |
| init(); |
| } |
| |
| /** |
| * Private method initializing the instance of <code>TimeZoneGenericName</code>. |
| * This method should be called from a constructor and readObject. |
| */ |
| private void init() { |
| if (_tznames == null) { |
| _tznames = TimeZoneNames.getInstance(_locale); |
| } |
| _genericLocationNamesMap = new ConcurrentHashMap<String, String>(); |
| _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>(); |
| |
| _gnamesTrie = new TextTrieMap<NameInfo>(true); |
| _gnamesTrieFullyLoaded = false; |
| |
| // Preload zone strings for the default time zone |
| TimeZone tz = TimeZone.getDefault(); |
| String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); |
| if (tzCanonicalID != null) { |
| loadStrings(tzCanonicalID); |
| } |
| } |
| |
| /** |
| * Constructs a <code>TimeZoneGenericNames</code> with the given locale. |
| * This constructor is private and called from {@link #getInstance(ULocale)}. |
| * @param locale the locale |
| */ |
| private TimeZoneGenericNames(ULocale locale) { |
| this(locale, null); |
| } |
| |
| /** |
| * The factory method of <code>TimeZoneGenericNames</code>. This static method |
| * returns a frozen instance of cached <code>TimeZoneGenericNames</code>. |
| * @param locale the locale |
| * @return A frozen <code>TimeZoneGenericNames</code>. |
| */ |
| public static TimeZoneGenericNames getInstance(ULocale locale) { |
| String key = locale.getBaseName(); |
| return GENERIC_NAMES_CACHE.getInstance(key, locale); |
| } |
| |
| /** |
| * Returns the display name of the time zone for the given name type |
| * at the given date, or null if the display name is not available. |
| * |
| * @param tz the time zone |
| * @param type the generic name type - see {@link GenericNameType} |
| * @param date the date |
| * @return the display name of the time zone for the given name type |
| * at the given date, or null. |
| */ |
| public String getDisplayName(TimeZone tz, GenericNameType type, long date) { |
| String name = null; |
| String tzCanonicalID = null; |
| switch (type) { |
| case LOCATION: |
| tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); |
| if (tzCanonicalID != null) { |
| name = getGenericLocationName(tzCanonicalID); |
| } |
| break; |
| case LONG: |
| case SHORT: |
| name = formatGenericNonLocationName(tz, type, date); |
| if (name == null) { |
| tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); |
| if (tzCanonicalID != null) { |
| name = getGenericLocationName(tzCanonicalID); |
| } |
| } |
| break; |
| } |
| return name; |
| } |
| |
| /** |
| * Returns the generic location name for the given canonical time zone ID. |
| * |
| * @param canonicalTzID the canonical time zone ID |
| * @return the generic location name for the given canonical time zone ID. |
| */ |
| public String getGenericLocationName(String canonicalTzID) { |
| if (canonicalTzID == null || canonicalTzID.length() == 0) { |
| return null; |
| } |
| String name = _genericLocationNamesMap.get(canonicalTzID); |
| if (name != null) { |
| if (name.length() == 0) { |
| // empty string to indicate the name is not available |
| return null; |
| } |
| return name; |
| } |
| |
| Output<Boolean> isPrimary = new Output<Boolean>(); |
| String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID, isPrimary); |
| if (countryCode != null) { |
| if (isPrimary.value) { |
| // If this is only the single zone in the country, use the country name |
| String country = getLocaleDisplayNames().regionDisplayName(countryCode); |
| name = formatPattern(Pattern.REGION_FORMAT, country); |
| } else { |
| // If there are multiple zones including this in the country, |
| // use the exemplar city name |
| |
| // getExemplarLocationName should return non-empty String |
| // if the time zone is associated with a location |
| String city = _tznames.getExemplarLocationName(canonicalTzID); |
| name = formatPattern(Pattern.REGION_FORMAT, city); |
| } |
| } |
| |
| if (name == null) { |
| _genericLocationNamesMap.putIfAbsent(canonicalTzID.intern(), ""); |
| } else { |
| synchronized (this) { // we have to sync the name map and the trie |
| canonicalTzID = canonicalTzID.intern(); |
| String tmp = _genericLocationNamesMap.putIfAbsent(canonicalTzID, name.intern()); |
| if (tmp == null) { |
| // Also put the name info the to trie |
| NameInfo info = new NameInfo(canonicalTzID, GenericNameType.LOCATION); |
| _gnamesTrie.put(name, info); |
| } else { |
| name = tmp; |
| } |
| } |
| } |
| return name; |
| } |
| |
| /** |
| * Sets the pattern string for the pattern type. |
| * Note: This method is designed for CLDR ST - not for common use. |
| * @param patType the pattern type |
| * @param patStr the pattern string |
| * @return this object. |
| */ |
| public TimeZoneGenericNames setFormatPattern(Pattern patType, String patStr) { |
| if (isFrozen()) { |
| throw new UnsupportedOperationException("Attempt to modify frozen object"); |
| } |
| |
| // Changing pattern will invalidates cached names |
| if (!_genericLocationNamesMap.isEmpty()) { |
| _genericLocationNamesMap = new ConcurrentHashMap<String, String>(); |
| } |
| if (!_genericPartialLocationNamesMap.isEmpty()) { |
| _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>(); |
| } |
| _gnamesTrie = null; |
| _gnamesTrieFullyLoaded = false; |
| |
| if (_patternFormatters == null) { |
| _patternFormatters = new MessageFormat[Pattern.values().length]; |
| } |
| _patternFormatters[patType.ordinal()] = new MessageFormat(patStr); |
| return this; |
| } |
| |
| /** |
| * Private method to get a generic string, with fallback logics involved, |
| * that is, |
| * |
| * 1. If a generic non-location string is available for the zone, return it. |
| * 2. If a generic non-location string is associated with a meta zone and |
| * the zone never use daylight time around the given date, use the standard |
| * string (if available). |
| * 3. If a generic non-location string is associated with a meta zone and |
| * the offset at the given time is different from the preferred zone for the |
| * current locale, then return the generic partial location string (if available) |
| * 4. If a generic non-location string is not available, use generic location |
| * string. |
| * |
| * @param tz the requested time zone |
| * @param date the date |
| * @param type the generic name type, either LONG or SHORT |
| * @return the name used for a generic name type, which could be the |
| * generic name, or the standard name (if the zone does not observes DST |
| * around the date), or the partial location name. |
| */ |
| private String formatGenericNonLocationName(TimeZone tz, GenericNameType type, long date) { |
| assert(type == GenericNameType.LONG || type == GenericNameType.SHORT); |
| String tzID = ZoneMeta.getCanonicalCLDRID(tz); |
| |
| if (tzID == null) { |
| return null; |
| } |
| |
| // Try to get a name from time zone first |
| NameType nameType = (type == GenericNameType.LONG) ? NameType.LONG_GENERIC : NameType.SHORT_GENERIC; |
| String name = _tznames.getTimeZoneDisplayName(tzID, nameType); |
| |
| if (name != null) { |
| return name; |
| } |
| |
| // Try meta zone |
| String mzID = _tznames.getMetaZoneID(tzID, date); |
| if (mzID != null) { |
| boolean useStandard = false; |
| int[] offsets = {0, 0}; |
| tz.getOffset(date, false, offsets); |
| |
| if (offsets[1] == 0) { |
| useStandard = true; |
| // Check if the zone actually uses daylight saving time around the time |
| if (tz instanceof BasicTimeZone) { |
| BasicTimeZone btz = (BasicTimeZone)tz; |
| TimeZoneTransition before = btz.getPreviousTransition(date, true); |
| if (before != null |
| && (date - before.getTime() < DST_CHECK_RANGE) |
| && before.getFrom().getDSTSavings() != 0) { |
| useStandard = false; |
| } else { |
| TimeZoneTransition after = btz.getNextTransition(date, false); |
| if (after != null |
| && (after.getTime() - date < DST_CHECK_RANGE) |
| && after.getTo().getDSTSavings() != 0) { |
| useStandard = false; |
| } |
| } |
| } else { |
| // If not BasicTimeZone... only if the instance is not an ICU's implementation. |
| // We may get a wrong answer in edge case, but it should practically work OK. |
| int[] tmpOffsets = new int[2]; |
| tz.getOffset(date - DST_CHECK_RANGE, false, tmpOffsets); |
| if (tmpOffsets[1] != 0) { |
| useStandard = false; |
| } else { |
| tz.getOffset(date + DST_CHECK_RANGE, false, tmpOffsets); |
| if (tmpOffsets[1] != 0){ |
| useStandard = false; |
| } |
| } |
| } |
| } |
| if (useStandard) { |
| NameType stdNameType = (nameType == NameType.LONG_GENERIC) ? |
| NameType.LONG_STANDARD : NameType.SHORT_STANDARD; |
| String stdName = _tznames.getDisplayName(tzID, stdNameType, date); |
| if (stdName != null) { |
| name = stdName; |
| |
| // TODO: revisit this issue later |
| // In CLDR, a same display name is used for both generic and standard |
| // for some meta zones in some locales. This looks like a data bugs. |
| // For now, we check if the standard name is different from its generic |
| // name below. |
| String mzGenericName = _tznames.getMetaZoneDisplayName(mzID, nameType); |
| if (stdName.equalsIgnoreCase(mzGenericName)) { |
| name = null; |
| } |
| } |
| } |
| |
| if (name == null) { |
| // Get a name from meta zone |
| String mzName = _tznames.getMetaZoneDisplayName(mzID, nameType); |
| if (mzName != null) { |
| // Check if we need to use a partial location format. |
| // This check is done by comparing offset with the meta zone's |
| // golden zone at the given date. |
| String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); |
| if (goldenID != null && !goldenID.equals(tzID)) { |
| TimeZone goldenZone = TimeZone.getFrozenTimeZone(goldenID); |
| int[] offsets1 = {0, 0}; |
| |
| // Check offset in the golden zone with wall time. |
| // With getOffset(date, false, offsets1), |
| // you may get incorrect results because of time overlap at DST->STD |
| // transition. |
| goldenZone.getOffset(date + offsets[0] + offsets[1], true, offsets1); |
| |
| if (offsets[0] != offsets1[0] || offsets[1] != offsets1[1]) { |
| // Now we need to use a partial location format. |
| name = getPartialLocationName(tzID, mzID, (nameType == NameType.LONG_GENERIC), mzName); |
| } else { |
| name = mzName; |
| } |
| } else { |
| name = mzName; |
| } |
| } |
| } |
| } |
| return name; |
| } |
| |
| /** |
| * Private simple pattern formatter used for formatting generic location names |
| * and partial location names. We intentionally use JDK MessageFormat |
| * for performance reason. |
| * |
| * @param pat the message pattern enum |
| * @param args the format argument(s) |
| * @return the formatted string |
| */ |
| private synchronized String formatPattern(Pattern pat, String... args) { |
| if (_patternFormatters == null) { |
| _patternFormatters = new MessageFormat[Pattern.values().length]; |
| } |
| |
| int idx = pat.ordinal(); |
| if (_patternFormatters[idx] == null) { |
| String patText; |
| try { |
| ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance( |
| ICUData.ICU_ZONE_BASE_NAME, _locale); |
| patText = bundle.getStringWithFallback("zoneStrings/" + pat.key()); |
| } catch (MissingResourceException e) { |
| patText = pat.defaultValue(); |
| } |
| |
| _patternFormatters[idx] = new MessageFormat(patText); |
| } |
| return _patternFormatters[idx].format(args); |
| } |
| |
| /** |
| * Private method returning LocaleDisplayNames instance for the locale of this |
| * instance. Because LocaleDisplayNames is only used for generic |
| * location formant and partial location format, the LocaleDisplayNames |
| * is instantiated lazily. |
| * |
| * @return the instance of LocaleDisplayNames for the locale of this object. |
| */ |
| private synchronized LocaleDisplayNames getLocaleDisplayNames() { |
| LocaleDisplayNames locNames = null; |
| if (_localeDisplayNamesRef != null) { |
| locNames = _localeDisplayNamesRef.get(); |
| } |
| if (locNames == null) { |
| locNames = LocaleDisplayNames.getInstance(_locale); |
| _localeDisplayNamesRef = new WeakReference<LocaleDisplayNames>(locNames); |
| } |
| return locNames; |
| } |
| |
| private synchronized void loadStrings(String tzCanonicalID) { |
| if (tzCanonicalID == null || tzCanonicalID.length() == 0) { |
| return; |
| } |
| // getGenericLocationName() formats a name and put it into the trie |
| getGenericLocationName(tzCanonicalID); |
| |
| // Generic partial location format |
| Set<String> mzIDs = _tznames.getAvailableMetaZoneIDs(tzCanonicalID); |
| for (String mzID : mzIDs) { |
| // if this time zone is not the golden zone of the meta zone, |
| // partial location name (such as "PT (Los Angeles)") might be |
| // available. |
| String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); |
| if (!tzCanonicalID.equals(goldenID)) { |
| for (NameType genNonLocType : GENERIC_NON_LOCATION_TYPES) { |
| String mzGenName = _tznames.getMetaZoneDisplayName(mzID, genNonLocType); |
| if (mzGenName != null) { |
| // getPartialLocationName() formats a name and put it into the trie |
| getPartialLocationName(tzCanonicalID, mzID, (genNonLocType == NameType.LONG_GENERIC), mzGenName); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Private method returning the target region. The target regions is determined by |
| * the locale of this instance. When a generic name is coming from |
| * a meta zone, this region is used for checking if the time zone |
| * is a reference zone of the meta zone. |
| * |
| * @return the target region |
| */ |
| private synchronized String getTargetRegion() { |
| if (_region == null) { |
| _region = _locale.getCountry(); |
| if (_region.length() == 0) { |
| ULocale tmp = ULocale.addLikelySubtags(_locale); |
| _region = tmp.getCountry(); |
| if (_region.length() == 0) { |
| _region = "001"; |
| } |
| } |
| } |
| return _region; |
| } |
| |
| /** |
| * Private method for formatting partial location names. This format |
| * is used when a generic name of a meta zone is available, but the given |
| * time zone is not a reference zone (golden zone) of the meta zone. |
| * |
| * @param tzID the canonical time zone ID |
| * @param mzID the meta zone ID |
| * @param isLong true when long generic name |
| * @param mzDisplayName the meta zone generic display name |
| * @return the partial location format string |
| */ |
| private String getPartialLocationName(String tzID, String mzID, boolean isLong, String mzDisplayName) { |
| String letter = isLong ? "L" : "S"; |
| String key = tzID + "&" + mzID + "#" + letter; |
| String name = _genericPartialLocationNamesMap.get(key); |
| if (name != null) { |
| return name; |
| } |
| String location = null; |
| String countryCode = ZoneMeta.getCanonicalCountry(tzID); |
| if (countryCode != null) { |
| // Is this the golden zone for the region? |
| String regionalGolden = _tznames.getReferenceZoneID(mzID, countryCode); |
| if (tzID.equals(regionalGolden)) { |
| // Use country name |
| location = getLocaleDisplayNames().regionDisplayName(countryCode); |
| } else { |
| // Otherwise, use exemplar city name |
| location = _tznames.getExemplarLocationName(tzID); |
| } |
| } else { |
| location = _tznames.getExemplarLocationName(tzID); |
| if (location == null) { |
| // This could happen when the time zone is not associated with a country, |
| // and its ID is not hierarchical, for example, CST6CDT. |
| // We use the canonical ID itself as the location for this case. |
| location = tzID; |
| } |
| } |
| name = formatPattern(Pattern.FALLBACK_FORMAT, location, mzDisplayName); |
| synchronized (this) { // we have to sync the name map and the trie |
| String tmp = _genericPartialLocationNamesMap.putIfAbsent(key.intern(), name.intern()); |
| if (tmp == null) { |
| NameInfo info = new NameInfo(tzID.intern(), |
| isLong ? GenericNameType.LONG : GenericNameType.SHORT); |
| _gnamesTrie.put(name, info); |
| } else { |
| name = tmp; |
| } |
| } |
| return name; |
| } |
| |
| /** |
| * A private class used for storing the name information in the local trie. |
| */ |
| private static class NameInfo { |
| final String tzID; |
| final GenericNameType type; |
| |
| NameInfo(String tzID, GenericNameType type) { |
| this.tzID = tzID; |
| this.type = type; |
| } |
| } |
| |
| /** |
| * A class used for returning the name search result used by |
| * {@link TimeZoneGenericNames#find(String, int, EnumSet)}. |
| */ |
| public static class GenericMatchInfo { |
| final GenericNameType nameType; |
| final String tzID; |
| final int matchLength; |
| final TimeType timeType; |
| |
| private GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength) { |
| this(nameType, tzID, matchLength, TimeType.UNKNOWN); |
| } |
| |
| private GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength, TimeType timeType) { |
| this.nameType = nameType; |
| this.tzID = tzID; |
| this.matchLength = matchLength; |
| this.timeType = timeType; |
| } |
| |
| public GenericNameType nameType() { |
| return nameType; |
| } |
| |
| public String tzID() { |
| return tzID; |
| } |
| |
| public TimeType timeType() { |
| return timeType; |
| } |
| |
| public int matchLength() { |
| return matchLength; |
| } |
| } |
| |
| /** |
| * A private class implementing the search callback interface in |
| * <code>TextTrieMap</code> for collecting match results. |
| */ |
| private static class GenericNameSearchHandler implements ResultHandler<NameInfo> { |
| private EnumSet<GenericNameType> _types; |
| private Collection<GenericMatchInfo> _matches; |
| private int _maxMatchLen; |
| |
| GenericNameSearchHandler(EnumSet<GenericNameType> types) { |
| _types = types; |
| } |
| |
| /* (non-Javadoc) |
| * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator) |
| */ |
| @Override |
| public boolean handlePrefixMatch(int matchLength, Iterator<NameInfo> values) { |
| while (values.hasNext()) { |
| NameInfo info = values.next(); |
| if (_types != null && !_types.contains(info.type)) { |
| continue; |
| } |
| GenericMatchInfo matchInfo = new GenericMatchInfo(info.type, info.tzID, matchLength); |
| if (_matches == null) { |
| _matches = new LinkedList<GenericMatchInfo>(); |
| } |
| _matches.add(matchInfo); |
| if (matchLength > _maxMatchLen) { |
| _maxMatchLen = matchLength; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns the match results |
| * @return the match results |
| */ |
| public Collection<GenericMatchInfo> getMatches() { |
| return _matches; |
| } |
| |
| /** |
| * Returns the maximum match length, or 0 if no match was found |
| * @return the maximum match length |
| */ |
| public int getMaxMatchLen() { |
| return _maxMatchLen; |
| } |
| |
| /** |
| * Resets the match results |
| */ |
| public void resetResults() { |
| _matches = null; |
| _maxMatchLen = 0; |
| } |
| } |
| |
| /** |
| * Returns the best match of time zone display name for the specified types in the |
| * given text at the given offset. |
| * @param text the text |
| * @param start the start offset in the text |
| * @param genericTypes the set of name types. |
| * @return the best matching name info. |
| */ |
| public GenericMatchInfo findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes) { |
| if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { |
| throw new IllegalArgumentException("bad input text or range"); |
| } |
| GenericMatchInfo bestMatch = null; |
| |
| // Find matches in the TimeZoneNames first |
| Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes); |
| if (tznamesMatches != null) { |
| MatchInfo longestMatch = null; |
| for (MatchInfo match : tznamesMatches) { |
| if (longestMatch == null || match.matchLength() > longestMatch.matchLength()) { |
| longestMatch = match; |
| } |
| } |
| if (longestMatch != null) { |
| bestMatch = createGenericMatchInfo(longestMatch); |
| if (bestMatch.matchLength() == (text.length() - start)) { |
| // Full match |
| //return bestMatch; |
| |
| // TODO Some time zone uses a same name for the long standard name |
| // and the location name. When the match is a long standard name, |
| // then we need to check if the name is same with the location name. |
| // This is probably a data error or a design bug. |
| // if (bestMatch.nameType != GenericNameType.LONG || bestMatch.timeType != TimeType.STANDARD) { |
| // return bestMatch; |
| // } |
| |
| // TODO The deprecation of commonlyUsed flag introduced the name |
| // conflict not only for long standard names, but short standard names too. |
| // These short names (found in zh_Hant) should be gone once we clean |
| // up CLDR time zone display name data. Once the short name conflict |
| // problem (with location name) is resolved, we should change the condition |
| // below back to the original one above. -Yoshito (2011-09-14) |
| if (bestMatch.timeType != TimeType.STANDARD) { |
| return bestMatch; |
| } |
| } |
| } |
| } |
| |
| // Find matches in the local trie |
| Collection<GenericMatchInfo> localMatches = findLocal(text, start, genericTypes); |
| if (localMatches != null) { |
| for (GenericMatchInfo match : localMatches) { |
| // TODO See the above TODO. We use match.matchLength() >= bestMatch.matcheLength() |
| // for the reason described above. |
| //if (bestMatch == null || match.matchLength() > bestMatch.matchLength()) { |
| if (bestMatch == null || match.matchLength() >= bestMatch.matchLength()) { |
| bestMatch = match; |
| } |
| } |
| } |
| |
| return bestMatch; |
| } |
| |
| /** |
| * Returns a collection of time zone display name matches for the specified types in the |
| * given text at the given offset. |
| * @param text the text |
| * @param start the start offset in the text |
| * @param genericTypes the set of name types. |
| * @return A collection of match info. |
| */ |
| public Collection<GenericMatchInfo> find(String text, int start, EnumSet<GenericNameType> genericTypes) { |
| if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { |
| throw new IllegalArgumentException("bad input text or range"); |
| } |
| // Find matches in the local trie |
| Collection<GenericMatchInfo> results = findLocal(text, start, genericTypes); |
| |
| // Also find matches in the TimeZoneNames |
| Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes); |
| if (tznamesMatches != null) { |
| // transform matches and append them to local matches |
| for (MatchInfo match : tznamesMatches) { |
| if (results == null) { |
| results = new LinkedList<GenericMatchInfo>(); |
| } |
| results.add(createGenericMatchInfo(match)); |
| } |
| } |
| return results; |
| } |
| |
| /** |
| * Returns a <code>GenericMatchInfo</code> for the given <code>MatchInfo</code>. |
| * @param matchInfo the MatchInfo |
| * @return A GenericMatchInfo |
| */ |
| private GenericMatchInfo createGenericMatchInfo(MatchInfo matchInfo) { |
| GenericNameType nameType = null; |
| TimeType timeType = TimeType.UNKNOWN; |
| switch (matchInfo.nameType()) { |
| case LONG_STANDARD: |
| nameType = GenericNameType.LONG; |
| timeType = TimeType.STANDARD; |
| break; |
| case LONG_GENERIC: |
| nameType = GenericNameType.LONG; |
| break; |
| case SHORT_STANDARD: |
| nameType = GenericNameType.SHORT; |
| timeType = TimeType.STANDARD; |
| break; |
| case SHORT_GENERIC: |
| nameType = GenericNameType.SHORT; |
| break; |
| default: |
| throw new IllegalArgumentException("Unexpected MatchInfo name type - " + matchInfo.nameType()); |
| } |
| |
| String tzID = matchInfo.tzID(); |
| if (tzID == null) { |
| String mzID = matchInfo.mzID(); |
| assert(mzID != null); |
| tzID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); |
| } |
| assert(tzID != null); |
| |
| GenericMatchInfo gmatch = new GenericMatchInfo(nameType, tzID, matchInfo.matchLength(), timeType); |
| |
| return gmatch; |
| } |
| |
| /** |
| * Returns a collection of time zone display name matches for the specified types in the |
| * given text at the given offset. This method only finds matches from the TimeZoneNames |
| * used by this object. |
| * @param text the text |
| * @param start the start offset in the text |
| * @param types the set of name types. |
| * @return A collection of match info. |
| */ |
| private Collection<MatchInfo> findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types) { |
| Collection<MatchInfo> tznamesMatches = null; |
| |
| // Check if the target name type is really in the TimeZoneNames |
| EnumSet<NameType> nameTypes = EnumSet.noneOf(NameType.class); |
| if (types.contains(GenericNameType.LONG)) { |
| nameTypes.add(NameType.LONG_GENERIC); |
| nameTypes.add(NameType.LONG_STANDARD); |
| } |
| if (types.contains(GenericNameType.SHORT)) { |
| nameTypes.add(NameType.SHORT_GENERIC); |
| nameTypes.add(NameType.SHORT_STANDARD); |
| } |
| |
| if (!nameTypes.isEmpty()) { |
| // Find matches in the TimeZoneNames |
| tznamesMatches = _tznames.find(text, start, nameTypes); |
| } |
| return tznamesMatches; |
| } |
| |
| /** |
| * Returns a collection of time zone display name matches for the specified types in the |
| * given text at the given offset. This method only finds matches from the local trie, |
| * that contains 1) generic location names and 2) long/short generic partial location names, |
| * used by this object. |
| * @param text the text |
| * @param start the start offset in the text |
| * @param types the set of name types. |
| * @return A collection of match info. |
| */ |
| private synchronized Collection<GenericMatchInfo> findLocal(String text, int start, EnumSet<GenericNameType> types) { |
| GenericNameSearchHandler handler = new GenericNameSearchHandler(types); |
| _gnamesTrie.find(text, start, handler); |
| if (handler.getMaxMatchLen() == (text.length() - start) || _gnamesTrieFullyLoaded) { |
| // perfect match |
| return handler.getMatches(); |
| } |
| |
| // All names are not yet loaded into the local trie. |
| // Load all available names into the trie. This could be very heavy. |
| |
| Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); |
| for (String tzID : tzIDs) { |
| loadStrings(tzID); |
| } |
| _gnamesTrieFullyLoaded = true; |
| |
| // now, try it again |
| handler.resetResults(); |
| _gnamesTrie.find(text, start, handler); |
| return handler.getMatches(); |
| } |
| |
| /** |
| * <code>TimeZoneGenericNames</code> cache implementation. |
| */ |
| private static class Cache extends SoftCache<String, TimeZoneGenericNames, ULocale> { |
| |
| /* (non-Javadoc) |
| * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) |
| */ |
| @Override |
| protected TimeZoneGenericNames createInstance(String key, ULocale data) { |
| return new TimeZoneGenericNames(data).freeze(); |
| } |
| |
| } |
| |
| /* |
| * The custom deserialization method. |
| * This implementation only read locale used by the object. |
| */ |
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { |
| in.defaultReadObject(); |
| init(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isFrozen() { |
| return _frozen; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public TimeZoneGenericNames freeze() { |
| _frozen = true; |
| return this; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public TimeZoneGenericNames cloneAsThawed() { |
| TimeZoneGenericNames copy = null; |
| try { |
| copy = (TimeZoneGenericNames)super.clone(); |
| copy._frozen = false; |
| } catch (Throwable t) { |
| // This should never happen |
| } |
| return copy; |
| } |
| } |