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