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