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