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) 2014-2016, International Business Machines Corporation and
      6  * others. All Rights Reserved.
      7  *******************************************************************************
      8  */
      9 package com.ibm.icu.impl;
     10 
     11 import java.util.Collection;
     12 import java.util.Collections;
     13 import java.util.EnumSet;
     14 import java.util.Iterator;
     15 import java.util.LinkedList;
     16 import java.util.MissingResourceException;
     17 import java.util.Set;
     18 import java.util.concurrent.ConcurrentHashMap;
     19 
     20 import com.ibm.icu.impl.TextTrieMap.ResultHandler;
     21 import com.ibm.icu.text.TimeZoneNames;
     22 import com.ibm.icu.util.ULocale;
     23 import com.ibm.icu.util.UResourceBundle;
     24 
     25 /**
     26  * Yet another TimeZoneNames implementation based on the tz database.
     27  * This implementation contains only tz abbreviations (short standard
     28  * and daylight names) for each metazone.
     29  *
     30  * The data file $ICU4C_ROOT/source/data/zone/tzdbNames.txt contains
     31  * the metazone - abbreviations mapping data (manually edited).
     32  *
     33  * Note: The abbreviations in the tz database are not necessarily
     34  * unique. For example, parsing abbreviation "IST" is ambiguous
     35  * (can be parsed as India Standard Time or Israel Standard Time).
     36  * The data file (tzdbNames.txt) contains regional mapping, and
     37  * the locale in the constructor is used as a hint for resolving
     38  * these ambiguous names.
     39  */
     40 public class TZDBTimeZoneNames extends TimeZoneNames {
     41     private static final long serialVersionUID = 1L;
     42 
     43     private static final ConcurrentHashMap<String, TZDBNames> TZDB_NAMES_MAP =
     44             new ConcurrentHashMap<String, TZDBNames>();
     45 
     46     private static volatile TextTrieMap<TZDBNameInfo> TZDB_NAMES_TRIE = null;
     47 
     48     private static final ICUResourceBundle ZONESTRINGS;
     49     static {
     50         UResourceBundle bundle = ICUResourceBundle
     51                 .getBundleInstance(ICUData.ICU_ZONE_BASE_NAME, "tzdbNames");
     52         ZONESTRINGS = (ICUResourceBundle)bundle.get("zoneStrings");
     53     }
     54 
     55     private ULocale _locale;
     56     private transient volatile String _region;
     57 
     58     public TZDBTimeZoneNames(ULocale loc) {
     59         _locale = loc;
     60     }
     61 
     62     /* (non-Javadoc)
     63      * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs()
     64      */
     65     @Override
     66     public Set<String> getAvailableMetaZoneIDs() {
     67         return TimeZoneNamesImpl._getAvailableMetaZoneIDs();
     68     }
     69 
     70     /* (non-Javadoc)
     71      * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs(java.lang.String)
     72      */
     73     @Override
     74     public Set<String> getAvailableMetaZoneIDs(String tzID) {
     75         return TimeZoneNamesImpl._getAvailableMetaZoneIDs(tzID);
     76     }
     77 
     78     /* (non-Javadoc)
     79      * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneID(java.lang.String, long)
     80      */
     81     @Override
     82     public String getMetaZoneID(String tzID, long date) {
     83         return TimeZoneNamesImpl._getMetaZoneID(tzID, date);
     84     }
     85 
     86     /* (non-Javadoc)
     87      * @see com.ibm.icu.text.TimeZoneNames#getReferenceZoneID(java.lang.String, java.lang.String)
     88      */
     89     @Override
     90     public String getReferenceZoneID(String mzID, String region) {
     91         return TimeZoneNamesImpl._getReferenceZoneID(mzID, region);
     92     }
     93 
     94     /* (non-Javadoc)
     95      * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneDisplayName(java.lang.String,
     96      *      com.ibm.icu.text.TimeZoneNames.NameType)
     97      */
     98     @Override
     99     public String getMetaZoneDisplayName(String mzID, NameType type) {
    100         if (mzID == null || mzID.length() == 0 ||
    101                 (type != NameType.SHORT_STANDARD && type != NameType.SHORT_DAYLIGHT)) {
    102             return null;
    103         }
    104         return getMetaZoneNames(mzID).getName(type);
    105     }
    106 
    107     /* (non-Javadoc)
    108      * @see com.ibm.icu.text.TimeZoneNames#getTimeZoneDisplayName(java.lang.String,
    109      *      com.ibm.icu.text.TimeZoneNames.NameType)
    110      */
    111     @Override
    112     public String getTimeZoneDisplayName(String tzID, NameType type) {
    113         // No abbreviations associated a zone directly for now.
    114         return null;
    115     }
    116 
    117 //    /* (non-Javadoc)
    118 //     * @see com.ibm.icu.text.TimeZoneNames#getExemplarLocationName(java.lang.String)
    119 //     */
    120 //    public String getExemplarLocationName(String tzID) {
    121 //        return super.getExemplarLocationName(tzID);
    122 //    }
    123 
    124     /* (non-Javadoc)
    125      * @see com.ibm.icu.text.TimeZoneNames#find(java.lang.CharSequence, int, java.util.EnumSet)
    126      */
    127     @Override
    128     public Collection<MatchInfo> find(CharSequence text, int start, EnumSet<NameType> nameTypes) {
    129         if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
    130             throw new IllegalArgumentException("bad input text or range");
    131         }
    132 
    133         prepareFind();
    134         TZDBNameSearchHandler handler = new TZDBNameSearchHandler(nameTypes, getTargetRegion());
    135         TZDB_NAMES_TRIE.find(text, start, handler);
    136         return handler.getMatches();
    137     }
    138 
    139     private static class TZDBNames {
    140         public static final TZDBNames EMPTY_TZDBNAMES = new TZDBNames(null, null);
    141 
    142         private String[] _names;
    143         private String[] _parseRegions;
    144         private static final String[] KEYS = {"ss", "sd"};
    145 
    146         private TZDBNames(String[] names, String[] parseRegions) {
    147             _names = names;
    148             _parseRegions = parseRegions;
    149         }
    150 
    151         static TZDBNames getInstance(ICUResourceBundle zoneStrings, String key) {
    152             if (zoneStrings == null || key == null || key.length() == 0) {
    153                 return EMPTY_TZDBNAMES;
    154             }
    155 
    156             ICUResourceBundle table = null;
    157             try {
    158                 table = (ICUResourceBundle)zoneStrings.get(key);
    159             } catch (MissingResourceException e) {
    160                 return EMPTY_TZDBNAMES;
    161             }
    162 
    163             boolean isEmpty = true;
    164             String[] names = new String[KEYS.length];
    165             for (int i = 0; i < names.length; i++) {
    166                 try {
    167                     names[i] = table.getString(KEYS[i]);
    168                     isEmpty = false;
    169                 } catch (MissingResourceException e) {
    170                     names[i] = null;
    171                 }
    172             }
    173 
    174             if (isEmpty) {
    175                 return EMPTY_TZDBNAMES;
    176             }
    177 
    178             String[] parseRegions = null;
    179             try {
    180                 ICUResourceBundle regionsRes = (ICUResourceBundle)table.get("parseRegions");
    181                 if (regionsRes.getType() == UResourceBundle.STRING) {
    182                     parseRegions = new String[1];
    183                     parseRegions[0] = regionsRes.getString();
    184                 } else if (regionsRes.getType() == UResourceBundle.ARRAY) {
    185                     parseRegions = regionsRes.getStringArray();
    186                 }
    187             } catch (MissingResourceException e) {
    188                 // fall through
    189             }
    190 
    191             return new TZDBNames(names, parseRegions);
    192         }
    193 
    194         String getName(NameType type) {
    195             if (_names == null) {
    196                 return null;
    197             }
    198             String name = null;
    199             switch (type) {
    200             case SHORT_STANDARD:
    201                 name = _names[0];
    202                 break;
    203             case SHORT_DAYLIGHT:
    204                 name = _names[1];
    205                 break;
    206             default:
    207                 // No names for all other types handled by
    208                 // this class.
    209                 break;
    210             }
    211 
    212             return name;
    213         }
    214 
    215         String[] getParseRegions() {
    216             return _parseRegions;
    217         }
    218     }
    219 
    220     private static class TZDBNameInfo {
    221         final String mzID;
    222         final NameType type;
    223         final boolean ambiguousType;
    224         final String[] parseRegions;
    225 
    226         TZDBNameInfo(String mzID, NameType type, boolean ambiguousType, String[] parseRegions) {
    227             this.mzID = mzID;
    228             this.type = type;
    229             this.ambiguousType = ambiguousType;
    230             this.parseRegions = parseRegions;
    231         }
    232     }
    233 
    234     private static class TZDBNameSearchHandler implements ResultHandler<TZDBNameInfo> {
    235         private EnumSet<NameType> _nameTypes;
    236         private Collection<MatchInfo> _matches;
    237         private String _region;
    238 
    239         TZDBNameSearchHandler(EnumSet<NameType> nameTypes, String region) {
    240             _nameTypes = nameTypes;
    241             assert region != null;
    242             _region = region;
    243         }
    244 
    245         /* (non-Javadoc)
    246          * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int,
    247          *      java.util.Iterator)
    248          */
    249         @Override
    250         public boolean handlePrefixMatch(int matchLength, Iterator<TZDBNameInfo> values) {
    251             TZDBNameInfo match = null;
    252             TZDBNameInfo defaultRegionMatch = null;
    253 
    254             while (values.hasNext()) {
    255                 TZDBNameInfo ninfo = values.next();
    256 
    257                 if (_nameTypes != null && !_nameTypes.contains(ninfo.type)) {
    258                     continue;
    259                 }
    260 
    261                 // Some tz database abbreviations are ambiguous. For example,
    262                 // CST means either Central Standard Time or China Standard Time.
    263                 // Unlike CLDR time zone display names, this implementation
    264                 // does not use unique names. And TimeZoneFormat does not expect
    265                 // multiple results returned for the same time zone type.
    266                 // For this reason, this implementation resolve one among same
    267                 // zone type with a same name at this level.
    268                 if (ninfo.parseRegions == null) {
    269                     // parseRegions == null means this is the default metazone
    270                     // mapping for the abbreviation.
    271                     if (defaultRegionMatch == null) {
    272                         match = defaultRegionMatch = ninfo;
    273                     }
    274                 } else {
    275                     boolean matchRegion = false;
    276                     // non-default metazone mapping for an abbreviation
    277                     // comes with applicable regions. For example, the default
    278                     // metazone mapping for "CST" is America_Central,
    279                     // but if region is one of CN/MO/TW, "CST" is parsed
    280                     // as metazone China (China Standard Time).
    281                     for (String region : ninfo.parseRegions) {
    282                         if (_region.equals(region)) {
    283                             match = ninfo;
    284                             matchRegion = true;
    285                             break;
    286                         }
    287                     }
    288                     if (matchRegion) {
    289                         break;
    290                     }
    291                     if (match == null) {
    292                         match = ninfo;
    293                     }
    294                 }
    295             }
    296 
    297             if (match != null) {
    298                 NameType ntype = match.type;
    299                 // Note: Workaround for duplicated standard/daylight names
    300                 // The tz database contains a few zones sharing a
    301                 // same name for both standard time and daylight saving
    302                 // time. For example, Australia/Sydney observes DST,
    303                 // but "EST" is used for both standard and daylight.
    304                 // When both SHORT_STANDARD and SHORT_DAYLIGHT are included
    305                 // in the find operation, we cannot tell which one was
    306                 // actually matched.
    307                 // TimeZoneFormat#parse returns a matched name type (standard
    308                 // or daylight) and DateFormat implementation uses the info to
    309                 // to adjust actual time. To avoid false type information,
    310                 // this implementation replaces the name type with SHORT_GENERIC.
    311                 if (match.ambiguousType
    312                         && (ntype == NameType.SHORT_STANDARD || ntype == NameType.SHORT_DAYLIGHT)
    313                         && _nameTypes.contains(NameType.SHORT_STANDARD)
    314                         && _nameTypes.contains(NameType.SHORT_DAYLIGHT)) {
    315                     ntype = NameType.SHORT_GENERIC;
    316                 }
    317                 MatchInfo minfo = new MatchInfo(ntype, null, match.mzID, matchLength);
    318                 if (_matches == null) {
    319                     _matches = new LinkedList<MatchInfo>();
    320                 }
    321                 _matches.add(minfo);
    322             }
    323 
    324             return true;
    325         }
    326 
    327         /**
    328          * Returns the match results
    329          * @return the match results
    330          */
    331         public Collection<MatchInfo> getMatches() {
    332             if (_matches == null) {
    333                 return Collections.emptyList();
    334             }
    335             return _matches;
    336         }
    337     }
    338 
    339     private static TZDBNames getMetaZoneNames(String mzID) {
    340         TZDBNames names = TZDB_NAMES_MAP.get(mzID);
    341         if (names == null) {
    342             names = TZDBNames.getInstance(ZONESTRINGS, "meta:" + mzID);
    343             mzID = mzID.intern();
    344             TZDBNames tmpNames = TZDB_NAMES_MAP.putIfAbsent(mzID, names);
    345             names = (tmpNames == null) ? names : tmpNames;
    346         }
    347         return names;
    348     }
    349 
    350     private static void prepareFind() {
    351         if (TZDB_NAMES_TRIE == null) {
    352             synchronized(TZDBTimeZoneNames.class) {
    353                 if (TZDB_NAMES_TRIE == null) {
    354                     // loading all names into trie
    355                     TextTrieMap<TZDBNameInfo> trie = new TextTrieMap<TZDBNameInfo>(true);
    356                     Set<String> mzIDs = TimeZoneNamesImpl._getAvailableMetaZoneIDs();
    357                     for (String mzID : mzIDs) {
    358                         TZDBNames names = getMetaZoneNames(mzID);
    359                         String std = names.getName(NameType.SHORT_STANDARD);
    360                         String dst = names.getName(NameType.SHORT_DAYLIGHT);
    361                         if (std == null && dst == null) {
    362                             continue;
    363                         }
    364                         String[] parseRegions = names.getParseRegions();
    365                         mzID = mzID.intern();
    366 
    367                         // The tz database contains a few zones sharing a
    368                         // same name for both standard time and daylight saving
    369                         // time. For example, Australia/Sydney observes DST,
    370                         // but "EST" is used for both standard and daylight.
    371                         // we need to store the information for later processing.
    372                         boolean ambiguousType = (std != null && dst != null && std.equals(dst));
    373 
    374                         if (std != null) {
    375                             TZDBNameInfo stdInf = new TZDBNameInfo(mzID,
    376                                     NameType.SHORT_STANDARD,
    377                                     ambiguousType,
    378                                     parseRegions);
    379                             trie.put(std, stdInf);
    380                         }
    381                         if (dst != null) {
    382                             TZDBNameInfo dstInf = new TZDBNameInfo(mzID,
    383                                     NameType.SHORT_DAYLIGHT,
    384                                     ambiguousType,
    385                                     parseRegions);
    386                             trie.put(dst, dstInf);
    387                         }
    388                     }
    389                     TZDB_NAMES_TRIE = trie;
    390                 }
    391             }
    392         }
    393     }
    394 
    395     private String getTargetRegion() {
    396         if (_region == null) {
    397             String region = _locale.getCountry();
    398             if (region.length() == 0) {
    399                 ULocale tmp = ULocale.addLikelySubtags(_locale);
    400                 region = tmp.getCountry();
    401                 if (region.length() == 0) {
    402                     region = "001";
    403                 }
    404             }
    405             _region = region;
    406         }
    407         return _region;
    408     }
    409 }
    410