1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // © 2016 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 /* 5 ******************************************************************************* 6 * Copyright (C) 2011-2016, International Business Machines Corporation and 7 * others. All Rights Reserved. 8 ******************************************************************************* 9 */ 10 package android.icu.impl; 11 12 import java.io.IOException; 13 import java.io.ObjectInputStream; 14 import java.io.Serializable; 15 import java.lang.ref.WeakReference; 16 import java.text.MessageFormat; 17 import java.util.Collection; 18 import java.util.EnumSet; 19 import java.util.Iterator; 20 import java.util.LinkedList; 21 import java.util.MissingResourceException; 22 import java.util.Set; 23 import java.util.concurrent.ConcurrentHashMap; 24 25 import android.icu.impl.TextTrieMap.ResultHandler; 26 import android.icu.text.LocaleDisplayNames; 27 import android.icu.text.TimeZoneFormat.TimeType; 28 import android.icu.text.TimeZoneNames; 29 import android.icu.text.TimeZoneNames.MatchInfo; 30 import android.icu.text.TimeZoneNames.NameType; 31 import android.icu.util.BasicTimeZone; 32 import android.icu.util.Freezable; 33 import android.icu.util.Output; 34 import android.icu.util.TimeZone; 35 import android.icu.util.TimeZone.SystemTimeZoneType; 36 import android.icu.util.TimeZoneTransition; 37 import android.icu.util.ULocale; 38 39 /** 40 * This class interact with TimeZoneNames and LocaleDisplayNames 41 * to format and parse time zone's generic display names. 42 * It is not recommended to use this class directly, instead 43 * use android.icu.text.TimeZoneFormat. 44 * @hide Only a subset of ICU is exposed in Android 45 */ 46 public class TimeZoneGenericNames implements Serializable, Freezable<TimeZoneGenericNames> { 47 48 // Note: This class implements Serializable, but we no longer serialize instance of 49 // TimeZoneGenericNames in ICU 49. ICU 4.8 android.icu.text.TimeZoneFormat used to 50 // serialize TimeZoneGenericNames field. TimeZoneFormat no longer read TimeZoneGenericNames 51 // field, we have to keep TimeZoneGenericNames Serializable. Otherwise it fails to read 52 // (unused) TimeZoneGenericNames serialized data. 53 54 private static final long serialVersionUID = 2729910342063468417L; 55 56 /** 57 * Generic name type enum 58 */ 59 public enum GenericNameType { 60 LOCATION ("LONG", "SHORT"), 61 LONG (), 62 SHORT (); 63 64 String[] _fallbackTypeOf; GenericNameType(String... fallbackTypeOf)65 GenericNameType(String... fallbackTypeOf) { 66 _fallbackTypeOf = fallbackTypeOf; 67 } 68 isFallbackTypeOf(GenericNameType type)69 public boolean isFallbackTypeOf(GenericNameType type) { 70 String typeStr = type.toString(); 71 for (String t : _fallbackTypeOf) { 72 if (t.equals(typeStr)) { 73 return true; 74 } 75 } 76 return false; 77 } 78 } 79 80 /** 81 * Format pattern enum used for composing location and partial location names 82 */ 83 public enum Pattern { 84 // The format pattern such as "{0} Time", where {0} is the country or city. 85 REGION_FORMAT("regionFormat", "({0})"), 86 87 // Note: FALLBACK_REGION_FORMAT is no longer used since ICU 50/CLDR 22.1 88 // The format pattern such as "{1} Time ({0})", where {1} is the country and {0} is a city. 89 //FALLBACK_REGION_FORMAT("fallbackRegionFormat", "{1} ({0})"), 90 91 // The format pattern such as "{1} ({0})", where {1} is the metazone, and {0} is the country or city. 92 FALLBACK_FORMAT("fallbackFormat", "{1} ({0})"); 93 94 String _key; 95 String _defaultVal; 96 Pattern(String key, String defaultVal)97 Pattern(String key, String defaultVal) { 98 _key = key; 99 _defaultVal = defaultVal; 100 } 101 key()102 String key() { 103 return _key; 104 } 105 defaultValue()106 String defaultValue() { 107 return _defaultVal; 108 } 109 } 110 111 private final ULocale _locale; 112 private TimeZoneNames _tznames; 113 114 private transient volatile boolean _frozen; 115 private transient String _region; 116 private transient WeakReference<LocaleDisplayNames> _localeDisplayNamesRef; 117 private transient MessageFormat[] _patternFormatters; 118 119 private transient ConcurrentHashMap<String, String> _genericLocationNamesMap; 120 private transient ConcurrentHashMap<String, String> _genericPartialLocationNamesMap; 121 private transient TextTrieMap<NameInfo> _gnamesTrie; 122 private transient boolean _gnamesTrieFullyLoaded; 123 124 private static Cache GENERIC_NAMES_CACHE = new Cache(); 125 126 // Window size used for DST check for a zone in a metazone (about a half year) 127 private static final long DST_CHECK_RANGE = 184L*(24*60*60*1000); 128 129 private static final NameType[] GENERIC_NON_LOCATION_TYPES = 130 {NameType.LONG_GENERIC, NameType.SHORT_GENERIC}; 131 132 133 /** 134 * Constructs a <code>TimeZoneGenericNames</code> with the given locale 135 * and the <code>TimeZoneNames</code>. 136 * @param locale the locale 137 * @param tznames the TimeZoneNames 138 */ TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames)139 public TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames) { 140 _locale = locale; 141 _tznames = tznames; 142 init(); 143 } 144 145 /** 146 * Private method initializing the instance of <code>TimeZoneGenericName</code>. 147 * This method should be called from a constructor and readObject. 148 */ init()149 private void init() { 150 if (_tznames == null) { 151 _tznames = TimeZoneNames.getInstance(_locale); 152 } 153 _genericLocationNamesMap = new ConcurrentHashMap<String, String>(); 154 _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>(); 155 156 _gnamesTrie = new TextTrieMap<NameInfo>(true); 157 _gnamesTrieFullyLoaded = false; 158 159 // Preload zone strings for the default time zone 160 TimeZone tz = TimeZone.getDefault(); 161 String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 162 if (tzCanonicalID != null) { 163 loadStrings(tzCanonicalID); 164 } 165 } 166 167 /** 168 * Constructs a <code>TimeZoneGenericNames</code> with the given locale. 169 * This constructor is private and called from {@link #getInstance(ULocale)}. 170 * @param locale the locale 171 */ TimeZoneGenericNames(ULocale locale)172 private TimeZoneGenericNames(ULocale locale) { 173 this(locale, null); 174 } 175 176 /** 177 * The factory method of <code>TimeZoneGenericNames</code>. This static method 178 * returns a frozen instance of cached <code>TimeZoneGenericNames</code>. 179 * @param locale the locale 180 * @return A frozen <code>TimeZoneGenericNames</code>. 181 */ getInstance(ULocale locale)182 public static TimeZoneGenericNames getInstance(ULocale locale) { 183 String key = locale.getBaseName(); 184 return GENERIC_NAMES_CACHE.getInstance(key, locale); 185 } 186 187 /** 188 * Returns the display name of the time zone for the given name type 189 * at the given date, or null if the display name is not available. 190 * 191 * @param tz the time zone 192 * @param type the generic name type - see {@link GenericNameType} 193 * @param date the date 194 * @return the display name of the time zone for the given name type 195 * at the given date, or null. 196 */ getDisplayName(TimeZone tz, GenericNameType type, long date)197 public String getDisplayName(TimeZone tz, GenericNameType type, long date) { 198 String name = null; 199 String tzCanonicalID = null; 200 switch (type) { 201 case LOCATION: 202 tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 203 if (tzCanonicalID != null) { 204 name = getGenericLocationName(tzCanonicalID); 205 } 206 break; 207 case LONG: 208 case SHORT: 209 name = formatGenericNonLocationName(tz, type, date); 210 if (name == null) { 211 tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 212 if (tzCanonicalID != null) { 213 name = getGenericLocationName(tzCanonicalID); 214 } 215 } 216 break; 217 } 218 return name; 219 } 220 221 /** 222 * Returns the generic location name for the given canonical time zone ID. 223 * 224 * @param canonicalTzID the canonical time zone ID 225 * @return the generic location name for the given canonical time zone ID. 226 */ getGenericLocationName(String canonicalTzID)227 public String getGenericLocationName(String canonicalTzID) { 228 if (canonicalTzID == null || canonicalTzID.length() == 0) { 229 return null; 230 } 231 String name = _genericLocationNamesMap.get(canonicalTzID); 232 if (name != null) { 233 if (name.length() == 0) { 234 // empty string to indicate the name is not available 235 return null; 236 } 237 return name; 238 } 239 240 Output<Boolean> isPrimary = new Output<Boolean>(); 241 String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID, isPrimary); 242 if (countryCode != null) { 243 if (isPrimary.value) { 244 // If this is only the single zone in the country, use the country name 245 String country = getLocaleDisplayNames().regionDisplayName(countryCode); 246 name = formatPattern(Pattern.REGION_FORMAT, country); 247 } else { 248 // If there are multiple zones including this in the country, 249 // use the exemplar city name 250 251 // getExemplarLocationName should return non-empty String 252 // if the time zone is associated with a location 253 String city = _tznames.getExemplarLocationName(canonicalTzID); 254 name = formatPattern(Pattern.REGION_FORMAT, city); 255 } 256 } 257 258 if (name == null) { 259 _genericLocationNamesMap.putIfAbsent(canonicalTzID.intern(), ""); 260 } else { 261 synchronized (this) { // we have to sync the name map and the trie 262 canonicalTzID = canonicalTzID.intern(); 263 String tmp = _genericLocationNamesMap.putIfAbsent(canonicalTzID, name.intern()); 264 if (tmp == null) { 265 // Also put the name info the to trie 266 NameInfo info = new NameInfo(canonicalTzID, 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 */ setFormatPattern(Pattern patType, String patStr)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 */ formatGenericNonLocationName(TimeZone tz, GenericNameType type, long date)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 */ formatPattern(Pattern pat, String... args)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 ICUData.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 */ getLocaleDisplayNames()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 loadStrings(String tzCanonicalID)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 */ getTargetRegion()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 */ getPartialLocationName(String tzID, String mzID, boolean isLong, String mzDisplayName)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(tzID.intern(), 576 isLong ? GenericNameType.LONG : GenericNameType.SHORT); 577 _gnamesTrie.put(name, info); 578 } else { 579 name = tmp; 580 } 581 } 582 return name; 583 } 584 585 /** 586 * A private class used for storing the name information in the local trie. 587 */ 588 private static class NameInfo { 589 final String tzID; 590 final GenericNameType type; 591 NameInfo(String tzID, GenericNameType type)592 NameInfo(String tzID, GenericNameType type) { 593 this.tzID = tzID; 594 this.type = type; 595 } 596 } 597 598 /** 599 * A class used for returning the name search result used by 600 * {@link TimeZoneGenericNames#find(String, int, EnumSet)}. 601 */ 602 public static class GenericMatchInfo { 603 final GenericNameType nameType; 604 final String tzID; 605 final int matchLength; 606 final TimeType timeType; 607 GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength)608 private GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength) { 609 this(nameType, tzID, matchLength, TimeType.UNKNOWN); 610 } 611 GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength, TimeType timeType)612 private GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength, TimeType timeType) { 613 this.nameType = nameType; 614 this.tzID = tzID; 615 this.matchLength = matchLength; 616 this.timeType = timeType; 617 } 618 nameType()619 public GenericNameType nameType() { 620 return nameType; 621 } 622 tzID()623 public String tzID() { 624 return tzID; 625 } 626 timeType()627 public TimeType timeType() { 628 return timeType; 629 } 630 matchLength()631 public int matchLength() { 632 return matchLength; 633 } 634 } 635 636 /** 637 * A private class implementing the search callback interface in 638 * <code>TextTrieMap</code> for collecting match results. 639 */ 640 private static class GenericNameSearchHandler implements ResultHandler<NameInfo> { 641 private EnumSet<GenericNameType> _types; 642 private Collection<GenericMatchInfo> _matches; 643 private int _maxMatchLen; 644 GenericNameSearchHandler(EnumSet<GenericNameType> types)645 GenericNameSearchHandler(EnumSet<GenericNameType> types) { 646 _types = types; 647 } 648 649 /* (non-Javadoc) 650 * @see android.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator) 651 */ 652 @Override handlePrefixMatch(int matchLength, Iterator<NameInfo> values)653 public boolean handlePrefixMatch(int matchLength, Iterator<NameInfo> values) { 654 while (values.hasNext()) { 655 NameInfo info = values.next(); 656 if (_types != null && !_types.contains(info.type)) { 657 continue; 658 } 659 GenericMatchInfo matchInfo = new GenericMatchInfo(info.type, info.tzID, matchLength); 660 if (_matches == null) { 661 _matches = new LinkedList<GenericMatchInfo>(); 662 } 663 _matches.add(matchInfo); 664 if (matchLength > _maxMatchLen) { 665 _maxMatchLen = matchLength; 666 } 667 } 668 return true; 669 } 670 671 /** 672 * Returns the match results 673 * @return the match results 674 */ getMatches()675 public Collection<GenericMatchInfo> getMatches() { 676 return _matches; 677 } 678 679 /** 680 * Returns the maximum match length, or 0 if no match was found 681 * @return the maximum match length 682 */ getMaxMatchLen()683 public int getMaxMatchLen() { 684 return _maxMatchLen; 685 } 686 687 /** 688 * Resets the match results 689 */ resetResults()690 public void resetResults() { 691 _matches = null; 692 _maxMatchLen = 0; 693 } 694 } 695 696 /** 697 * Returns the best match of time zone display name for the specified types in the 698 * given text at the given offset. 699 * @param text the text 700 * @param start the start offset in the text 701 * @param genericTypes the set of name types. 702 * @return the best matching name info. 703 */ findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes)704 public GenericMatchInfo findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes) { 705 if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { 706 throw new IllegalArgumentException("bad input text or range"); 707 } 708 GenericMatchInfo bestMatch = null; 709 710 // Find matches in the TimeZoneNames first 711 Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes); 712 if (tznamesMatches != null) { 713 MatchInfo longestMatch = null; 714 for (MatchInfo match : tznamesMatches) { 715 if (longestMatch == null || match.matchLength() > longestMatch.matchLength()) { 716 longestMatch = match; 717 } 718 } 719 if (longestMatch != null) { 720 bestMatch = createGenericMatchInfo(longestMatch); 721 if (bestMatch.matchLength() == (text.length() - start)) { 722 // Full match 723 //return bestMatch; 724 725 // TODO Some time zone uses a same name for the long standard name 726 // and the location name. When the match is a long standard name, 727 // then we need to check if the name is same with the location name. 728 // This is probably a data error or a design bug. 729 // if (bestMatch.nameType != GenericNameType.LONG || bestMatch.timeType != TimeType.STANDARD) { 730 // return bestMatch; 731 // } 732 733 // TODO The deprecation of commonlyUsed flag introduced the name 734 // conflict not only for long standard names, but short standard names too. 735 // These short names (found in zh_Hant) should be gone once we clean 736 // up CLDR time zone display name data. Once the short name conflict 737 // problem (with location name) is resolved, we should change the condition 738 // below back to the original one above. -Yoshito (2011-09-14) 739 if (bestMatch.timeType != TimeType.STANDARD) { 740 return bestMatch; 741 } 742 } 743 } 744 } 745 746 // Find matches in the local trie 747 Collection<GenericMatchInfo> localMatches = findLocal(text, start, genericTypes); 748 if (localMatches != null) { 749 for (GenericMatchInfo match : localMatches) { 750 // TODO See the above TODO. We use match.matchLength() >= bestMatch.matcheLength() 751 // for the reason described above. 752 //if (bestMatch == null || match.matchLength() > bestMatch.matchLength()) { 753 if (bestMatch == null || match.matchLength() >= bestMatch.matchLength()) { 754 bestMatch = match; 755 } 756 } 757 } 758 759 return bestMatch; 760 } 761 762 /** 763 * Returns a collection of time zone display name matches for the specified types in the 764 * given text at the given offset. 765 * @param text the text 766 * @param start the start offset in the text 767 * @param genericTypes the set of name types. 768 * @return A collection of match info. 769 */ find(String text, int start, EnumSet<GenericNameType> genericTypes)770 public Collection<GenericMatchInfo> find(String text, int start, EnumSet<GenericNameType> genericTypes) { 771 if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { 772 throw new IllegalArgumentException("bad input text or range"); 773 } 774 // Find matches in the local trie 775 Collection<GenericMatchInfo> results = findLocal(text, start, genericTypes); 776 777 // Also find matches in the TimeZoneNames 778 Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes); 779 if (tznamesMatches != null) { 780 // transform matches and append them to local matches 781 for (MatchInfo match : tznamesMatches) { 782 if (results == null) { 783 results = new LinkedList<GenericMatchInfo>(); 784 } 785 results.add(createGenericMatchInfo(match)); 786 } 787 } 788 return results; 789 } 790 791 /** 792 * Returns a <code>GenericMatchInfo</code> for the given <code>MatchInfo</code>. 793 * @param matchInfo the MatchInfo 794 * @return A GenericMatchInfo 795 */ createGenericMatchInfo(MatchInfo matchInfo)796 private GenericMatchInfo createGenericMatchInfo(MatchInfo matchInfo) { 797 GenericNameType nameType = null; 798 TimeType timeType = TimeType.UNKNOWN; 799 switch (matchInfo.nameType()) { 800 case LONG_STANDARD: 801 nameType = GenericNameType.LONG; 802 timeType = TimeType.STANDARD; 803 break; 804 case LONG_GENERIC: 805 nameType = GenericNameType.LONG; 806 break; 807 case SHORT_STANDARD: 808 nameType = GenericNameType.SHORT; 809 timeType = TimeType.STANDARD; 810 break; 811 case SHORT_GENERIC: 812 nameType = GenericNameType.SHORT; 813 break; 814 default: 815 throw new IllegalArgumentException("Unexpected MatchInfo name type - " + matchInfo.nameType()); 816 } 817 818 String tzID = matchInfo.tzID(); 819 if (tzID == null) { 820 String mzID = matchInfo.mzID(); 821 assert(mzID != null); 822 tzID = _tznames.getReferenceZoneID(mzID, getTargetRegion()); 823 } 824 assert(tzID != null); 825 826 GenericMatchInfo gmatch = new GenericMatchInfo(nameType, tzID, matchInfo.matchLength(), timeType); 827 828 return gmatch; 829 } 830 831 /** 832 * Returns a collection of time zone display name matches for the specified types in the 833 * given text at the given offset. This method only finds matches from the TimeZoneNames 834 * used by this object. 835 * @param text the text 836 * @param start the start offset in the text 837 * @param types the set of name types. 838 * @return A collection of match info. 839 */ findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types)840 private Collection<MatchInfo> findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types) { 841 Collection<MatchInfo> tznamesMatches = null; 842 843 // Check if the target name type is really in the TimeZoneNames 844 EnumSet<NameType> nameTypes = EnumSet.noneOf(NameType.class); 845 if (types.contains(GenericNameType.LONG)) { 846 nameTypes.add(NameType.LONG_GENERIC); 847 nameTypes.add(NameType.LONG_STANDARD); 848 } 849 if (types.contains(GenericNameType.SHORT)) { 850 nameTypes.add(NameType.SHORT_GENERIC); 851 nameTypes.add(NameType.SHORT_STANDARD); 852 } 853 854 if (!nameTypes.isEmpty()) { 855 // Find matches in the TimeZoneNames 856 tznamesMatches = _tznames.find(text, start, nameTypes); 857 } 858 return tznamesMatches; 859 } 860 861 /** 862 * Returns a collection of time zone display name matches for the specified types in the 863 * given text at the given offset. This method only finds matches from the local trie, 864 * that contains 1) generic location names and 2) long/short generic partial location names, 865 * used by this object. 866 * @param text the text 867 * @param start the start offset in the text 868 * @param types the set of name types. 869 * @return A collection of match info. 870 */ findLocal(String text, int start, EnumSet<GenericNameType> types)871 private synchronized Collection<GenericMatchInfo> findLocal(String text, int start, EnumSet<GenericNameType> types) { 872 GenericNameSearchHandler handler = new GenericNameSearchHandler(types); 873 _gnamesTrie.find(text, start, handler); 874 if (handler.getMaxMatchLen() == (text.length() - start) || _gnamesTrieFullyLoaded) { 875 // perfect match 876 return handler.getMatches(); 877 } 878 879 // All names are not yet loaded into the local trie. 880 // Load all available names into the trie. This could be very heavy. 881 882 Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); 883 for (String tzID : tzIDs) { 884 loadStrings(tzID); 885 } 886 _gnamesTrieFullyLoaded = true; 887 888 // now, try it again 889 handler.resetResults(); 890 _gnamesTrie.find(text, start, handler); 891 return handler.getMatches(); 892 } 893 894 /** 895 * <code>TimeZoneGenericNames</code> cache implementation. 896 */ 897 private static class Cache extends SoftCache<String, TimeZoneGenericNames, ULocale> { 898 899 /* (non-Javadoc) 900 * @see android.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 901 */ 902 @Override createInstance(String key, ULocale data)903 protected TimeZoneGenericNames createInstance(String key, ULocale data) { 904 return new TimeZoneGenericNames(data).freeze(); 905 } 906 907 } 908 909 /* 910 * The custom deserialization method. 911 * This implementation only read locale used by the object. 912 */ readObject(ObjectInputStream in)913 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 914 in.defaultReadObject(); 915 init(); 916 } 917 918 /** 919 * {@inheritDoc} 920 */ 921 @Override isFrozen()922 public boolean isFrozen() { 923 return _frozen; 924 } 925 926 /** 927 * {@inheritDoc} 928 */ 929 @Override freeze()930 public TimeZoneGenericNames freeze() { 931 _frozen = true; 932 return this; 933 } 934 935 /** 936 * {@inheritDoc} 937 */ 938 @Override cloneAsThawed()939 public TimeZoneGenericNames cloneAsThawed() { 940 TimeZoneGenericNames copy = null; 941 try { 942 copy = (TimeZoneGenericNames)super.clone(); 943 copy._frozen = false; 944 } catch (Throwable t) { 945 // This should never happen 946 } 947 return copy; 948 } 949 } 950