/* | |
******************************************************************************* | |
* Copyright (C) 2011, 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.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> { | |
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. | |
REGION_FORMAT("regionFormat", "({0})"), | |
// 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 ULocale _locale; | |
private TimeZoneNames _tznames; | |
private transient 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; | |
} | |
String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID); | |
if (countryCode != null) { | |
String country = getLocaleDisplayNames().regionDisplayName(countryCode); | |
if (ZoneMeta.getSingleCountry(canonicalTzID) != null) { | |
// If the zone is only one zone in the country, do not add city | |
name = formatPattern(Pattern.REGION_FORMAT, country); | |
} else { | |
// getExemplarLocationName should return non-empty String | |
// if the time zone is associated with a location | |
String city = _tznames.getExemplarLocationName(canonicalTzID); | |
name = formatPattern(Pattern.FALLBACK_REGION_FORMAT, city, country); | |
} | |
} | |
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(); | |
info.tzID = canonicalTzID; | |
info.type = 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_COMMONLY_USED; | |
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.getTimeZone(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( | |
ICUResourceBundle.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.getSingleCountry(tzID); | |
if (countryCode != null) { | |
location = getLocaleDisplayNames().regionDisplayName(countryCode); | |
} 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(); | |
info.tzID = tzID.intern(); | |
info.type = 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 { | |
String tzID; | |
GenericNameType type; | |
} | |
/** | |
* A class used for returning the name search result used by | |
* {@link TimeZoneGenericNames#find(String, int, EnumSet)}. | |
*/ | |
public static class GenericMatchInfo { | |
GenericNameType nameType; | |
String tzID; | |
int matchLength; | |
TimeType timeType = TimeType.UNKNOWN; | |
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) | |
*/ | |
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(); | |
matchInfo.tzID = info.tzID; | |
matchInfo.nameType = info.type; | |
matchInfo.matchLength = matchLength; | |
//matchInfo.timeType = TimeType.UNKNOWN; | |
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; | |
} | |
} | |
} | |
} | |
// 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_COMMONLY_USED: | |
nameType = GenericNameType.SHORT; | |
timeType = TimeType.STANDARD; | |
break; | |
case SHORT_GENERIC: | |
nameType = GenericNameType.SHORT; | |
break; | |
} | |
assert(nameType != null); | |
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(); | |
gmatch.nameType = nameType; | |
gmatch.tzID = tzID; | |
gmatch.matchLength = matchInfo.matchLength(); | |
gmatch.timeType = 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_COMMONLY_USED); | |
} | |
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} | |
*/ | |
public boolean isFrozen() { | |
return _frozen; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public TimeZoneGenericNames freeze() { | |
_frozen = true; | |
return this; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public TimeZoneGenericNames cloneAsThawed() { | |
TimeZoneGenericNames copy = null; | |
try { | |
copy = (TimeZoneGenericNames)super.clone(); | |
copy._frozen = false; | |
} catch (Throwable t) { | |
// This should never happen | |
} | |
return copy; | |
} | |
} |