Home | History | Annotate | Download | only in impl
      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