1/* 2 ******************************************************************************* 3 * Copyright (C) 2014-2015, International Business Machines Corporation and 4 * others. All Rights Reserved. 5 ******************************************************************************* 6 */ 7package com.ibm.icu.impl; 8 9import java.util.Collection; 10import java.util.Collections; 11import java.util.EnumSet; 12import java.util.Iterator; 13import java.util.LinkedList; 14import java.util.MissingResourceException; 15import java.util.Set; 16import java.util.concurrent.ConcurrentHashMap; 17 18import com.ibm.icu.impl.TextTrieMap.ResultHandler; 19import com.ibm.icu.text.TimeZoneNames; 20import com.ibm.icu.util.ULocale; 21import com.ibm.icu.util.UResourceBundle; 22 23/** 24 * Yet another TimeZoneNames implementation based on the tz database. 25 * This implementation contains only tz abbreviations (short standard 26 * and daylight names) for each metazone. 27 * 28 * The data file $ICU4C_ROOT/source/data/zone/tzdbNames.txt contains 29 * the metazone - abbreviations mapping data (manually edited). 30 * 31 * Note: The abbreviations in the tz database are not necessarily 32 * unique. For example, parsing abbreviation "IST" is ambiguous 33 * (can be parsed as India Standard Time or Israel Standard Time). 34 * The data file (tzdbNames.txt) contains regional mapping, and 35 * the locale in the constructor is used as a hint for resolving 36 * these ambiguous names. 37 */ 38public class TZDBTimeZoneNames extends TimeZoneNames { 39 private static final long serialVersionUID = 1L; 40 41 private static final ConcurrentHashMap<String, TZDBNames> TZDB_NAMES_MAP = 42 new ConcurrentHashMap<String, TZDBNames>(); 43 44 private static volatile TextTrieMap<TZDBNameInfo> TZDB_NAMES_TRIE = null; 45 46 private static final ICUResourceBundle ZONESTRINGS; 47 static { 48 UResourceBundle bundle = ICUResourceBundle 49 .getBundleInstance(ICUResourceBundle.ICU_ZONE_BASE_NAME, "tzdbNames"); 50 ZONESTRINGS = (ICUResourceBundle)bundle.get("zoneStrings"); 51 } 52 53 private ULocale _locale; 54 private transient volatile String _region; 55 56 public TZDBTimeZoneNames(ULocale loc) { 57 _locale = loc; 58 } 59 60 /* (non-Javadoc) 61 * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs() 62 */ 63 @Override 64 public Set<String> getAvailableMetaZoneIDs() { 65 return TimeZoneNamesImpl._getAvailableMetaZoneIDs(); 66 } 67 68 /* (non-Javadoc) 69 * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs(java.lang.String) 70 */ 71 @Override 72 public Set<String> getAvailableMetaZoneIDs(String tzID) { 73 return TimeZoneNamesImpl._getAvailableMetaZoneIDs(tzID); 74 } 75 76 /* (non-Javadoc) 77 * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneID(java.lang.String, long) 78 */ 79 @Override 80 public String getMetaZoneID(String tzID, long date) { 81 return TimeZoneNamesImpl._getMetaZoneID(tzID, date); 82 } 83 84 /* (non-Javadoc) 85 * @see com.ibm.icu.text.TimeZoneNames#getReferenceZoneID(java.lang.String, java.lang.String) 86 */ 87 @Override 88 public String getReferenceZoneID(String mzID, String region) { 89 return TimeZoneNamesImpl._getReferenceZoneID(mzID, region); 90 } 91 92 /* (non-Javadoc) 93 * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneDisplayName(java.lang.String, 94 * com.ibm.icu.text.TimeZoneNames.NameType) 95 */ 96 @Override 97 public String getMetaZoneDisplayName(String mzID, NameType type) { 98 if (mzID == null || mzID.length() == 0 || 99 (type != NameType.SHORT_STANDARD && type != NameType.SHORT_DAYLIGHT)) { 100 return null; 101 } 102 return getMetaZoneNames(mzID).getName(type); 103 } 104 105 /* (non-Javadoc) 106 * @see com.ibm.icu.text.TimeZoneNames#getTimeZoneDisplayName(java.lang.String, 107 * com.ibm.icu.text.TimeZoneNames.NameType) 108 */ 109 @Override 110 public String getTimeZoneDisplayName(String tzID, NameType type) { 111 // No abbreviations associated a zone directly for now. 112 return null; 113 } 114 115// /* (non-Javadoc) 116// * @see com.ibm.icu.text.TimeZoneNames#getExemplarLocationName(java.lang.String) 117// */ 118// public String getExemplarLocationName(String tzID) { 119// return super.getExemplarLocationName(tzID); 120// } 121 122 /* (non-Javadoc) 123 * @see com.ibm.icu.text.TimeZoneNames#find(java.lang.CharSequence, int, java.util.EnumSet) 124 */ 125 @Override 126 public Collection<MatchInfo> find(CharSequence text, int start, EnumSet<NameType> nameTypes) { 127 if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { 128 throw new IllegalArgumentException("bad input text or range"); 129 } 130 131 prepareFind(); 132 TZDBNameSearchHandler handler = new TZDBNameSearchHandler(nameTypes, getTargetRegion()); 133 TZDB_NAMES_TRIE.find(text, start, handler); 134 return handler.getMatches(); 135 } 136 137 private static class TZDBNames { 138 public static final TZDBNames EMPTY_TZDBNAMES = new TZDBNames(null, null); 139 140 private String[] _names; 141 private String[] _parseRegions; 142 private static final String[] KEYS = {"ss", "sd"}; 143 144 private TZDBNames(String[] names, String[] parseRegions) { 145 _names = names; 146 _parseRegions = parseRegions; 147 } 148 149 static TZDBNames getInstance(ICUResourceBundle zoneStrings, String key) { 150 if (zoneStrings == null || key == null || key.length() == 0) { 151 return EMPTY_TZDBNAMES; 152 } 153 154 ICUResourceBundle table = null; 155 try { 156 table = (ICUResourceBundle)zoneStrings.get(key); 157 } catch (MissingResourceException e) { 158 return EMPTY_TZDBNAMES; 159 } 160 161 boolean isEmpty = true; 162 String[] names = new String[KEYS.length]; 163 for (int i = 0; i < names.length; i++) { 164 try { 165 names[i] = table.getString(KEYS[i]); 166 isEmpty = false; 167 } catch (MissingResourceException e) { 168 names[i] = null; 169 } 170 } 171 172 if (isEmpty) { 173 return EMPTY_TZDBNAMES; 174 } 175 176 String[] parseRegions = null; 177 try { 178 ICUResourceBundle regionsRes = (ICUResourceBundle)table.get("parseRegions"); 179 if (regionsRes.getType() == UResourceBundle.STRING) { 180 parseRegions = new String[1]; 181 parseRegions[0] = regionsRes.getString(); 182 } else if (regionsRes.getType() == UResourceBundle.ARRAY) { 183 parseRegions = regionsRes.getStringArray(); 184 } 185 } catch (MissingResourceException e) { 186 // fall through 187 } 188 189 return new TZDBNames(names, parseRegions); 190 } 191 192 String getName(NameType type) { 193 if (_names == null) { 194 return null; 195 } 196 String name = null; 197 switch (type) { 198 case SHORT_STANDARD: 199 name = _names[0]; 200 break; 201 case SHORT_DAYLIGHT: 202 name = _names[1]; 203 break; 204 } 205 206 return name; 207 } 208 209 String[] getParseRegions() { 210 return _parseRegions; 211 } 212 } 213 214 private static class TZDBNameInfo { 215 String mzID; 216 NameType type; 217 boolean ambiguousType; 218 String[] parseRegions; 219 } 220 221 private static class TZDBNameSearchHandler implements ResultHandler<TZDBNameInfo> { 222 private EnumSet<NameType> _nameTypes; 223 private Collection<MatchInfo> _matches; 224 private String _region; 225 226 TZDBNameSearchHandler(EnumSet<NameType> nameTypes, String region) { 227 _nameTypes = nameTypes; 228 assert region != null; 229 _region = region; 230 } 231 232 /* (non-Javadoc) 233 * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, 234 * java.util.Iterator) 235 */ 236 public boolean handlePrefixMatch(int matchLength, Iterator<TZDBNameInfo> values) { 237 TZDBNameInfo match = null; 238 TZDBNameInfo defaultRegionMatch = null; 239 240 while (values.hasNext()) { 241 TZDBNameInfo ninfo = values.next(); 242 243 if (_nameTypes != null && !_nameTypes.contains(ninfo.type)) { 244 continue; 245 } 246 247 // Some tz database abbreviations are ambiguous. For example, 248 // CST means either Central Standard Time or China Standard Time. 249 // Unlike CLDR time zone display names, this implementation 250 // does not use unique names. And TimeZoneFormat does not expect 251 // multiple results returned for the same time zone type. 252 // For this reason, this implementation resolve one among same 253 // zone type with a same name at this level. 254 if (ninfo.parseRegions == null) { 255 // parseRegions == null means this is the default metazone 256 // mapping for the abbreviation. 257 if (defaultRegionMatch == null) { 258 match = defaultRegionMatch = ninfo; 259 } 260 } else { 261 boolean matchRegion = false; 262 // non-default metazone mapping for an abbreviation 263 // comes with applicable regions. For example, the default 264 // metazone mapping for "CST" is America_Central, 265 // but if region is one of CN/MO/TW, "CST" is parsed 266 // as metazone China (China Standard Time). 267 for (String region : ninfo.parseRegions) { 268 if (_region.equals(region)) { 269 match = ninfo; 270 matchRegion = true; 271 break; 272 } 273 } 274 if (matchRegion) { 275 break; 276 } 277 if (match == null) { 278 match = ninfo; 279 } 280 } 281 } 282 283 if (match != null) { 284 NameType ntype = match.type; 285 // Note: Workaround for duplicated standard/daylight names 286 // The tz database contains a few zones sharing a 287 // same name for both standard time and daylight saving 288 // time. For example, Australia/Sydney observes DST, 289 // but "EST" is used for both standard and daylight. 290 // When both SHORT_STANDARD and SHORT_DAYLIGHT are included 291 // in the find operation, we cannot tell which one was 292 // actually matched. 293 // TimeZoneFormat#parse returns a matched name type (standard 294 // or daylight) and DateFormat implementation uses the info to 295 // to adjust actual time. To avoid false type information, 296 // this implementation replaces the name type with SHORT_GENERIC. 297 if (match.ambiguousType 298 && (ntype == NameType.SHORT_STANDARD || ntype == NameType.SHORT_DAYLIGHT) 299 && _nameTypes.contains(NameType.SHORT_STANDARD) 300 && _nameTypes.contains(NameType.SHORT_DAYLIGHT)) { 301 ntype = NameType.SHORT_GENERIC; 302 } 303 MatchInfo minfo = new MatchInfo(ntype, null, match.mzID, matchLength); 304 if (_matches == null) { 305 _matches = new LinkedList<MatchInfo>(); 306 } 307 _matches.add(minfo); 308 } 309 310 return true; 311 } 312 313 /** 314 * Returns the match results 315 * @return the match results 316 */ 317 public Collection<MatchInfo> getMatches() { 318 if (_matches == null) { 319 return Collections.emptyList(); 320 } 321 return _matches; 322 } 323 } 324 325 private static TZDBNames getMetaZoneNames(String mzID) { 326 TZDBNames names = TZDB_NAMES_MAP.get(mzID); 327 if (names == null) { 328 names = TZDBNames.getInstance(ZONESTRINGS, "meta:" + mzID); 329 mzID = mzID.intern(); 330 TZDBNames tmpNames = TZDB_NAMES_MAP.putIfAbsent(mzID, names); 331 names = (tmpNames == null) ? names : tmpNames; 332 } 333 return names; 334 } 335 336 private static void prepareFind() { 337 if (TZDB_NAMES_TRIE == null) { 338 synchronized(TZDBTimeZoneNames.class) { 339 if (TZDB_NAMES_TRIE == null) { 340 // loading all names into trie 341 TextTrieMap<TZDBNameInfo> trie = new TextTrieMap<TZDBNameInfo>(true); 342 Set<String> mzIDs = TimeZoneNamesImpl._getAvailableMetaZoneIDs(); 343 for (String mzID : mzIDs) { 344 TZDBNames names = getMetaZoneNames(mzID); 345 String std = names.getName(NameType.SHORT_STANDARD); 346 String dst = names.getName(NameType.SHORT_DAYLIGHT); 347 if (std == null && dst == null) { 348 continue; 349 } 350 String[] parseRegions = names.getParseRegions(); 351 mzID = mzID.intern(); 352 353 // The tz database contains a few zones sharing a 354 // same name for both standard time and daylight saving 355 // time. For example, Australia/Sydney observes DST, 356 // but "EST" is used for both standard and daylight. 357 // we need to store the information for later processing. 358 boolean ambiguousType = (std != null && dst != null && std.equals(dst)); 359 360 if (std != null) { 361 TZDBNameInfo stdInf = new TZDBNameInfo(); 362 stdInf.mzID = mzID; 363 stdInf.type = NameType.SHORT_STANDARD; 364 stdInf.ambiguousType = ambiguousType; 365 stdInf.parseRegions = parseRegions; 366 trie.put(std, stdInf); 367 } 368 if (dst != null) { 369 TZDBNameInfo dstInf = new TZDBNameInfo(); 370 dstInf.mzID = mzID; 371 dstInf.type = NameType.SHORT_DAYLIGHT; 372 dstInf.ambiguousType = ambiguousType; 373 dstInf.parseRegions = parseRegions; 374 trie.put(dst, dstInf); 375 } 376 } 377 TZDB_NAMES_TRIE = trie; 378 } 379 } 380 } 381 } 382 383 private String getTargetRegion() { 384 if (_region == null) { 385 String region = _locale.getCountry(); 386 if (region.length() == 0) { 387 ULocale tmp = ULocale.addLikelySubtags(_locale); 388 region = tmp.getCountry(); 389 if (region.length() == 0) { 390 region = "001"; 391 } 392 } 393 _region = region; 394 } 395 return _region; 396 } 397} 398