blob: 22a8dd47f63db458be2fcded22ea76fc44023f46 [file] [log] [blame]
/*
*******************************************************************************
* Copyright (C) 2014, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.impl;
import java.util.Collection;
import java.util.Collections;
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.TimeZoneNames;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* Yet another TimeZoneNames implementation based on the tz database.
* This implementation contains only tz abbreviations (short standard
* and daylight names) for each metazone.
*
* The data file $ICU4C_ROOT/source/data/zone/tzdbNames.txt contains
* the metazone - abbreviations mapping data (manually edited).
*
* Note: The abbreviations in the tz database are not necessarily
* unique. For example, parsing abbreviation "IST" is ambiguous
* (can be parsed as India Standard Time or Israel Standard Time).
* The data file (tzdbNames.txt) contains regional mapping, and
* the locale in the constructor is used as a hint for resolving
* these ambiguous names.
*/
public class TZDBTimeZoneNames extends TimeZoneNames {
private static final long serialVersionUID = 1L;
private static final ConcurrentHashMap<String, TZDBNames> TZDB_NAMES_MAP =
new ConcurrentHashMap<String, TZDBNames>();
private static volatile TextTrieMap<TZDBNameInfo> TZDB_NAMES_TRIE = null;
private static final ICUResourceBundle ZONESTRINGS;
static {
UResourceBundle bundle = ICUResourceBundle
.getBundleInstance(ICUResourceBundle.ICU_ZONE_BASE_NAME, "tzdbNames");
ZONESTRINGS = (ICUResourceBundle)bundle.get("zoneStrings");
}
private ULocale _locale;
private transient volatile String _region;
public TZDBTimeZoneNames(ULocale loc) {
_locale = loc;
}
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs()
*/
@Override
public Set<String> getAvailableMetaZoneIDs() {
return TimeZoneNamesImpl._getAvailableMetaZoneIDs();
}
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs(java.lang.String)
*/
@Override
public Set<String> getAvailableMetaZoneIDs(String tzID) {
return TimeZoneNamesImpl._getAvailableMetaZoneIDs(tzID);
}
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#getMetaZoneID(java.lang.String, long)
*/
@Override
public String getMetaZoneID(String tzID, long date) {
return TimeZoneNamesImpl._getMetaZoneID(tzID, date);
}
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#getReferenceZoneID(java.lang.String, java.lang.String)
*/
@Override
public String getReferenceZoneID(String mzID, String region) {
return TimeZoneNamesImpl._getReferenceZoneID(mzID, region);
}
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#getMetaZoneDisplayName(java.lang.String,
* com.ibm.icu.text.TimeZoneNames.NameType)
*/
@Override
public String getMetaZoneDisplayName(String mzID, NameType type) {
if (mzID == null || mzID.length() == 0 ||
(type != NameType.SHORT_STANDARD && type != NameType.SHORT_DAYLIGHT)) {
return null;
}
return getMetaZoneNames(mzID).getName(type);
}
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#getTimeZoneDisplayName(java.lang.String,
* com.ibm.icu.text.TimeZoneNames.NameType)
*/
@Override
public String getTimeZoneDisplayName(String tzID, NameType type) {
// No abbreviations associated a zone directly for now.
return null;
}
// /* (non-Javadoc)
// * @see com.ibm.icu.text.TimeZoneNames#getExemplarLocationName(java.lang.String)
// */
// public String getExemplarLocationName(String tzID) {
// return super.getExemplarLocationName(tzID);
// }
/* (non-Javadoc)
* @see com.ibm.icu.text.TimeZoneNames#find(java.lang.CharSequence, int, java.util.EnumSet)
*/
@Override
public Collection<MatchInfo> find(CharSequence text, int start, EnumSet<NameType> nameTypes) {
if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
throw new IllegalArgumentException("bad input text or range");
}
prepareFind();
TZDBNameSearchHandler handler = new TZDBNameSearchHandler(nameTypes, getTargetRegion());
TZDB_NAMES_TRIE.find(text, start, handler);
return handler.getMatches();
}
private static class TZDBNames {
public static final TZDBNames EMPTY_TZDBNAMES = new TZDBNames(null, null);
private String[] _names;
private String[] _parseRegions;
private static final String[] KEYS = {"ss", "sd"};
private TZDBNames(String[] names, String[] parseRegions) {
_names = names;
_parseRegions = parseRegions;
}
static TZDBNames getInstance(ICUResourceBundle zoneStrings, String key) {
if (zoneStrings == null || key == null || key.length() == 0) {
return EMPTY_TZDBNAMES;
}
ICUResourceBundle table = null;
try {
table = (ICUResourceBundle)zoneStrings.get(key);
} catch (MissingResourceException e) {
return EMPTY_TZDBNAMES;
}
boolean isEmpty = true;
String[] names = new String[KEYS.length];
for (int i = 0; i < names.length; i++) {
try {
names[i] = table.getString(KEYS[i]);
isEmpty = false;
} catch (MissingResourceException e) {
names[i] = null;
}
}
if (isEmpty) {
return EMPTY_TZDBNAMES;
}
String[] parseRegions = null;
try {
ICUResourceBundle regionsRes = (ICUResourceBundle)table.get("parseRegions");
if (regionsRes.getType() == UResourceBundle.STRING) {
parseRegions = new String[1];
parseRegions[0] = regionsRes.getString();
} else if (regionsRes.getType() == UResourceBundle.ARRAY) {
parseRegions = regionsRes.getStringArray();
}
} catch (MissingResourceException e) {
// fall through
}
return new TZDBNames(names, parseRegions);
}
String getName(NameType type) {
if (_names == null) {
return null;
}
String name = null;
switch (type) {
case SHORT_STANDARD:
name = _names[0];
break;
case SHORT_DAYLIGHT:
name = _names[1];
break;
}
return name;
}
String[] getParseRegions() {
return _parseRegions;
}
}
private static class TZDBNameInfo {
String mzID;
NameType type;
boolean ambiguousType;
String[] parseRegions;
}
private static class TZDBNameSearchHandler implements ResultHandler<TZDBNameInfo> {
private EnumSet<NameType> _nameTypes;
private Collection<MatchInfo> _matches;
private String _region;
TZDBNameSearchHandler(EnumSet<NameType> nameTypes, String region) {
_nameTypes = nameTypes;
assert region != null;
_region = region;
}
/* (non-Javadoc)
* @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int,
* java.util.Iterator)
*/
public boolean handlePrefixMatch(int matchLength, Iterator<TZDBNameInfo> values) {
TZDBNameInfo match = null;
TZDBNameInfo defaultRegionMatch = null;
while (values.hasNext()) {
TZDBNameInfo ninfo = values.next();
if (_nameTypes != null && !_nameTypes.contains(ninfo.type)) {
continue;
}
// Some tz database abbreviations are ambiguous. For example,
// CST means either Central Standard Time or China Standard Time.
// Unlike CLDR time zone display names, this implementation
// does not use unique names. And TimeZoneFormat does not expect
// multiple results returned for the same time zone type.
// For this reason, this implementation resolve one among same
// zone type with a same name at this level.
if (ninfo.parseRegions == null) {
// parseRegions == null means this is the default metazone
// mapping for the abbreviation.
if (defaultRegionMatch == null) {
match = defaultRegionMatch = ninfo;
}
} else {
boolean matchRegion = false;
// non-default metazone mapping for an abbreviation
// comes with applicable regions. For example, the default
// metazone mapping for "CST" is America_Central,
// but if region is one of CN/MO/TW, "CST" is parsed
// as metazone China (China Standard Time).
for (String region : ninfo.parseRegions) {
if (_region.equals(region)) {
match = ninfo;
matchRegion = true;
break;
}
}
if (matchRegion) {
break;
}
if (match == null) {
match = ninfo;
}
}
}
if (match != null) {
NameType ntype = match.type;
// Note: Workaround for duplicated standard/daylight names
// The tz database contains a few zones sharing a
// same name for both standard time and daylight saving
// time. For example, Australia/Sydney observes DST,
// but "EST" is used for both standard and daylight.
// When both SHORT_STANDARD and SHORT_DAYLIGHT are included
// in the find operation, we cannot tell which one was
// actually matched.
// TimeZoneFormat#parse returns a matched name type (standard
// or daylight) and DateFormat implementation uses the info to
// to adjust actual time. To avoid false type information,
// this implementation replaces the name type with SHORT_GENERIC.
if (match.ambiguousType
&& (ntype == NameType.SHORT_STANDARD || ntype == NameType.SHORT_DAYLIGHT)
&& _nameTypes.contains(NameType.SHORT_STANDARD)
&& _nameTypes.contains(NameType.SHORT_DAYLIGHT)) {
ntype = NameType.SHORT_GENERIC;
}
MatchInfo minfo = new MatchInfo(ntype, null, match.mzID, matchLength);
if (_matches == null) {
_matches = new LinkedList<MatchInfo>();
}
_matches.add(minfo);
}
return true;
}
/**
* Returns the match results
* @return the match results
*/
public Collection<MatchInfo> getMatches() {
if (_matches == null) {
return Collections.emptyList();
}
return _matches;
}
}
private static TZDBNames getMetaZoneNames(String mzID) {
TZDBNames names = TZDB_NAMES_MAP.get(mzID);
if (names == null) {
names = TZDBNames.getInstance(ZONESTRINGS, "meta:" + mzID);
mzID = mzID.intern();
TZDBNames tmpNames = TZDB_NAMES_MAP.putIfAbsent(mzID, names);
names = (tmpNames == null) ? names : tmpNames;
}
return names;
}
private static void prepareFind() {
if (TZDB_NAMES_TRIE == null) {
synchronized(TZDBTimeZoneNames.class) {
if (TZDB_NAMES_TRIE == null) {
// loading all names into trie
TZDB_NAMES_TRIE = new TextTrieMap<TZDBNameInfo>(true);
Set<String> mzIDs = TimeZoneNamesImpl._getAvailableMetaZoneIDs();
for (String mzID : mzIDs) {
TZDBNames names = getMetaZoneNames(mzID);
String std = names.getName(NameType.SHORT_STANDARD);
String dst = names.getName(NameType.SHORT_DAYLIGHT);
if (std == null && dst == null) {
continue;
}
String[] parseRegions = names.getParseRegions();
mzID = mzID.intern();
// The tz database contains a few zones sharing a
// same name for both standard time and daylight saving
// time. For example, Australia/Sydney observes DST,
// but "EST" is used for both standard and daylight.
// we need to store the information for later processing.
boolean ambiguousType = (std != null && dst != null && std.equals(dst));
if (std != null) {
TZDBNameInfo stdInf = new TZDBNameInfo();
stdInf.mzID = mzID;
stdInf.type = NameType.SHORT_STANDARD;
stdInf.ambiguousType = ambiguousType;
stdInf.parseRegions = parseRegions;
TZDB_NAMES_TRIE.put(std, stdInf);
}
if (dst != null) {
TZDBNameInfo dstInf = new TZDBNameInfo();
dstInf.mzID = mzID;
dstInf.type = NameType.SHORT_DAYLIGHT;
dstInf.ambiguousType = ambiguousType;
dstInf.parseRegions = parseRegions;
TZDB_NAMES_TRIE.put(dst, dstInf);
}
}
}
}
}
}
private String getTargetRegion() {
if (_region == null) {
String region = _locale.getCountry();
if (region.length() == 0) {
ULocale tmp = ULocale.addLikelySubtags(_locale);
region = tmp.getCountry();
if (region.length() == 0) {
region = "001";
}
}
_region = region;
}
return _region;
}
}