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