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