1 // 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html#License 3 /* 4 ******************************************************************************* 5 * Copyright (C) 2011-2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 ******************************************************************************* 8 */ 9 package com.ibm.icu.impl; 10 11 import java.io.IOException; 12 import java.io.ObjectInputStream; 13 import java.io.ObjectOutputStream; 14 import java.util.ArrayList; 15 import java.util.Arrays; 16 import java.util.Collection; 17 import java.util.Collections; 18 import java.util.EnumSet; 19 import java.util.HashMap; 20 import java.util.HashSet; 21 import java.util.Iterator; 22 import java.util.LinkedList; 23 import java.util.List; 24 import java.util.Map; 25 import java.util.MissingResourceException; 26 import java.util.Set; 27 import java.util.concurrent.ConcurrentHashMap; 28 import java.util.regex.Pattern; 29 30 import com.ibm.icu.impl.TextTrieMap.ResultHandler; 31 import com.ibm.icu.text.TimeZoneNames; 32 import com.ibm.icu.util.TimeZone; 33 import com.ibm.icu.util.TimeZone.SystemTimeZoneType; 34 import com.ibm.icu.util.ULocale; 35 import com.ibm.icu.util.UResourceBundle; 36 37 /** 38 * The standard ICU implementation of TimeZoneNames 39 */ 40 public class TimeZoneNamesImpl extends TimeZoneNames { 41 42 private static final long serialVersionUID = -2179814848495897472L; 43 44 private static final String ZONE_STRINGS_BUNDLE = "zoneStrings"; 45 private static final String MZ_PREFIX = "meta:"; 46 47 private static volatile Set<String> METAZONE_IDS; 48 private static final TZ2MZsCache TZ_TO_MZS_CACHE = new TZ2MZsCache(); 49 private static final MZ2TZsCache MZ_TO_TZS_CACHE = new MZ2TZsCache(); 50 51 private transient ICUResourceBundle _zoneStrings; 52 53 54 // These are hard cache. We create only one TimeZoneNamesImpl per locale 55 // and it's stored in SoftCache, so we do not need to worry about the 56 // footprint much. 57 private transient ConcurrentHashMap<String, ZNames> _mzNamesMap; 58 private transient ConcurrentHashMap<String, ZNames> _tzNamesMap; 59 private transient boolean _namesFullyLoaded; 60 61 private transient TextTrieMap<NameInfo> _namesTrie; 62 private transient boolean _namesTrieFullyLoaded; 63 64 public TimeZoneNamesImpl(ULocale locale) { 65 initialize(locale); 66 } 67 68 /* (non-Javadoc) 69 * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs() 70 */ 71 @Override 72 public Set<String> getAvailableMetaZoneIDs() { 73 return _getAvailableMetaZoneIDs(); 74 } 75 76 static Set<String> _getAvailableMetaZoneIDs() { 77 if (METAZONE_IDS == null) { 78 synchronized (TimeZoneNamesImpl.class) { 79 if (METAZONE_IDS == null) { 80 UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metaZones"); 81 UResourceBundle mapTimezones = bundle.get("mapTimezones"); 82 Set<String> keys = mapTimezones.keySet(); 83 METAZONE_IDS = Collections.unmodifiableSet(keys); 84 } 85 } 86 } 87 return METAZONE_IDS; 88 } 89 90 /* (non-Javadoc) 91 * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs(java.lang.String) 92 */ 93 @Override 94 public Set<String> getAvailableMetaZoneIDs(String tzID) { 95 return _getAvailableMetaZoneIDs(tzID); 96 } 97 98 static Set<String> _getAvailableMetaZoneIDs(String tzID) { 99 if (tzID == null || tzID.length() == 0) { 100 return Collections.emptySet(); 101 } 102 List<MZMapEntry> maps = TZ_TO_MZS_CACHE.getInstance(tzID, tzID); 103 if (maps.isEmpty()) { 104 return Collections.emptySet(); 105 } 106 Set<String> mzIDs = new HashSet<String>(maps.size()); 107 for (MZMapEntry map : maps) { 108 mzIDs.add(map.mzID()); 109 } 110 // make it unmodifiable because of the API contract. We may cache the results in futre. 111 return Collections.unmodifiableSet(mzIDs); 112 } 113 114 /* (non-Javadoc) 115 * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneID(java.lang.String, long) 116 */ 117 @Override 118 public String getMetaZoneID(String tzID, long date) { 119 return _getMetaZoneID(tzID, date); 120 } 121 122 static String _getMetaZoneID(String tzID, long date) { 123 if (tzID == null || tzID.length() == 0) { 124 return null; 125 } 126 String mzID = null; 127 List<MZMapEntry> maps = TZ_TO_MZS_CACHE.getInstance(tzID, tzID); 128 for (MZMapEntry map : maps) { 129 if (date >= map.from() && date < map.to()) { 130 mzID = map.mzID(); 131 break; 132 } 133 } 134 return mzID; 135 } 136 137 /* (non-Javadoc) 138 * @see com.ibm.icu.text.TimeZoneNames#getReferenceZoneID(java.lang.String, java.lang.String) 139 */ 140 @Override 141 public String getReferenceZoneID(String mzID, String region) { 142 return _getReferenceZoneID(mzID, region); 143 } 144 145 static String _getReferenceZoneID(String mzID, String region) { 146 if (mzID == null || mzID.length() == 0) { 147 return null; 148 } 149 String refID = null; 150 Map<String, String> regionTzMap = MZ_TO_TZS_CACHE.getInstance(mzID, mzID); 151 if (!regionTzMap.isEmpty()) { 152 refID = regionTzMap.get(region); 153 if (refID == null) { 154 refID = regionTzMap.get("001"); 155 } 156 } 157 return refID; 158 } 159 160 /* 161 * (non-Javadoc) 162 * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneDisplayName(java.lang.String, com.ibm.icu.text.TimeZoneNames.NameType) 163 */ 164 @Override 165 public String getMetaZoneDisplayName(String mzID, NameType type) { 166 if (mzID == null || mzID.length() == 0) { 167 return null; 168 } 169 return loadMetaZoneNames(mzID).getName(type); 170 } 171 172 /* 173 * (non-Javadoc) 174 * @see com.ibm.icu.text.TimeZoneNames#getTimeZoneDisplayName(java.lang.String, com.ibm.icu.text.TimeZoneNames.NameType) 175 */ 176 @Override 177 public String getTimeZoneDisplayName(String tzID, NameType type) { 178 if (tzID == null || tzID.length() == 0) { 179 return null; 180 } 181 return loadTimeZoneNames(tzID).getName(type); 182 } 183 184 /* (non-Javadoc) 185 * @see com.ibm.icu.text.TimeZoneNames#getExemplarLocationName(java.lang.String) 186 */ 187 @Override 188 public String getExemplarLocationName(String tzID) { 189 if (tzID == null || tzID.length() == 0) { 190 return null; 191 } 192 String locName = loadTimeZoneNames(tzID).getName(NameType.EXEMPLAR_LOCATION); 193 return locName; 194 } 195 196 /* (non-Javadoc) 197 * @see com.ibm.icu.text.TimeZoneNames#find(java.lang.CharSequence, int, java.util.Set) 198 */ 199 @Override 200 public synchronized Collection<MatchInfo> find(CharSequence text, int start, EnumSet<NameType> nameTypes) { 201 if (text == null || text.length() == 0 || start < 0 || start >= text.length()) { 202 throw new IllegalArgumentException("bad input text or range"); 203 } 204 NameSearchHandler handler = new NameSearchHandler(nameTypes); 205 Collection<MatchInfo> matches; 206 207 // First try of lookup. 208 matches = doFind(handler, text, start); 209 if (matches != null) { 210 return matches; 211 } 212 213 // All names are not yet loaded into the trie. 214 // We may have loaded names for formatting several time zones, 215 // and might be parsing one of those. 216 // Populate the parsing trie from all of the already-loaded names. 217 addAllNamesIntoTrie(); 218 219 // Second try of lookup. 220 matches = doFind(handler, text, start); 221 if (matches != null) { 222 return matches; 223 } 224 225 // There are still some names we haven't loaded into the trie yet. 226 // Load everything now. 227 internalLoadAllDisplayNames(); 228 229 // Set default time zone location names 230 // for time zones without explicit display names. 231 // TODO: Should this logic be moved into internalLoadAllDisplayNames? 232 Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); 233 for (String tzID : tzIDs) { 234 if (!_tzNamesMap.containsKey(tzID)) { 235 ZNames.createTimeZoneAndPutInCache(_tzNamesMap, null, tzID); 236 } 237 } 238 addAllNamesIntoTrie(); 239 _namesTrieFullyLoaded = true; 240 241 // Third try: we must return this one. 242 return doFind(handler, text, start); 243 } 244 245 private Collection<MatchInfo> doFind(NameSearchHandler handler, CharSequence text, int start) { 246 handler.resetResults(); 247 _namesTrie.find(text, start, handler); 248 if (handler.getMaxMatchLen() == (text.length() - start) || _namesTrieFullyLoaded) { 249 return handler.getMatches(); 250 } 251 return null; 252 } 253 254 @Override 255 public synchronized void loadAllDisplayNames() { 256 internalLoadAllDisplayNames(); 257 } 258 259 @Override 260 public void getDisplayNames(String tzID, NameType[] types, long date, 261 String[] dest, int destOffset) { 262 if (tzID == null || tzID.length() == 0) { 263 return; 264 } 265 ZNames tzNames = loadTimeZoneNames(tzID); 266 ZNames mzNames = null; 267 for (int i = 0; i < types.length; ++i) { 268 NameType type = types[i]; 269 String name = tzNames.getName(type); 270 if (name == null) { 271 if (mzNames == null) { 272 String mzID = getMetaZoneID(tzID, date); 273 if (mzID == null || mzID.length() == 0) { 274 mzNames = ZNames.EMPTY_ZNAMES; 275 } else { 276 mzNames = loadMetaZoneNames(mzID); 277 } 278 } 279 name = mzNames.getName(type); 280 } 281 dest[destOffset + i] = name; 282 } 283 } 284 285 /** Caller must synchronize. */ 286 private void internalLoadAllDisplayNames() { 287 if (!_namesFullyLoaded) { 288 _namesFullyLoaded = true; 289 new ZoneStringsLoader().load(); 290 } 291 } 292 293 /** Caller must synchronize. */ 294 private void addAllNamesIntoTrie() { 295 for (Map.Entry<String, ZNames> entry : _tzNamesMap.entrySet()) { 296 entry.getValue().addAsTimeZoneIntoTrie(entry.getKey(), _namesTrie); 297 } 298 for (Map.Entry<String, ZNames> entry : _mzNamesMap.entrySet()) { 299 entry.getValue().addAsMetaZoneIntoTrie(entry.getKey(), _namesTrie); 300 } 301 } 302 303 /** 304 * Loads all meta zone and time zone names for this TimeZoneNames' locale. 305 */ 306 private final class ZoneStringsLoader extends UResource.Sink { 307 /** 308 * Prepare for several hundred time zones and meta zones. 309 * _zoneStrings.getSize() is ineffective in a sparsely populated locale like en-GB. 310 */ 311 private static final int INITIAL_NUM_ZONES = 300; 312 private HashMap<UResource.Key, ZNamesLoader> keyToLoader = 313 new HashMap<UResource.Key, ZNamesLoader>(INITIAL_NUM_ZONES); 314 private StringBuilder sb = new StringBuilder(32); 315 316 /** Caller must synchronize. */ 317 void load() { 318 _zoneStrings.getAllItemsWithFallback("", this); 319 for (Map.Entry<UResource.Key, ZNamesLoader> entry : keyToLoader.entrySet()) { 320 ZNamesLoader loader = entry.getValue(); 321 if (loader == ZNamesLoader.DUMMY_LOADER) { continue; } 322 UResource.Key key = entry.getKey(); 323 324 if (isMetaZone(key)) { 325 String mzID = mzIDFromKey(key); 326 ZNames.createMetaZoneAndPutInCache(_mzNamesMap, loader.getNames(), mzID); 327 } else { 328 String tzID = tzIDFromKey(key); 329 ZNames.createTimeZoneAndPutInCache(_tzNamesMap, loader.getNames(), tzID); 330 } 331 } 332 } 333 334 @Override 335 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 336 UResource.Table timeZonesTable = value.getTable(); 337 for (int j = 0; timeZonesTable.getKeyAndValue(j, key, value); ++j) { 338 assert !value.isNoInheritanceMarker(); 339 if (value.getType() == UResourceBundle.TABLE) { 340 consumeNamesTable(key, value, noFallback); 341 } else { 342 // Ignore fields that aren't tables (e.g., fallbackFormat and regionFormatStandard). 343 // All time zone fields are tables. 344 } 345 } 346 } 347 348 private void consumeNamesTable(UResource.Key key, UResource.Value value, boolean noFallback) { 349 ZNamesLoader loader = keyToLoader.get(key); 350 if (loader == null) { 351 if (isMetaZone(key)) { 352 String mzID = mzIDFromKey(key); 353 if (_mzNamesMap.containsKey(mzID)) { 354 // We have already loaded the names for this meta zone. 355 loader = ZNamesLoader.DUMMY_LOADER; 356 } else { 357 loader = new ZNamesLoader(); 358 } 359 } else { 360 String tzID = tzIDFromKey(key); 361 if (_tzNamesMap.containsKey(tzID)) { 362 // We have already loaded the names for this time zone. 363 loader = ZNamesLoader.DUMMY_LOADER; 364 } else { 365 loader = new ZNamesLoader(); 366 } 367 } 368 369 UResource.Key newKey = createKey(key); 370 keyToLoader.put(newKey, loader); 371 } 372 373 if (loader != ZNamesLoader.DUMMY_LOADER) { 374 // Let the ZNamesLoader consume the names table. 375 loader.put(key, value, noFallback); 376 } 377 } 378 379 UResource.Key createKey(UResource.Key key) { 380 return key.clone(); 381 } 382 383 boolean isMetaZone(UResource.Key key) { 384 return key.startsWith(MZ_PREFIX); 385 } 386 387 /** 388 * Equivalent to key.substring(MZ_PREFIX.length()) 389 * except reuses our StringBuilder. 390 */ 391 private String mzIDFromKey(UResource.Key key) { 392 sb.setLength(0); 393 for (int i = MZ_PREFIX.length(); i < key.length(); ++i) { 394 sb.append(key.charAt(i)); 395 } 396 return sb.toString(); 397 } 398 399 private String tzIDFromKey(UResource.Key key) { 400 sb.setLength(0); 401 for (int i = 0; i < key.length(); ++i) { 402 char c = key.charAt(i); 403 if (c == ':') { 404 c = '/'; 405 } 406 sb.append(c); 407 } 408 return sb.toString(); 409 } 410 } 411 412 /** 413 * Initialize the transient fields, called from the constructor and 414 * readObject. 415 * 416 * @param locale The locale 417 */ 418 private void initialize(ULocale locale) { 419 ICUResourceBundle bundle = (ICUResourceBundle)ICUResourceBundle.getBundleInstance( 420 ICUData.ICU_ZONE_BASE_NAME, locale); 421 _zoneStrings = (ICUResourceBundle)bundle.get(ZONE_STRINGS_BUNDLE); 422 423 // TODO: Access is synchronized, can we use a non-concurrent map? 424 _tzNamesMap = new ConcurrentHashMap<String, ZNames>(); 425 _mzNamesMap = new ConcurrentHashMap<String, ZNames>(); 426 _namesFullyLoaded = false; 427 428 _namesTrie = new TextTrieMap<NameInfo>(true); 429 _namesTrieFullyLoaded = false; 430 431 // Preload zone strings for the default time zone 432 TimeZone tz = TimeZone.getDefault(); 433 String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz); 434 if (tzCanonicalID != null) { 435 loadStrings(tzCanonicalID); 436 } 437 } 438 439 /** 440 * Load all strings used by the specified time zone. 441 * This is called from the initializer to load default zone's 442 * strings. 443 * @param tzCanonicalID the canonical time zone ID 444 */ 445 private synchronized void loadStrings(String tzCanonicalID) { 446 if (tzCanonicalID == null || tzCanonicalID.length() == 0) { 447 return; 448 } 449 loadTimeZoneNames(tzCanonicalID); 450 451 Set<String> mzIDs = getAvailableMetaZoneIDs(tzCanonicalID); 452 for (String mzID : mzIDs) { 453 loadMetaZoneNames(mzID); 454 } 455 } 456 457 /* 458 * The custom serialization method. 459 * This implementation only preserve locale object used for the names. 460 */ 461 private void writeObject(ObjectOutputStream out) throws IOException { 462 ULocale locale = _zoneStrings.getULocale(); 463 out.writeObject(locale); 464 } 465 466 /* 467 * The custom deserialization method. 468 * This implementation only read locale object used by the object. 469 */ 470 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 471 ULocale locale = (ULocale)in.readObject(); 472 initialize(locale); 473 } 474 475 /** 476 * Returns a set of names for the given meta zone ID. This method loads 477 * the set of names into the internal map and trie for future references. 478 * @param mzID the meta zone ID 479 * @return An instance of ZNames that includes a set of meta zone display names. 480 */ 481 private synchronized ZNames loadMetaZoneNames(String mzID) { 482 ZNames mznames = _mzNamesMap.get(mzID); 483 if (mznames == null) { 484 ZNamesLoader loader = new ZNamesLoader(); 485 loader.loadMetaZone(_zoneStrings, mzID); 486 mznames = ZNames.createMetaZoneAndPutInCache(_mzNamesMap, loader.getNames(), mzID); 487 } 488 return mznames; 489 } 490 491 /** 492 * Returns a set of names for the given time zone ID. This method loads 493 * the set of names into the internal map and trie for future references. 494 * @param tzID the canonical time zone ID 495 * @return An instance of ZNames that includes a set of time zone display names. 496 */ 497 private synchronized ZNames loadTimeZoneNames(String tzID) { 498 ZNames tznames = _tzNamesMap.get(tzID); 499 if (tznames == null) { 500 ZNamesLoader loader = new ZNamesLoader(); 501 loader.loadTimeZone(_zoneStrings, tzID); 502 tznames = ZNames.createTimeZoneAndPutInCache(_tzNamesMap, loader.getNames(), tzID); 503 } 504 return tznames; 505 } 506 507 /** 508 * An instance of NameInfo is stored in the zone names trie. 509 */ 510 private static class NameInfo { 511 String tzID; 512 String mzID; 513 NameType type; 514 } 515 516 /** 517 * NameSearchHandler is used for collecting name matches. 518 */ 519 private static class NameSearchHandler implements ResultHandler<NameInfo> { 520 private EnumSet<NameType> _nameTypes; 521 private Collection<MatchInfo> _matches; 522 private int _maxMatchLen; 523 524 NameSearchHandler(EnumSet<NameType> nameTypes) { 525 _nameTypes = nameTypes; 526 } 527 528 /* (non-Javadoc) 529 * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator) 530 */ 531 @Override 532 public boolean handlePrefixMatch(int matchLength, Iterator<NameInfo> values) { 533 while (values.hasNext()) { 534 NameInfo ninfo = values.next(); 535 if (_nameTypes != null && !_nameTypes.contains(ninfo.type)) { 536 continue; 537 } 538 MatchInfo minfo; 539 if (ninfo.tzID != null) { 540 minfo = new MatchInfo(ninfo.type, ninfo.tzID, null, matchLength); 541 } else { 542 assert(ninfo.mzID != null); 543 minfo = new MatchInfo(ninfo.type, null, ninfo.mzID, matchLength); 544 } 545 if (_matches == null) { 546 _matches = new LinkedList<MatchInfo>(); 547 } 548 _matches.add(minfo); 549 if (matchLength > _maxMatchLen) { 550 _maxMatchLen = matchLength; 551 } 552 } 553 return true; 554 } 555 556 /** 557 * Returns the match results 558 * @return the match results 559 */ 560 public Collection<MatchInfo> getMatches() { 561 if (_matches == null) { 562 return Collections.emptyList(); 563 } 564 return _matches; 565 } 566 567 /** 568 * Returns the maximum match length, or 0 if no match was found 569 * @return the maximum match length 570 */ 571 public int getMaxMatchLen() { 572 return _maxMatchLen; 573 } 574 575 /** 576 * Resets the match results 577 */ 578 public void resetResults() { 579 _matches = null; 580 _maxMatchLen = 0; 581 } 582 } 583 584 private static final class ZNamesLoader extends UResource.Sink { 585 private String[] names; 586 587 /** 588 * Does not load any names, for no-fallback handling. 589 */ 590 private static ZNamesLoader DUMMY_LOADER = new ZNamesLoader(); 591 592 void loadMetaZone(ICUResourceBundle zoneStrings, String mzID) { 593 String key = MZ_PREFIX + mzID; 594 loadNames(zoneStrings, key); 595 } 596 597 void loadTimeZone(ICUResourceBundle zoneStrings, String tzID) { 598 String key = tzID.replace('/', ':'); 599 loadNames(zoneStrings, key); 600 } 601 602 void loadNames(ICUResourceBundle zoneStrings, String key) { 603 assert zoneStrings != null; 604 assert key != null; 605 assert key.length() > 0; 606 607 // Reset names so that this instance can be used to load data multiple times. 608 names = null; 609 try { 610 zoneStrings.getAllItemsWithFallback(key, this); 611 } catch (MissingResourceException e) { 612 } 613 } 614 615 private static ZNames.NameTypeIndex nameTypeIndexFromKey(UResource.Key key) { 616 // Avoid key.toString() object creation. 617 if (key.length() != 2) { 618 return null; 619 } 620 char c0 = key.charAt(0); 621 char c1 = key.charAt(1); 622 if (c0 == 'l') { 623 return c1 == 'g' ? ZNames.NameTypeIndex.LONG_GENERIC : 624 c1 == 's' ? ZNames.NameTypeIndex.LONG_STANDARD : 625 c1 == 'd' ? ZNames.NameTypeIndex.LONG_DAYLIGHT : null; 626 } else if (c0 == 's') { 627 return c1 == 'g' ? ZNames.NameTypeIndex.SHORT_GENERIC : 628 c1 == 's' ? ZNames.NameTypeIndex.SHORT_STANDARD : 629 c1 == 'd' ? ZNames.NameTypeIndex.SHORT_DAYLIGHT : null; 630 } else if (c0 == 'e' && c1 == 'c') { 631 return ZNames.NameTypeIndex.EXEMPLAR_LOCATION; 632 } 633 return null; 634 } 635 636 private void setNameIfEmpty(UResource.Key key, UResource.Value value) { 637 if (names == null) { 638 names = new String[ZNames.NUM_NAME_TYPES]; 639 } 640 ZNames.NameTypeIndex index = nameTypeIndexFromKey(key); 641 if (index == null) { return; } 642 assert index.ordinal() < ZNames.NUM_NAME_TYPES; 643 if (names[index.ordinal()] == null) { 644 names[index.ordinal()] = value.getString(); 645 } 646 } 647 648 @Override 649 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 650 UResource.Table namesTable = value.getTable(); 651 for (int i = 0; namesTable.getKeyAndValue(i, key, value); ++i) { 652 assert value.getType() == UResourceBundle.STRING; 653 setNameIfEmpty(key, value); // could be value.isNoInheritanceMarker() 654 } 655 } 656 657 private String[] getNames() { 658 if (Utility.sameObjects(names, null)) { 659 return null; 660 } 661 int length = 0; 662 for (int i = 0; i < ZNames.NUM_NAME_TYPES; ++i) { 663 String name = names[i]; 664 if (name != null) { 665 if (name.equals(ICUResourceBundle.NO_INHERITANCE_MARKER)) { 666 names[i] = null; 667 } else { 668 length = i + 1; 669 } 670 } 671 } 672 673 String[] result; 674 if (length == ZNames.NUM_NAME_TYPES) { 675 // Return the full array if the last name is set. 676 result = names; 677 } else if (length == 0) { 678 // Return null instead of a zero-length array. 679 result = null; 680 } else { 681 // Return a shorter array for permanent storage. 682 // Copy all names into the minimal array. 683 result = Arrays.copyOfRange(names, 0, length); 684 } 685 return result; 686 } 687 } 688 689 /** 690 * This class stores name data for a meta zone or time zone. 691 */ 692 private static class ZNames { 693 /** 694 * Private enum corresponding to the public TimeZoneNames::NameType for the order in 695 * which fields are stored in a ZNames instance. EXEMPLAR_LOCATION is stored first 696 * for efficiency. 697 */ 698 private static enum NameTypeIndex { 699 EXEMPLAR_LOCATION, LONG_GENERIC, LONG_STANDARD, LONG_DAYLIGHT, SHORT_GENERIC, SHORT_STANDARD, SHORT_DAYLIGHT; 700 static final NameTypeIndex values[] = values(); 701 }; 702 703 public static final int NUM_NAME_TYPES = 7; 704 705 private static int getNameTypeIndex(NameType type) { 706 switch (type) { 707 case EXEMPLAR_LOCATION: 708 return NameTypeIndex.EXEMPLAR_LOCATION.ordinal(); 709 case LONG_GENERIC: 710 return NameTypeIndex.LONG_GENERIC.ordinal(); 711 case LONG_STANDARD: 712 return NameTypeIndex.LONG_STANDARD.ordinal(); 713 case LONG_DAYLIGHT: 714 return NameTypeIndex.LONG_DAYLIGHT.ordinal(); 715 case SHORT_GENERIC: 716 return NameTypeIndex.SHORT_GENERIC.ordinal(); 717 case SHORT_STANDARD: 718 return NameTypeIndex.SHORT_STANDARD.ordinal(); 719 case SHORT_DAYLIGHT: 720 return NameTypeIndex.SHORT_DAYLIGHT.ordinal(); 721 default: 722 throw new AssertionError("No NameTypeIndex match for " + type); 723 } 724 } 725 726 private static NameType getNameType(int index) { 727 switch (NameTypeIndex.values[index]) { 728 case EXEMPLAR_LOCATION: 729 return NameType.EXEMPLAR_LOCATION; 730 case LONG_GENERIC: 731 return NameType.LONG_GENERIC; 732 case LONG_STANDARD: 733 return NameType.LONG_STANDARD; 734 case LONG_DAYLIGHT: 735 return NameType.LONG_DAYLIGHT; 736 case SHORT_GENERIC: 737 return NameType.SHORT_GENERIC; 738 case SHORT_STANDARD: 739 return NameType.SHORT_STANDARD; 740 case SHORT_DAYLIGHT: 741 return NameType.SHORT_DAYLIGHT; 742 default: 743 throw new AssertionError("No NameType match for " + index); 744 } 745 } 746 747 static final ZNames EMPTY_ZNAMES = new ZNames(null); 748 // A meta zone names instance never has an exemplar location string. 749 private static final int EX_LOC_INDEX = NameTypeIndex.EXEMPLAR_LOCATION.ordinal(); 750 751 private String[] _names; 752 private boolean didAddIntoTrie; 753 754 protected ZNames(String[] names) { 755 _names = names; 756 didAddIntoTrie = names == null; 757 } 758 759 public static ZNames createMetaZoneAndPutInCache(Map<String, ZNames> cache, 760 String[] names, String mzID) { 761 String key = mzID.intern(); 762 ZNames value; 763 if (names == null) { 764 value = EMPTY_ZNAMES; 765 } else { 766 value = new ZNames(names); 767 } 768 cache.put(key, value); 769 return value; 770 } 771 772 public static ZNames createTimeZoneAndPutInCache(Map<String, ZNames> cache, 773 String[] names, String tzID) { 774 // For time zones, check that the exemplar city name is populated. If necessary, use 775 // "getDefaultExemplarLocationName" to extract it from the time zone name. 776 names = (names == null) ? new String[EX_LOC_INDEX + 1] : names; 777 if (names[EX_LOC_INDEX] == null) { 778 names[EX_LOC_INDEX] = getDefaultExemplarLocationName(tzID); 779 } 780 781 String key = tzID.intern(); 782 ZNames value = new ZNames(names); 783 cache.put(key, value); 784 return value; 785 } 786 787 public String getName(NameType type) { 788 int index = getNameTypeIndex(type); 789 if (_names != null && index < _names.length) { 790 return _names[index]; 791 } else { 792 return null; 793 } 794 } 795 796 public void addAsMetaZoneIntoTrie(String mzID, TextTrieMap<NameInfo> trie) { 797 addNamesIntoTrie(mzID, null, trie); 798 } 799 800 public void addAsTimeZoneIntoTrie(String tzID, TextTrieMap<NameInfo> trie) { 801 addNamesIntoTrie(null, tzID, trie); 802 } 803 804 private void addNamesIntoTrie(String mzID, String tzID, TextTrieMap<NameInfo> trie) { 805 if (_names == null || didAddIntoTrie) { 806 return; 807 } 808 didAddIntoTrie = true; 809 810 for (int i = 0; i < _names.length; ++i) { 811 String name = _names[i]; 812 if (name != null) { 813 NameInfo info = new NameInfo(); 814 info.mzID = mzID; 815 info.tzID = tzID; 816 info.type = getNameType(i); 817 trie.put(name, info); 818 } 819 } 820 } 821 } 822 823 // 824 // Canonical time zone ID -> meta zone ID 825 // 826 827 private static class MZMapEntry { 828 private String _mzID; 829 private long _from; 830 private long _to; 831 832 MZMapEntry(String mzID, long from, long to) { 833 _mzID = mzID; 834 _from = from; 835 _to = to; 836 } 837 838 String mzID() { 839 return _mzID; 840 } 841 842 long from() { 843 return _from; 844 } 845 846 long to() { 847 return _to; 848 } 849 } 850 851 private static class TZ2MZsCache extends SoftCache<String, List<MZMapEntry>, String> { 852 /* (non-Javadoc) 853 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 854 */ 855 @Override 856 protected List<MZMapEntry> createInstance(String key, String data) { 857 List<MZMapEntry> mzMaps = null; 858 859 UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metaZones"); 860 UResourceBundle metazoneInfoBundle = bundle.get("metazoneInfo"); 861 862 String tzkey = data.replace('/', ':'); 863 try { 864 UResourceBundle zoneBundle = metazoneInfoBundle.get(tzkey); 865 866 mzMaps = new ArrayList<MZMapEntry>(zoneBundle.getSize()); 867 for (int idx = 0; idx < zoneBundle.getSize(); idx++) { 868 UResourceBundle mz = zoneBundle.get(idx); 869 String mzid = mz.getString(0); 870 String fromStr = "1970-01-01 00:00"; 871 String toStr = "9999-12-31 23:59"; 872 if (mz.getSize() == 3) { 873 fromStr = mz.getString(1); 874 toStr = mz.getString(2); 875 } 876 long from, to; 877 from = parseDate(fromStr); 878 to = parseDate(toStr); 879 mzMaps.add(new MZMapEntry(mzid, from, to)); 880 } 881 882 } catch (MissingResourceException mre) { 883 mzMaps = Collections.emptyList(); 884 } 885 return mzMaps; 886 } 887 888 /** 889 * Private static method parsing the date text used by meta zone to 890 * time zone mapping data in locale resource. 891 * 892 * @param text the UTC date text in the format of "yyyy-MM-dd HH:mm", 893 * for example - "1970-01-01 00:00" 894 * @return the date 895 */ 896 private static long parseDate (String text) { 897 int year = 0, month = 0, day = 0, hour = 0, min = 0; 898 int idx; 899 int n; 900 901 // "yyyy" (0 - 3) 902 for (idx = 0; idx <= 3; idx++) { 903 n = text.charAt(idx) - '0'; 904 if (n >= 0 && n < 10) { 905 year = 10*year + n; 906 } else { 907 throw new IllegalArgumentException("Bad year"); 908 } 909 } 910 // "MM" (5 - 6) 911 for (idx = 5; idx <= 6; idx++) { 912 n = text.charAt(idx) - '0'; 913 if (n >= 0 && n < 10) { 914 month = 10*month + n; 915 } else { 916 throw new IllegalArgumentException("Bad month"); 917 } 918 } 919 // "dd" (8 - 9) 920 for (idx = 8; idx <= 9; idx++) { 921 n = text.charAt(idx) - '0'; 922 if (n >= 0 && n < 10) { 923 day = 10*day + n; 924 } else { 925 throw new IllegalArgumentException("Bad day"); 926 } 927 } 928 // "HH" (11 - 12) 929 for (idx = 11; idx <= 12; idx++) { 930 n = text.charAt(idx) - '0'; 931 if (n >= 0 && n < 10) { 932 hour = 10*hour + n; 933 } else { 934 throw new IllegalArgumentException("Bad hour"); 935 } 936 } 937 // "mm" (14 - 15) 938 for (idx = 14; idx <= 15; idx++) { 939 n = text.charAt(idx) - '0'; 940 if (n >= 0 && n < 10) { 941 min = 10*min + n; 942 } else { 943 throw new IllegalArgumentException("Bad minute"); 944 } 945 } 946 947 long date = Grego.fieldsToDay(year, month - 1, day) * Grego.MILLIS_PER_DAY 948 + (long)hour * Grego.MILLIS_PER_HOUR + (long)min * Grego.MILLIS_PER_MINUTE; 949 return date; 950 } 951 } 952 953 // 954 // Meta zone ID -> time zone ID 955 // 956 957 private static class MZ2TZsCache extends SoftCache<String, Map<String, String>, String> { 958 959 /* (non-Javadoc) 960 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 961 */ 962 @Override 963 protected Map<String, String> createInstance(String key, String data) { 964 Map<String, String> map = null; 965 966 UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metaZones"); 967 UResourceBundle mapTimezones = bundle.get("mapTimezones"); 968 969 try { 970 UResourceBundle regionMap = mapTimezones.get(key); 971 972 Set<String> regions = regionMap.keySet(); 973 map = new HashMap<String, String>(regions.size()); 974 975 for (String region : regions) { 976 String tzID = regionMap.getString(region).intern(); 977 map.put(region.intern(), tzID); 978 } 979 } catch (MissingResourceException e) { 980 map = Collections.emptyMap(); 981 } 982 return map; 983 } 984 } 985 986 private static final Pattern LOC_EXCLUSION_PATTERN = Pattern.compile("Etc/.*|SystemV/.*|.*/Riyadh8[7-9]"); 987 988 /** 989 * Default exemplar location name based on time zone ID. 990 * For example, "America/New_York" -> "New York" 991 * @param tzID the time zone ID 992 * @return the exemplar location name or null if location is not available. 993 */ 994 public static String getDefaultExemplarLocationName(String tzID) { 995 if (tzID == null || tzID.length() == 0 || LOC_EXCLUSION_PATTERN.matcher(tzID).matches()) { 996 return null; 997 } 998 999 String location = null; 1000 int sep = tzID.lastIndexOf('/'); 1001 if (sep > 0 && sep + 1 < tzID.length()) { 1002 location = tzID.substring(sep + 1).replace('_', ' '); 1003 } 1004 1005 return location; 1006 } 1007 } 1008