1/* 2 ******************************************************************************* 3 * Copyright (C) 2011-2014, International Business Machines Corporation and * 4 * others. All Rights Reserved. * 5 ******************************************************************************* 6 */ 7package com.ibm.icu.impl; 8 9import java.io.IOException; 10import java.io.ObjectInputStream; 11import java.io.Serializable; 12import java.lang.ref.WeakReference; 13import java.text.MessageFormat; 14import java.util.Collection; 15import java.util.EnumSet; 16import java.util.Iterator; 17import java.util.LinkedList; 18import java.util.MissingResourceException; 19import java.util.Set; 20import java.util.concurrent.ConcurrentHashMap; 21 22import com.ibm.icu.impl.TextTrieMap.ResultHandler; 23import com.ibm.icu.text.LocaleDisplayNames; 24import com.ibm.icu.text.TimeZoneFormat.TimeType; 25import com.ibm.icu.text.TimeZoneNames; 26import com.ibm.icu.text.TimeZoneNames.MatchInfo; 27import com.ibm.icu.text.TimeZoneNames.NameType; 28import com.ibm.icu.util.BasicTimeZone; 29import com.ibm.icu.util.Freezable; 30import com.ibm.icu.util.Output; 31import com.ibm.icu.util.TimeZone; 32import com.ibm.icu.util.TimeZone.SystemTimeZoneType; 33import com.ibm.icu.util.TimeZoneTransition; 34import com.ibm.icu.util.ULocale; 35 36/** 37 * This class interact with TimeZoneNames and LocaleDisplayNames 38 * to format and parse time zone's generic display names. 39 * It is not recommended to use this class directly, instead 40 * use com.ibm.icu.text.TimeZoneFormat. 41 */ 42public class TimeZoneGenericNames implements Serializable, Freezable<TimeZoneGenericNames> { 43 44 // Note: This class implements Serializable, but we no longer serialize instance of 45 // TimeZoneGenericNames in ICU 49. ICU 4.8 com.ibm.icu.text.TimeZoneFormat used to 46 // serialize TimeZoneGenericNames field. TimeZoneFormat no longer read TimeZoneGenericNames 47 // field, we have to keep TimeZoneGenericNames Serializable. Otherwise it fails to read 48 // (unused) TimeZoneGenericNames serialized data. 49 50 private static final long serialVersionUID = 2729910342063468417L; 51 52 /** 53 * Generic name type enum 54 */ 55 public enum GenericNameType { 56 LOCATION ("LONG", "SHORT"), 57 LONG (), 58 SHORT (); 59 60 String[] _fallbackTypeOf; 61 GenericNameType(String... fallbackTypeOf) { 62 _fallbackTypeOf = fallbackTypeOf; 63 } 64 65 public boolean isFallbackTypeOf(GenericNameType type) { 66 String typeStr = type.toString(); 67 for (String t : _fallbackTypeOf) { 68 if (t.equals(typeStr)) { 69 return true; 70 } 71 } 72 return false; 73 } 74 } 75 76 /** 77 * Format pattern enum used for composing location and partial location names 78 */ 79 public enum Pattern { 80 // The format pattern such as "{0} Time", where {0} is the country or city. 81 REGION_FORMAT("regionFormat", "({0})"), 82 83 // Note: FALLBACK_REGION_FORMAT is no longer used since ICU 50/CLDR 22.1 84 // The format pattern such as "{1} Time ({0})", where {1} is the country and {0} is a city. 85 //FALLBACK_REGION_FORMAT("fallbackRegionFormat", "{1} ({0})"), 86 87 // The format pattern such as "{1} ({0})", where {1} is the metazone, and {0} is the country or city. 88 FALLBACK_FORMAT("fallbackFormat", "{1} ({0})"); 89 90 String _key; 91 String _defaultVal; 92 93 Pattern(String key, String defaultVal) { 94 _key = key; 95 _defaultVal = defaultVal; 96 } 97 98 String key() { 99 return _key; 100 } 101 102 String defaultValue() { 103 return _defaultVal; 104 } 105 } 106 107 private ULocale _locale; 108 private TimeZoneNames _tznames; 109 110 private transient volatile boolean _frozen; 111 private transient String _region; 112 private transient WeakReference<LocaleDisplayNames> _localeDisplayNamesRef; 113 private transient MessageFormat[] _patternFormatters; 114 115 private transient ConcurrentHashMap<String, String> _genericLocationNamesMap; 116 private transient ConcurrentHashMap<String, String> _genericPartialLocationNamesMap; 117 private transient TextTrieMap<NameInfo> _gnamesTrie; 118 private transient boolean _gnamesTrieFullyLoaded; 119 120 private static Cache GENERIC_NAMES_CACHE = new Cache(); 121 122 // Window size used for DST check for a zone in a metazone (about a half year) 123 private static final long DST_CHECK_RANGE = 184L*(24*60*60*1000); 124 125 private static final NameType[] GENERIC_NON_LOCATION_TYPES = 126 {NameType.LONG_GENERIC, NameType.SHORT_GENERIC}; 127 128 129 /** 130 * Constructs a <code>TimeZoneGenericNames</code> with the given locale 131 * and the <code>TimeZoneNames</code>. 132 * @param locale the locale 133 * @param tznames the TimeZoneNames 134 */ 135 public TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames) { 136 _locale = locale; 137 _tznames = tznames; 138 init(); 139 } 140 141 /** 142 * Private method initializing the instance of <code>TimeZoneGenericName</code>. 143 * This method should be called from a constructor and readObject. 144 */ 145 private void init() { 146 if (_tznames == null) { 147 _tznames = TimeZoneNames.getInstance(_locale); 148 } 149 _genericLocationNamesMap = new ConcurrentHashMap<String, String>(); 150 _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>(); 151 152 _gnamesTrie = new TextTrieMap<NameInfo>(true); 153 _gnamesTrieFullyLoaded = false; 154 155 // Preload zone strings for the default time zone 156 TimeZone tz = TimeZone.getDefault(); 157 String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 158 if (tzCanonicalID != null) { 159 loadStrings(tzCanonicalID); 160 } 161 } 162 163 /** 164 * Constructs a <code>TimeZoneGenericNames</code> with the given locale. 165 * This constructor is private and called from {@link #getInstance(ULocale)}. 166 * @param locale the locale 167 */ 168 private TimeZoneGenericNames(ULocale locale) { 169 this(locale, null); 170 } 171 172 /** 173 * The factory method of <code>TimeZoneGenericNames</code>. This static method 174 * returns a frozen instance of cached <code>TimeZoneGenericNames</code>. 175 * @param locale the locale 176 * @return A frozen <code>TimeZoneGenericNames</code>. 177 */ 178 public static TimeZoneGenericNames getInstance(ULocale locale) { 179 String key = locale.getBaseName(); 180 return GENERIC_NAMES_CACHE.getInstance(key, locale); 181 } 182 183 /** 184 * Returns the display name of the time zone for the given name type 185 * at the given date, or null if the display name is not available. 186 * 187 * @param tz the time zone 188 * @param type the generic name type - see {@link GenericNameType} 189 * @param date the date 190 * @return the display name of the time zone for the given name type 191 * at the given date, or null. 192 */ 193 public String getDisplayName(TimeZone tz, GenericNameType type, long date) { 194 String name = null; 195 String tzCanonicalID = null; 196 switch (type) { 197 case LOCATION: 198 tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 199 if (tzCanonicalID != null) { 200 name = getGenericLocationName(tzCanonicalID); 201 } 202 break; 203 case LONG: 204 case SHORT: 205 name = formatGenericNonLocationName(tz, type, date); 206 if (name == null) { 207 tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 208 if (tzCanonicalID != null) { 209 name = getGenericLocationName(tzCanonicalID); 210 } 211 } 212 break; 213 } 214 return name; 215 } 216 217 /** 218 * Returns the generic location name for the given canonical time zone ID. 219 * 220 * @param canonicalTzID the canonical time zone ID 221 * @return the generic location name for the given canonical time zone ID. 222 */ 223 public String getGenericLocationName(String canonicalTzID) { 224 if (canonicalTzID == null || canonicalTzID.length() == 0) { 225 return null; 226 } 227 String name = _genericLocationNamesMap.get(canonicalTzID); 228 if (name != null) { 229 if (name.length() == 0) { 230 // empty string to indicate the name is not available 231 return null; 232 } 233 return name; 234 } 235 236 Output<Boolean> isPrimary = new Output<Boolean>(); 237 String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID, isPrimary); 238 if (countryCode != null) { 239 if (isPrimary.value) { 240 // If this is only the single zone in the country, use the country name 241 String country = getLocaleDisplayNames().regionDisplayName(countryCode); 242 name = formatPattern(Pattern.REGION_FORMAT, country); 243 } else { 244 // If there are multiple zones including this in the country, 245 // use the exemplar city name 246 247 // getExemplarLocationName should return non-empty String 248 // if the time zone is associated with a location 249 String city = _tznames.getExemplarLocationName(canonicalTzID); 250 name = formatPattern(Pattern.REGION_FORMAT, city); 251 } 252 } 253 254 if (name == null) { 255 _genericLocationNamesMap.putIfAbsent(canonicalTzID.intern(), ""); 256 } else { 257 synchronized (this) { // we have to sync the name map and the trie 258 canonicalTzID = canonicalTzID.intern(); 259 String tmp = _genericLocationNamesMap.putIfAbsent(canonicalTzID, name.intern()); 260 if (tmp == null) { 261 // Also put the name info the to trie 262 NameInfo info = new NameInfo(); 263 info.tzID = canonicalTzID; 264 info.type = GenericNameType.LOCATION; 265 _gnamesTrie.put(name, info); 266 } else { 267 name = tmp; 268 } 269 } 270 } 271 return name; 272 } 273 274 /** 275 * Sets the pattern string for the pattern type. 276 * Note: This method is designed for CLDR ST - not for common use. 277 * @param patType the pattern type 278 * @param patStr the pattern string 279 * @return this object. 280 */ 281 public TimeZoneGenericNames setFormatPattern(Pattern patType, String patStr) { 282 if (isFrozen()) { 283 throw new UnsupportedOperationException("Attempt to modify frozen object"); 284 } 285 286 // Changing pattern will invalidates cached names 287 if (!_genericLocationNamesMap.isEmpty()) { 288 _genericLocationNamesMap = new ConcurrentHashMap<String, String>(); 289 } 290 if (!_genericPartialLocationNamesMap.isEmpty()) { 291 _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>(); 292 } 293 _gnamesTrie = null; 294 _gnamesTrieFullyLoaded = false; 295 296 if (_patternFormatters == null) { 297 _patternFormatters = new MessageFormat[Pattern.values().length]; 298 } 299 _patternFormatters[patType.ordinal()] = new MessageFormat(patStr); 300 return this; 301 } 302 303 /** 304 * Private method to get a generic string, with fallback logics involved, 305 * that is, 306 * 307 * 1. If a generic non-location string is available for the zone, return it. 308 * 2. If a generic non-location string is associated with a meta zone and 309 * the zone never use daylight time around the given date, use the standard 310 * string (if available). 311 * 3. If a generic non-location string is associated with a meta zone and 312 * the offset at the given time is different from the preferred zone for the 313 * current locale, then return the generic partial location string (if available) 314 * 4. If a generic non-location string is not available, use generic location 315 * string. 316 * 317 * @param tz the requested time zone 318 * @param date the date 319 * @param type the generic name type, either LONG or SHORT 320 * @return the name used for a generic name type, which could be the 321 * generic name, or the standard name (if the zone does not observes DST 322 * around the date), or the partial location name. 323 */ 324 private String formatGenericNonLocationName(TimeZone tz, GenericNameType type, long date) { 325 assert(type == GenericNameType.LONG || type == GenericNameType.SHORT); 326 String tzID = ZoneMeta.getCanonicalCLDRID(tz); 327 328 if (tzID == null) { 329 return null; 330 } 331 332 // Try to get a name from time zone first 333 NameType nameType = (type == GenericNameType.LONG) ? NameType.LONG_GENERIC : NameType.SHORT_GENERIC; 334 String name = _tznames.getTimeZoneDisplayName(tzID, nameType); 335 336 if (name != null) { 337 return name; 338 } 339 340 // Try meta zone 341 String mzID = _tznames.getMetaZoneID(tzID, date); 342 if (mzID != null) { 343 boolean useStandard = false; 344 int[] offsets = {0, 0}; 345 tz.getOffset(date, false, offsets); 346 347 if (offsets[1] == 0) { 348 useStandard = true; 349 // Check if the zone actually uses daylight saving time around the time 350 if (tz instanceof BasicTimeZone) { 351 BasicTimeZone btz = (BasicTimeZone)tz; 352 TimeZoneTransition before = btz.getPreviousTransition(date, true); 353 if (before != null 354 && (date - before.getTime() < DST_CHECK_RANGE) 355 && before.getFrom().getDSTSavings() != 0) { 356 useStandard = false; 357 } else { 358 TimeZoneTransition after = btz.getNextTransition(date, false); 359 if (after != null 360 && (after.getTime() - date < DST_CHECK_RANGE) 361 && after.getTo().getDSTSavings() != 0) { 362 useStandard = false; 363 } 364 } 365 } else { 366 // If not BasicTimeZone... only if the instance is not an ICU's implementation. 367 // We may get a wrong answer in edge case, but it should practically work OK. 368 int[] tmpOffsets = new int[2]; 369 tz.getOffset(date - DST_CHECK_RANGE, false, tmpOffsets); 370 if (tmpOffsets[1] != 0) { 371 useStandard = false; 372 } else { 373 tz.getOffset(date + DST_CHECK_RANGE, false, tmpOffsets); 374 if (tmpOffsets[1] != 0){ 375 useStandard = false; 376 } 377 } 378 } 379 } 380 if (useStandard) { 381 NameType stdNameType = (nameType == NameType.LONG_GENERIC) ? 382 NameType.LONG_STANDARD : NameType.SHORT_STANDARD; 383 String stdName = _tznames.getDisplayName(tzID, stdNameType, date); 384 if (stdName != null) { 385 name = stdName; 386 387 // TODO: revisit this issue later 388 // In CLDR, a same display name is used for both generic and standard 389 // for some meta zones in some locales. This looks like a data bugs. 390 // For now, we check if the standard name is different from its generic 391 // name below. 392 String mzGenericName = _tznames.getMetaZoneDisplayName(mzID, nameType); 393 if (stdName.equalsIgnoreCase(mzGenericName)) { 394 name = null; 395 } 396 } 397 } 398 399 if (name == null) { 400 // Get a name from meta zone 401 String mzName = _tznames.getMetaZoneDisplayName(mzID, nameType); 402 if (mzName != null) { 403 // Check if we need to use a partial location format. 404 // This check is done by comparing offset with the meta zone's 405 // golden zone at the given date. 406 String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); 407 if (goldenID != null && !goldenID.equals(tzID)) { 408 TimeZone goldenZone = TimeZone.getFrozenTimeZone(goldenID); 409 int[] offsets1 = {0, 0}; 410 411 // Check offset in the golden zone with wall time. 412 // With getOffset(date, false, offsets1), 413 // you may get incorrect results because of time overlap at DST->STD 414 // transition. 415 goldenZone.getOffset(date + offsets[0] + offsets[1], true, offsets1); 416 417 if (offsets[0] != offsets1[0] || offsets[1] != offsets1[1]) { 418 // Now we need to use a partial location format. 419 name = getPartialLocationName(tzID, mzID, (nameType == NameType.LONG_GENERIC), mzName); 420 } else { 421 name = mzName; 422 } 423 } else { 424 name = mzName; 425 } 426 } 427 } 428 } 429 return name; 430 } 431 432 /** 433 * Private simple pattern formatter used for formatting generic location names 434 * and partial location names. We intentionally use JDK MessageFormat 435 * for performance reason. 436 * 437 * @param pat the message pattern enum 438 * @param args the format argument(s) 439 * @return the formatted string 440 */ 441 private synchronized String formatPattern(Pattern pat, String... args) { 442 if (_patternFormatters == null) { 443 _patternFormatters = new MessageFormat[Pattern.values().length]; 444 } 445 446 int idx = pat.ordinal(); 447 if (_patternFormatters[idx] == null) { 448 String patText; 449 try { 450 ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance( 451 ICUResourceBundle.ICU_ZONE_BASE_NAME, _locale); 452 patText = bundle.getStringWithFallback("zoneStrings/" + pat.key()); 453 } catch (MissingResourceException e) { 454 patText = pat.defaultValue(); 455 } 456 457 _patternFormatters[idx] = new MessageFormat(patText); 458 } 459 return _patternFormatters[idx].format(args); 460 } 461 462 /** 463 * Private method returning LocaleDisplayNames instance for the locale of this 464 * instance. Because LocaleDisplayNames is only used for generic 465 * location formant and partial location format, the LocaleDisplayNames 466 * is instantiated lazily. 467 * 468 * @return the instance of LocaleDisplayNames for the locale of this object. 469 */ 470 private synchronized LocaleDisplayNames getLocaleDisplayNames() { 471 LocaleDisplayNames locNames = null; 472 if (_localeDisplayNamesRef != null) { 473 locNames = _localeDisplayNamesRef.get(); 474 } 475 if (locNames == null) { 476 locNames = LocaleDisplayNames.getInstance(_locale); 477 _localeDisplayNamesRef = new WeakReference<LocaleDisplayNames>(locNames); 478 } 479 return locNames; 480 } 481 482 private synchronized void loadStrings(String tzCanonicalID) { 483 if (tzCanonicalID == null || tzCanonicalID.length() == 0) { 484 return; 485 } 486 // getGenericLocationName() formats a name and put it into the trie 487 getGenericLocationName(tzCanonicalID); 488 489 // Generic partial location format 490 Set<String> mzIDs = _tznames.getAvailableMetaZoneIDs(tzCanonicalID); 491 for (String mzID : mzIDs) { 492 // if this time zone is not the golden zone of the meta zone, 493 // partial location name (such as "PT (Los Angeles)") might be 494 // available. 495 String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); 496 if (!tzCanonicalID.equals(goldenID)) { 497 for (NameType genNonLocType : GENERIC_NON_LOCATION_TYPES) { 498 String mzGenName = _tznames.getMetaZoneDisplayName(mzID, genNonLocType); 499 if (mzGenName != null) { 500 // getPartialLocationName() formats a name and put it into the trie 501 getPartialLocationName(tzCanonicalID, mzID, (genNonLocType == NameType.LONG_GENERIC), mzGenName); 502 } 503 } 504 } 505 } 506 } 507 508 /** 509 * Private method returning the target region. The target regions is determined by 510 * the locale of this instance. When a generic name is coming from 511 * a meta zone, this region is used for checking if the time zone 512 * is a reference zone of the meta zone. 513 * 514 * @return the target region 515 */ 516 private synchronized String getTargetRegion() { 517 if (_region == null) { 518 _region = _locale.getCountry(); 519 if (_region.length() == 0) { 520 ULocale tmp = ULocale.addLikelySubtags(_locale); 521 _region = tmp.getCountry(); 522 if (_region.length() == 0) { 523 _region = "001"; 524 } 525 } 526 } 527 return _region; 528 } 529 530 /** 531 * Private method for formatting partial location names. This format 532 * is used when a generic name of a meta zone is available, but the given 533 * time zone is not a reference zone (golden zone) of the meta zone. 534 * 535 * @param tzID the canonical time zone ID 536 * @param mzID the meta zone ID 537 * @param isLong true when long generic name 538 * @param mzDisplayName the meta zone generic display name 539 * @return the partial location format string 540 */ 541 private String getPartialLocationName(String tzID, String mzID, boolean isLong, String mzDisplayName) { 542 String letter = isLong ? "L" : "S"; 543 String key = tzID + "&" + mzID + "#" + letter; 544 String name = _genericPartialLocationNamesMap.get(key); 545 if (name != null) { 546 return name; 547 } 548 String location = null; 549 String countryCode = ZoneMeta.getCanonicalCountry(tzID); 550 if (countryCode != null) { 551 // Is this the golden zone for the region? 552 String regionalGolden = _tznames.getReferenceZoneID(mzID, countryCode); 553 if (tzID.equals(regionalGolden)) { 554 // Use country name 555 location = getLocaleDisplayNames().regionDisplayName(countryCode); 556 } else { 557 // Otherwise, use exemplar city name 558 location = _tznames.getExemplarLocationName(tzID); 559 } 560 } else { 561 location = _tznames.getExemplarLocationName(tzID); 562 if (location == null) { 563 // This could happen when the time zone is not associated with a country, 564 // and its ID is not hierarchical, for example, CST6CDT. 565 // We use the canonical ID itself as the location for this case. 566 location = tzID; 567 } 568 } 569 name = formatPattern(Pattern.FALLBACK_FORMAT, location, mzDisplayName); 570 synchronized (this) { // we have to sync the name map and the trie 571 String tmp = _genericPartialLocationNamesMap.putIfAbsent(key.intern(), name.intern()); 572 if (tmp == null) { 573 NameInfo info = new NameInfo(); 574 info.tzID = tzID.intern(); 575 info.type = isLong ? GenericNameType.LONG : GenericNameType.SHORT; 576 _gnamesTrie.put(name, info); 577 } else { 578 name = tmp; 579 } 580 } 581 return name; 582 } 583 584 /** 585 * A private class used for storing the name information in the local trie. 586 */ 587 private static class NameInfo { 588 String tzID; 589 GenericNameType type; 590 } 591 592 /** 593 * A class used for returning the name search result used by 594 * {@link TimeZoneGenericNames#find(String, int, EnumSet)}. 595 */ 596 public static class GenericMatchInfo { 597 GenericNameType nameType; 598 String tzID; 599 int matchLength; 600 TimeType timeType = TimeType.UNKNOWN; 601 602 public GenericNameType nameType() { 603 return nameType; 604 } 605 606 public String tzID() { 607 return tzID; 608 } 609 610 public TimeType timeType() { 611 return timeType; 612 } 613 614 public int matchLength() { 615 return matchLength; 616 } 617 } 618 619 /** 620 * A private class implementing the search callback interface in 621 * <code>TextTrieMap</code> for collecting match results. 622 */ 623 private static class GenericNameSearchHandler implements ResultHandler<NameInfo> { 624 private EnumSet<GenericNameType> _types; 625 private Collection<GenericMatchInfo> _matches; 626 private int _maxMatchLen; 627 628 GenericNameSearchHandler(EnumSet<GenericNameType> types) { 629 _types = types; 630 } 631 632 /* (non-Javadoc) 633 * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator) 634 */ 635 public boolean handlePrefixMatch(int matchLength, Iterator<NameInfo> values) { 636 while (values.hasNext()) { 637 NameInfo info = values.next(); 638 if (_types != null && !_types.contains(info.type)) { 639 continue; 640 } 641 GenericMatchInfo matchInfo = new GenericMatchInfo(); 642 matchInfo.tzID = info.tzID; 643 matchInfo.nameType = info.type; 644 matchInfo.matchLength = matchLength; 645 //matchInfo.timeType = TimeType.UNKNOWN; 646 if (_matches == null) { 647 _matches = new LinkedList<GenericMatchInfo>(); 648 } 649 _matches.add(matchInfo); 650 if (matchLength > _maxMatchLen) { 651 _maxMatchLen = matchLength; 652 } 653 } 654 return true; 655 } 656 657 /** 658 * Returns the match results 659 * @return the match results 660 */ 661 public Collection<GenericMatchInfo> getMatches() { 662 return _matches; 663 } 664 665 /** 666 * Returns the maximum match length, or 0 if no match was found 667 * @return the maximum match length 668 */ 669 public int getMaxMatchLen() { 670 return _maxMatchLen; 671 } 672 673 /** 674 * Resets the match results 675 */ 676 public void resetResults() { 677 _matches = null; 678 _maxMatchLen = 0; 679 } 680 } 681 682 /** 683 * Returns the best match of time zone display name for the specified types in the 684 * given text at the given offset. 685 * @param text the text 686 * @param start the start offset in the text 687 * @param genericTypes the set of name types. 688 * @return the best matching name info. 689 */ 690 public GenericMatchInfo findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes) { 691 if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { 692 throw new IllegalArgumentException("bad input text or range"); 693 } 694 GenericMatchInfo bestMatch = null; 695 696 // Find matches in the TimeZoneNames first 697 Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes); 698 if (tznamesMatches != null) { 699 MatchInfo longestMatch = null; 700 for (MatchInfo match : tznamesMatches) { 701 if (longestMatch == null || match.matchLength() > longestMatch.matchLength()) { 702 longestMatch = match; 703 } 704 } 705 if (longestMatch != null) { 706 bestMatch = createGenericMatchInfo(longestMatch); 707 if (bestMatch.matchLength() == (text.length() - start)) { 708 // Full match 709 //return bestMatch; 710 711 // TODO Some time zone uses a same name for the long standard name 712 // and the location name. When the match is a long standard name, 713 // then we need to check if the name is same with the location name. 714 // This is probably a data error or a design bug. 715// if (bestMatch.nameType != GenericNameType.LONG || bestMatch.timeType != TimeType.STANDARD) { 716// return bestMatch; 717// } 718 719 // TODO The deprecation of commonlyUsed flag introduced the name 720 // conflict not only for long standard names, but short standard names too. 721 // These short names (found in zh_Hant) should be gone once we clean 722 // up CLDR time zone display name data. Once the short name conflict 723 // problem (with location name) is resolved, we should change the condition 724 // below back to the original one above. -Yoshito (2011-09-14) 725 if (bestMatch.timeType != TimeType.STANDARD) { 726 return bestMatch; 727 } 728 } 729 } 730 } 731 732 // Find matches in the local trie 733 Collection<GenericMatchInfo> localMatches = findLocal(text, start, genericTypes); 734 if (localMatches != null) { 735 for (GenericMatchInfo match : localMatches) { 736 // TODO See the above TODO. We use match.matchLength() >= bestMatch.matcheLength() 737 // for the reason described above. 738 //if (bestMatch == null || match.matchLength() > bestMatch.matchLength()) { 739 if (bestMatch == null || match.matchLength() >= bestMatch.matchLength()) { 740 bestMatch = match; 741 } 742 } 743 } 744 745 return bestMatch; 746 } 747 748 /** 749 * Returns a collection of time zone display name matches for the specified types in the 750 * given text at the given offset. 751 * @param text the text 752 * @param start the start offset in the text 753 * @param genericTypes the set of name types. 754 * @return A collection of match info. 755 */ 756 public Collection<GenericMatchInfo> find(String text, int start, EnumSet<GenericNameType> genericTypes) { 757 if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { 758 throw new IllegalArgumentException("bad input text or range"); 759 } 760 // Find matches in the local trie 761 Collection<GenericMatchInfo> results = findLocal(text, start, genericTypes); 762 763 // Also find matches in the TimeZoneNames 764 Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes); 765 if (tznamesMatches != null) { 766 // transform matches and append them to local matches 767 for (MatchInfo match : tznamesMatches) { 768 if (results == null) { 769 results = new LinkedList<GenericMatchInfo>(); 770 } 771 results.add(createGenericMatchInfo(match)); 772 } 773 } 774 return results; 775 } 776 777 /** 778 * Returns a <code>GenericMatchInfo</code> for the given <code>MatchInfo</code>. 779 * @param matchInfo the MatchInfo 780 * @return A GenericMatchInfo 781 */ 782 private GenericMatchInfo createGenericMatchInfo(MatchInfo matchInfo) { 783 GenericNameType nameType = null; 784 TimeType timeType = TimeType.UNKNOWN; 785 switch (matchInfo.nameType()) { 786 case LONG_STANDARD: 787 nameType = GenericNameType.LONG; 788 timeType = TimeType.STANDARD; 789 break; 790 case LONG_GENERIC: 791 nameType = GenericNameType.LONG; 792 break; 793 case SHORT_STANDARD: 794 nameType = GenericNameType.SHORT; 795 timeType = TimeType.STANDARD; 796 break; 797 case SHORT_GENERIC: 798 nameType = GenericNameType.SHORT; 799 break; 800 default: 801 throw new IllegalArgumentException("Unexpected MatchInfo name type - " + matchInfo.nameType()); 802 } 803 804 String tzID = matchInfo.tzID(); 805 if (tzID == null) { 806 String mzID = matchInfo.mzID(); 807 assert(mzID != null); 808 tzID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); 809 } 810 assert(tzID != null); 811 812 GenericMatchInfo gmatch = new GenericMatchInfo(); 813 gmatch.nameType = nameType; 814 gmatch.tzID = tzID; 815 gmatch.matchLength = matchInfo.matchLength(); 816 gmatch.timeType = timeType; 817 818 return gmatch; 819 } 820 821 /** 822 * Returns a collection of time zone display name matches for the specified types in the 823 * given text at the given offset. This method only finds matches from the TimeZoneNames 824 * used by this object. 825 * @param text the text 826 * @param start the start offset in the text 827 * @param types the set of name types. 828 * @return A collection of match info. 829 */ 830 private Collection<MatchInfo> findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types) { 831 Collection<MatchInfo> tznamesMatches = null; 832 833 // Check if the target name type is really in the TimeZoneNames 834 EnumSet<NameType> nameTypes = EnumSet.noneOf(NameType.class); 835 if (types.contains(GenericNameType.LONG)) { 836 nameTypes.add(NameType.LONG_GENERIC); 837 nameTypes.add(NameType.LONG_STANDARD); 838 } 839 if (types.contains(GenericNameType.SHORT)) { 840 nameTypes.add(NameType.SHORT_GENERIC); 841 nameTypes.add(NameType.SHORT_STANDARD); 842 } 843 844 if (!nameTypes.isEmpty()) { 845 // Find matches in the TimeZoneNames 846 tznamesMatches = _tznames.find(text, start, nameTypes); 847 } 848 return tznamesMatches; 849 } 850 851 /** 852 * Returns a collection of time zone display name matches for the specified types in the 853 * given text at the given offset. This method only finds matches from the local trie, 854 * that contains 1) generic location names and 2) long/short generic partial location names, 855 * used by this object. 856 * @param text the text 857 * @param start the start offset in the text 858 * @param types the set of name types. 859 * @return A collection of match info. 860 */ 861 private synchronized Collection<GenericMatchInfo> findLocal(String text, int start, EnumSet<GenericNameType> types) { 862 GenericNameSearchHandler handler = new GenericNameSearchHandler(types); 863 _gnamesTrie.find(text, start, handler); 864 if (handler.getMaxMatchLen() == (text.length() - start) || _gnamesTrieFullyLoaded) { 865 // perfect match 866 return handler.getMatches(); 867 } 868 869 // All names are not yet loaded into the local trie. 870 // Load all available names into the trie. This could be very heavy. 871 872 Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); 873 for (String tzID : tzIDs) { 874 loadStrings(tzID); 875 } 876 _gnamesTrieFullyLoaded = true; 877 878 // now, try it again 879 handler.resetResults(); 880 _gnamesTrie.find(text, start, handler); 881 return handler.getMatches(); 882 } 883 884 /** 885 * <code>TimeZoneGenericNames</code> cache implementation. 886 */ 887 private static class Cache extends SoftCache<String, TimeZoneGenericNames, ULocale> { 888 889 /* (non-Javadoc) 890 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 891 */ 892 @Override 893 protected TimeZoneGenericNames createInstance(String key, ULocale data) { 894 return new TimeZoneGenericNames(data).freeze(); 895 } 896 897 } 898 899 /* 900 * The custom deserialization method. 901 * This implementation only read locale used by the object. 902 */ 903 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 904 in.defaultReadObject(); 905 init(); 906 } 907 908 /** 909 * {@inheritDoc} 910 */ 911 public boolean isFrozen() { 912 return _frozen; 913 } 914 915 /** 916 * {@inheritDoc} 917 */ 918 public TimeZoneGenericNames freeze() { 919 _frozen = true; 920 return this; 921 } 922 923 /** 924 * {@inheritDoc} 925 */ 926 public TimeZoneGenericNames cloneAsThawed() { 927 TimeZoneGenericNames copy = null; 928 try { 929 copy = (TimeZoneGenericNames)super.clone(); 930 copy._frozen = false; 931 } catch (Throwable t) { 932 // This should never happen 933 } 934 return copy; 935 } 936} 937