1/* GENERATED SOURCE. DO NOT MODIFY. */
2/*
3 *******************************************************************************
4 * Copyright (C) 2011-2014, International Business Machines Corporation and    *
5 * others. All Rights Reserved.                                                *
6 *******************************************************************************
7 */
8package android.icu.impl;
9
10import java.io.IOException;
11import java.io.ObjectInputStream;
12import java.io.Serializable;
13import java.lang.ref.WeakReference;
14import java.text.MessageFormat;
15import java.util.Collection;
16import java.util.EnumSet;
17import java.util.Iterator;
18import java.util.LinkedList;
19import java.util.MissingResourceException;
20import java.util.Set;
21import java.util.concurrent.ConcurrentHashMap;
22
23import android.icu.impl.TextTrieMap.ResultHandler;
24import android.icu.text.LocaleDisplayNames;
25import android.icu.text.TimeZoneFormat.TimeType;
26import android.icu.text.TimeZoneNames;
27import android.icu.text.TimeZoneNames.MatchInfo;
28import android.icu.text.TimeZoneNames.NameType;
29import android.icu.util.BasicTimeZone;
30import android.icu.util.Freezable;
31import android.icu.util.Output;
32import android.icu.util.TimeZone;
33import android.icu.util.TimeZone.SystemTimeZoneType;
34import android.icu.util.TimeZoneTransition;
35import android.icu.util.ULocale;
36
37/**
38 * This class interact with TimeZoneNames and LocaleDisplayNames
39 * to format and parse time zone's generic display names.
40 * It is not recommended to use this class directly, instead
41 * use android.icu.text.TimeZoneFormat.
42 * @hide Only a subset of ICU is exposed in Android
43 */
44public class TimeZoneGenericNames implements Serializable, Freezable<TimeZoneGenericNames> {
45
46    // Note: This class implements Serializable, but we no longer serialize instance of
47    // TimeZoneGenericNames in ICU 49. ICU 4.8 android.icu.text.TimeZoneFormat used to
48    // serialize TimeZoneGenericNames field. TimeZoneFormat no longer read TimeZoneGenericNames
49    // field, we have to keep TimeZoneGenericNames Serializable. Otherwise it fails to read
50    // (unused) TimeZoneGenericNames serialized data.
51
52    private static final long serialVersionUID = 2729910342063468417L;
53
54    /**
55     * Generic name type enum
56     */
57    public enum GenericNameType {
58        LOCATION ("LONG", "SHORT"),
59        LONG (),
60        SHORT ();
61
62        String[] _fallbackTypeOf;
63        GenericNameType(String... fallbackTypeOf) {
64            _fallbackTypeOf = fallbackTypeOf;
65        }
66
67        public boolean isFallbackTypeOf(GenericNameType type) {
68            String typeStr = type.toString();
69            for (String t : _fallbackTypeOf) {
70                if (t.equals(typeStr)) {
71                    return true;
72                }
73            }
74            return false;
75        }
76    }
77
78    /**
79     * Format pattern enum used for composing location and partial location names
80     */
81    public enum Pattern {
82        // The format pattern such as "{0} Time", where {0} is the country or city.
83        REGION_FORMAT("regionFormat", "({0})"),
84
85        // Note: FALLBACK_REGION_FORMAT is no longer used since ICU 50/CLDR 22.1
86        // The format pattern such as "{1} Time ({0})", where {1} is the country and {0} is a city.
87        //FALLBACK_REGION_FORMAT("fallbackRegionFormat", "{1} ({0})"),
88
89        // The format pattern such as "{1} ({0})", where {1} is the metazone, and {0} is the country or city.
90        FALLBACK_FORMAT("fallbackFormat", "{1} ({0})");
91
92        String _key;
93        String _defaultVal;
94
95        Pattern(String key, String defaultVal) {
96            _key = key;
97            _defaultVal = defaultVal;
98        }
99
100        String key() {
101            return _key;
102        }
103
104        String defaultValue() {
105            return _defaultVal;
106        }
107    }
108
109    private ULocale _locale;
110    private TimeZoneNames _tznames;
111
112    private transient volatile boolean _frozen;
113    private transient String _region;
114    private transient WeakReference<LocaleDisplayNames> _localeDisplayNamesRef;
115    private transient MessageFormat[] _patternFormatters;
116
117    private transient ConcurrentHashMap<String, String> _genericLocationNamesMap;
118    private transient ConcurrentHashMap<String, String> _genericPartialLocationNamesMap;
119    private transient TextTrieMap<NameInfo> _gnamesTrie;
120    private transient boolean _gnamesTrieFullyLoaded;
121
122    private static Cache GENERIC_NAMES_CACHE = new Cache();
123
124    // Window size used for DST check for a zone in a metazone (about a half year)
125    private static final long DST_CHECK_RANGE = 184L*(24*60*60*1000);
126
127    private static final NameType[] GENERIC_NON_LOCATION_TYPES =
128                                {NameType.LONG_GENERIC, NameType.SHORT_GENERIC};
129
130
131    /**
132     * Constructs a <code>TimeZoneGenericNames</code> with the given locale
133     * and the <code>TimeZoneNames</code>.
134     * @param locale the locale
135     * @param tznames the TimeZoneNames
136     */
137    public TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames) {
138        _locale = locale;
139        _tznames = tznames;
140        init();
141    }
142
143    /**
144     * Private method initializing the instance of <code>TimeZoneGenericName</code>.
145     * This method should be called from a constructor and readObject.
146     */
147    private void init() {
148        if (_tznames == null) {
149            _tznames = TimeZoneNames.getInstance(_locale);
150        }
151        _genericLocationNamesMap = new ConcurrentHashMap<String, String>();
152        _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>();
153
154        _gnamesTrie = new TextTrieMap<NameInfo>(true);
155        _gnamesTrieFullyLoaded = false;
156
157        // Preload zone strings for the default time zone
158        TimeZone tz = TimeZone.getDefault();
159        String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
160        if (tzCanonicalID != null) {
161            loadStrings(tzCanonicalID);
162        }
163    }
164
165    /**
166     * Constructs a <code>TimeZoneGenericNames</code> with the given locale.
167     * This constructor is private and called from {@link #getInstance(ULocale)}.
168     * @param locale the locale
169     */
170    private TimeZoneGenericNames(ULocale locale) {
171        this(locale, null);
172    }
173
174    /**
175     * The factory method of <code>TimeZoneGenericNames</code>. This static method
176     * returns a frozen instance of cached <code>TimeZoneGenericNames</code>.
177     * @param locale the locale
178     * @return A frozen <code>TimeZoneGenericNames</code>.
179     */
180    public static TimeZoneGenericNames getInstance(ULocale locale) {
181        String key = locale.getBaseName();
182        return GENERIC_NAMES_CACHE.getInstance(key, locale);
183    }
184
185    /**
186     * Returns the display name of the time zone for the given name type
187     * at the given date, or null if the display name is not available.
188     *
189     * @param tz the time zone
190     * @param type the generic name type - see {@link GenericNameType}
191     * @param date the date
192     * @return the display name of the time zone for the given name type
193     * at the given date, or null.
194     */
195    public String getDisplayName(TimeZone tz, GenericNameType type, long date) {
196        String name = null;
197        String tzCanonicalID = null;
198        switch (type) {
199        case LOCATION:
200            tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
201            if (tzCanonicalID != null) {
202                name = getGenericLocationName(tzCanonicalID);
203            }
204            break;
205        case LONG:
206        case SHORT:
207            name = formatGenericNonLocationName(tz, type, date);
208            if (name == null) {
209                tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
210                if (tzCanonicalID != null) {
211                    name = getGenericLocationName(tzCanonicalID);
212                }
213            }
214            break;
215        }
216        return name;
217    }
218
219    /**
220     * Returns the generic location name for the given canonical time zone ID.
221     *
222     * @param canonicalTzID the canonical time zone ID
223     * @return the generic location name for the given canonical time zone ID.
224     */
225    public String getGenericLocationName(String canonicalTzID) {
226        if (canonicalTzID == null || canonicalTzID.length() == 0) {
227            return null;
228        }
229        String name = _genericLocationNamesMap.get(canonicalTzID);
230        if (name != null) {
231            if (name.length() == 0) {
232                // empty string to indicate the name is not available
233                return null;
234            }
235            return name;
236        }
237
238        Output<Boolean> isPrimary = new Output<Boolean>();
239        String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID, isPrimary);
240        if (countryCode != null) {
241            if (isPrimary.value) {
242                // If this is only the single zone in the country, use the country name
243                String country = getLocaleDisplayNames().regionDisplayName(countryCode);
244                name = formatPattern(Pattern.REGION_FORMAT, country);
245            } else {
246                // If there are multiple zones including this in the country,
247                // use the exemplar city name
248
249                // getExemplarLocationName should return non-empty String
250                // if the time zone is associated with a location
251                String city = _tznames.getExemplarLocationName(canonicalTzID);
252                name = formatPattern(Pattern.REGION_FORMAT, city);
253            }
254        }
255
256        if (name == null) {
257            _genericLocationNamesMap.putIfAbsent(canonicalTzID.intern(), "");
258        } else {
259            synchronized (this) {   // we have to sync the name map and the trie
260                canonicalTzID = canonicalTzID.intern();
261                String tmp = _genericLocationNamesMap.putIfAbsent(canonicalTzID, name.intern());
262                if (tmp == null) {
263                    // Also put the name info the to trie
264                    NameInfo info = new NameInfo();
265                    info.tzID = canonicalTzID;
266                    info.type = GenericNameType.LOCATION;
267                    _gnamesTrie.put(name, info);
268                } else {
269                    name = tmp;
270                }
271            }
272        }
273        return name;
274    }
275
276    /**
277     * Sets the pattern string for the pattern type.
278     * Note: This method is designed for CLDR ST - not for common use.
279     * @param patType the pattern type
280     * @param patStr the pattern string
281     * @return this object.
282     */
283    public TimeZoneGenericNames setFormatPattern(Pattern patType, String patStr) {
284        if (isFrozen()) {
285            throw new UnsupportedOperationException("Attempt to modify frozen object");
286        }
287
288        // Changing pattern will invalidates cached names
289        if (!_genericLocationNamesMap.isEmpty()) {
290            _genericLocationNamesMap = new ConcurrentHashMap<String, String>();
291        }
292        if (!_genericPartialLocationNamesMap.isEmpty()) {
293            _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>();
294        }
295        _gnamesTrie = null;
296        _gnamesTrieFullyLoaded = false;
297
298        if (_patternFormatters == null) {
299            _patternFormatters = new MessageFormat[Pattern.values().length];
300        }
301        _patternFormatters[patType.ordinal()] = new MessageFormat(patStr);
302        return this;
303    }
304
305    /**
306     * Private method to get a generic string, with fallback logics involved,
307     * that is,
308     *
309     * 1. If a generic non-location string is available for the zone, return it.
310     * 2. If a generic non-location string is associated with a meta zone and
311     *    the zone never use daylight time around the given date, use the standard
312     *    string (if available).
313     * 3. If a generic non-location string is associated with a meta zone and
314     *    the offset at the given time is different from the preferred zone for the
315     *    current locale, then return the generic partial location string (if available)
316     * 4. If a generic non-location string is not available, use generic location
317     *    string.
318     *
319     * @param tz the requested time zone
320     * @param date the date
321     * @param type the generic name type, either LONG or SHORT
322     * @return the name used for a generic name type, which could be the
323     * generic name, or the standard name (if the zone does not observes DST
324     * around the date), or the partial location name.
325     */
326    private String formatGenericNonLocationName(TimeZone tz, GenericNameType type, long date) {
327        assert(type == GenericNameType.LONG || type == GenericNameType.SHORT);
328        String tzID = ZoneMeta.getCanonicalCLDRID(tz);
329
330        if (tzID == null) {
331            return null;
332        }
333
334        // Try to get a name from time zone first
335        NameType nameType = (type == GenericNameType.LONG) ? NameType.LONG_GENERIC : NameType.SHORT_GENERIC;
336        String name = _tznames.getTimeZoneDisplayName(tzID, nameType);
337
338        if (name != null) {
339            return name;
340        }
341
342        // Try meta zone
343        String mzID = _tznames.getMetaZoneID(tzID, date);
344        if (mzID != null) {
345            boolean useStandard = false;
346            int[] offsets = {0, 0};
347            tz.getOffset(date, false, offsets);
348
349            if (offsets[1] == 0) {
350                useStandard = true;
351                // Check if the zone actually uses daylight saving time around the time
352                if (tz instanceof BasicTimeZone) {
353                    BasicTimeZone btz = (BasicTimeZone)tz;
354                    TimeZoneTransition before = btz.getPreviousTransition(date, true);
355                    if (before != null
356                            && (date - before.getTime() < DST_CHECK_RANGE)
357                            && before.getFrom().getDSTSavings() != 0) {
358                        useStandard = false;
359                    } else {
360                        TimeZoneTransition after = btz.getNextTransition(date, false);
361                        if (after != null
362                                && (after.getTime() - date < DST_CHECK_RANGE)
363                                && after.getTo().getDSTSavings() != 0) {
364                            useStandard = false;
365                        }
366                    }
367                } else {
368                    // If not BasicTimeZone... only if the instance is not an ICU's implementation.
369                    // We may get a wrong answer in edge case, but it should practically work OK.
370                    int[] tmpOffsets = new int[2];
371                    tz.getOffset(date - DST_CHECK_RANGE, false, tmpOffsets);
372                    if (tmpOffsets[1] != 0) {
373                        useStandard = false;
374                    } else {
375                        tz.getOffset(date + DST_CHECK_RANGE, false, tmpOffsets);
376                        if (tmpOffsets[1] != 0){
377                            useStandard = false;
378                        }
379                    }
380                }
381            }
382            if (useStandard) {
383                NameType stdNameType = (nameType == NameType.LONG_GENERIC) ?
384                        NameType.LONG_STANDARD : NameType.SHORT_STANDARD;
385                String stdName = _tznames.getDisplayName(tzID, stdNameType, date);
386                if (stdName != null) {
387                    name = stdName;
388
389                    // TODO: revisit this issue later
390                    // In CLDR, a same display name is used for both generic and standard
391                    // for some meta zones in some locales.  This looks like a data bugs.
392                    // For now, we check if the standard name is different from its generic
393                    // name below.
394                    String mzGenericName = _tznames.getMetaZoneDisplayName(mzID, nameType);
395                    if (stdName.equalsIgnoreCase(mzGenericName)) {
396                        name = null;
397                    }
398                }
399            }
400
401            if (name == null) {
402                // Get a name from meta zone
403                String mzName = _tznames.getMetaZoneDisplayName(mzID, nameType);
404                if (mzName != null) {
405                    // Check if we need to use a partial location format.
406                    // This check is done by comparing offset with the meta zone's
407                    // golden zone at the given date.
408                    String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
409                    if (goldenID != null && !goldenID.equals(tzID)) {
410                        TimeZone goldenZone = TimeZone.getFrozenTimeZone(goldenID);
411                        int[] offsets1 = {0, 0};
412
413                        // Check offset in the golden zone with wall time.
414                        // With getOffset(date, false, offsets1),
415                        // you may get incorrect results because of time overlap at DST->STD
416                        // transition.
417                        goldenZone.getOffset(date + offsets[0] + offsets[1], true, offsets1);
418
419                        if (offsets[0] != offsets1[0] || offsets[1] != offsets1[1]) {
420                            // Now we need to use a partial location format.
421                            name = getPartialLocationName(tzID, mzID, (nameType == NameType.LONG_GENERIC), mzName);
422                        } else {
423                            name = mzName;
424                        }
425                    } else {
426                        name = mzName;
427                    }
428                }
429            }
430        }
431        return name;
432    }
433
434    /**
435     * Private simple pattern formatter used for formatting generic location names
436     * and partial location names. We intentionally use JDK MessageFormat
437     * for performance reason.
438     *
439     * @param pat the message pattern enum
440     * @param args the format argument(s)
441     * @return the formatted string
442     */
443    private synchronized String formatPattern(Pattern pat, String... args) {
444        if (_patternFormatters == null) {
445            _patternFormatters = new MessageFormat[Pattern.values().length];
446        }
447
448        int idx = pat.ordinal();
449        if (_patternFormatters[idx] == null) {
450            String patText;
451            try {
452                ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance(
453                    ICUResourceBundle.ICU_ZONE_BASE_NAME, _locale);
454                patText = bundle.getStringWithFallback("zoneStrings/" + pat.key());
455            } catch (MissingResourceException e) {
456                patText = pat.defaultValue();
457            }
458
459            _patternFormatters[idx] = new MessageFormat(patText);
460        }
461        return _patternFormatters[idx].format(args);
462    }
463
464    /**
465     * Private method returning LocaleDisplayNames instance for the locale of this
466     * instance. Because LocaleDisplayNames is only used for generic
467     * location formant and partial location format, the LocaleDisplayNames
468     * is instantiated lazily.
469     *
470     * @return the instance of LocaleDisplayNames for the locale of this object.
471     */
472    private synchronized LocaleDisplayNames getLocaleDisplayNames() {
473        LocaleDisplayNames locNames = null;
474        if (_localeDisplayNamesRef != null) {
475            locNames = _localeDisplayNamesRef.get();
476        }
477        if (locNames == null) {
478            locNames = LocaleDisplayNames.getInstance(_locale);
479            _localeDisplayNamesRef = new WeakReference<LocaleDisplayNames>(locNames);
480        }
481        return locNames;
482    }
483
484    private synchronized void loadStrings(String tzCanonicalID) {
485        if (tzCanonicalID == null || tzCanonicalID.length() == 0) {
486            return;
487        }
488        // getGenericLocationName() formats a name and put it into the trie
489        getGenericLocationName(tzCanonicalID);
490
491        // Generic partial location format
492        Set<String> mzIDs = _tznames.getAvailableMetaZoneIDs(tzCanonicalID);
493        for (String mzID : mzIDs) {
494            // if this time zone is not the golden zone of the meta zone,
495            // partial location name (such as "PT (Los Angeles)") might be
496            // available.
497            String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
498            if (!tzCanonicalID.equals(goldenID)) {
499                for (NameType genNonLocType : GENERIC_NON_LOCATION_TYPES) {
500                    String mzGenName = _tznames.getMetaZoneDisplayName(mzID, genNonLocType);
501                    if (mzGenName != null) {
502                        // getPartialLocationName() formats a name and put it into the trie
503                        getPartialLocationName(tzCanonicalID, mzID, (genNonLocType == NameType.LONG_GENERIC), mzGenName);
504                    }
505                }
506            }
507        }
508    }
509
510    /**
511     * Private method returning the target region. The target regions is determined by
512     * the locale of this instance. When a generic name is coming from
513     * a meta zone, this region is used for checking if the time zone
514     * is a reference zone of the meta zone.
515     *
516     * @return the target region
517     */
518    private synchronized String getTargetRegion() {
519        if (_region == null) {
520            _region = _locale.getCountry();
521            if (_region.length() == 0) {
522                ULocale tmp = ULocale.addLikelySubtags(_locale);
523                _region = tmp.getCountry();
524                if (_region.length() == 0) {
525                    _region = "001";
526                }
527            }
528        }
529        return _region;
530    }
531
532    /**
533     * Private method for formatting partial location names. This format
534     * is used when a generic name of a meta zone is available, but the given
535     * time zone is not a reference zone (golden zone) of the meta zone.
536     *
537     * @param tzID the canonical time zone ID
538     * @param mzID the meta zone ID
539     * @param isLong true when long generic name
540     * @param mzDisplayName the meta zone generic display name
541     * @return the partial location format string
542     */
543    private String getPartialLocationName(String tzID, String mzID, boolean isLong, String mzDisplayName) {
544        String letter = isLong ? "L" : "S";
545        String key = tzID + "&" + mzID + "#" + letter;
546        String name = _genericPartialLocationNamesMap.get(key);
547        if (name != null) {
548            return name;
549        }
550        String location = null;
551        String countryCode = ZoneMeta.getCanonicalCountry(tzID);
552        if (countryCode != null) {
553            // Is this the golden zone for the region?
554            String regionalGolden = _tznames.getReferenceZoneID(mzID, countryCode);
555            if (tzID.equals(regionalGolden)) {
556                // Use country name
557                location = getLocaleDisplayNames().regionDisplayName(countryCode);
558            } else {
559                // Otherwise, use exemplar city name
560                location = _tznames.getExemplarLocationName(tzID);
561            }
562        } else {
563            location = _tznames.getExemplarLocationName(tzID);
564            if (location == null) {
565                // This could happen when the time zone is not associated with a country,
566                // and its ID is not hierarchical, for example, CST6CDT.
567                // We use the canonical ID itself as the location for this case.
568                location = tzID;
569            }
570        }
571        name = formatPattern(Pattern.FALLBACK_FORMAT, location, mzDisplayName);
572        synchronized (this) {   // we have to sync the name map and the trie
573            String tmp = _genericPartialLocationNamesMap.putIfAbsent(key.intern(), name.intern());
574            if (tmp == null) {
575                NameInfo info = new NameInfo();
576                info.tzID = tzID.intern();
577                info.type = isLong ? GenericNameType.LONG : GenericNameType.SHORT;
578                _gnamesTrie.put(name, info);
579            } else {
580                name = tmp;
581            }
582        }
583        return name;
584    }
585
586    /**
587     * A private class used for storing the name information in the local trie.
588     */
589    private static class NameInfo {
590        String tzID;
591        GenericNameType type;
592    }
593
594    /**
595     * A class used for returning the name search result used by
596     * {@link TimeZoneGenericNames#find(String, int, EnumSet)}.
597     */
598    public static class GenericMatchInfo {
599        GenericNameType nameType;
600        String tzID;
601        int matchLength;
602        TimeType timeType = TimeType.UNKNOWN;
603
604        public GenericNameType nameType() {
605            return nameType;
606        }
607
608        public String tzID() {
609            return tzID;
610        }
611
612        public TimeType timeType() {
613            return timeType;
614        }
615
616        public int matchLength() {
617            return matchLength;
618        }
619    }
620
621    /**
622     * A private class implementing the search callback interface in
623     * <code>TextTrieMap</code> for collecting match results.
624     */
625    private static class GenericNameSearchHandler implements ResultHandler<NameInfo> {
626        private EnumSet<GenericNameType> _types;
627        private Collection<GenericMatchInfo> _matches;
628        private int _maxMatchLen;
629
630        GenericNameSearchHandler(EnumSet<GenericNameType> types) {
631            _types = types;
632        }
633
634        /* (non-Javadoc)
635         * @see android.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator)
636         */
637        public boolean handlePrefixMatch(int matchLength, Iterator<NameInfo> values) {
638            while (values.hasNext()) {
639                NameInfo info = values.next();
640                if (_types != null && !_types.contains(info.type)) {
641                    continue;
642                }
643                GenericMatchInfo matchInfo = new GenericMatchInfo();
644                matchInfo.tzID = info.tzID;
645                matchInfo.nameType = info.type;
646                matchInfo.matchLength = matchLength;
647                //matchInfo.timeType = TimeType.UNKNOWN;
648                if (_matches == null) {
649                    _matches = new LinkedList<GenericMatchInfo>();
650                }
651                _matches.add(matchInfo);
652                if (matchLength > _maxMatchLen) {
653                    _maxMatchLen = matchLength;
654                }
655            }
656            return true;
657        }
658
659        /**
660         * Returns the match results
661         * @return the match results
662         */
663        public Collection<GenericMatchInfo> getMatches() {
664            return _matches;
665        }
666
667        /**
668         * Returns the maximum match length, or 0 if no match was found
669         * @return the maximum match length
670         */
671        public int getMaxMatchLen() {
672            return _maxMatchLen;
673        }
674
675        /**
676         * Resets the match results
677         */
678        public void resetResults() {
679            _matches = null;
680            _maxMatchLen = 0;
681        }
682    }
683
684    /**
685     * Returns the best match of time zone display name for the specified types in the
686     * given text at the given offset.
687     * @param text the text
688     * @param start the start offset in the text
689     * @param genericTypes the set of name types.
690     * @return the best matching name info.
691     */
692    public GenericMatchInfo findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes) {
693        if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
694            throw new IllegalArgumentException("bad input text or range");
695        }
696        GenericMatchInfo bestMatch = null;
697
698        // Find matches in the TimeZoneNames first
699        Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes);
700        if (tznamesMatches != null) {
701            MatchInfo longestMatch = null;
702            for (MatchInfo match : tznamesMatches) {
703                if (longestMatch == null || match.matchLength() > longestMatch.matchLength()) {
704                    longestMatch = match;
705                }
706            }
707            if (longestMatch != null) {
708                bestMatch = createGenericMatchInfo(longestMatch);
709                if (bestMatch.matchLength() == (text.length() - start)) {
710                    // Full match
711                    //return bestMatch;
712
713                    // TODO Some time zone uses a same name for the long standard name
714                    // and the location name. When the match is a long standard name,
715                    // then we need to check if the name is same with the location name.
716                    // This is probably a data error or a design bug.
717//                    if (bestMatch.nameType != GenericNameType.LONG || bestMatch.timeType != TimeType.STANDARD) {
718//                        return bestMatch;
719//                    }
720
721                    // TODO The deprecation of commonlyUsed flag introduced the name
722                    // conflict not only for long standard names, but short standard names too.
723                    // These short names (found in zh_Hant) should be gone once we clean
724                    // up CLDR time zone display name data. Once the short name conflict
725                    // problem (with location name) is resolved, we should change the condition
726                    // below back to the original one above. -Yoshito (2011-09-14)
727                    if (bestMatch.timeType != TimeType.STANDARD) {
728                        return bestMatch;
729                    }
730                }
731            }
732        }
733
734        // Find matches in the local trie
735        Collection<GenericMatchInfo> localMatches = findLocal(text, start, genericTypes);
736        if (localMatches != null) {
737            for (GenericMatchInfo match : localMatches) {
738                // TODO See the above TODO. We use match.matchLength() >= bestMatch.matcheLength()
739                // for the reason described above.
740                //if (bestMatch == null || match.matchLength() > bestMatch.matchLength()) {
741                if (bestMatch == null || match.matchLength() >= bestMatch.matchLength()) {
742                    bestMatch = match;
743                }
744            }
745        }
746
747        return bestMatch;
748    }
749
750    /**
751     * Returns a collection of time zone display name matches for the specified types in the
752     * given text at the given offset.
753     * @param text the text
754     * @param start the start offset in the text
755     * @param genericTypes the set of name types.
756     * @return A collection of match info.
757     */
758    public Collection<GenericMatchInfo> find(String text, int start, EnumSet<GenericNameType> genericTypes) {
759        if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
760            throw new IllegalArgumentException("bad input text or range");
761        }
762        // Find matches in the local trie
763        Collection<GenericMatchInfo> results = findLocal(text, start, genericTypes);
764
765        // Also find matches in the TimeZoneNames
766        Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes);
767        if (tznamesMatches != null) {
768            // transform matches and append them to local matches
769            for (MatchInfo match : tznamesMatches) {
770                if (results == null) {
771                    results = new LinkedList<GenericMatchInfo>();
772                }
773                results.add(createGenericMatchInfo(match));
774            }
775        }
776        return results;
777    }
778
779    /**
780     * Returns a <code>GenericMatchInfo</code> for the given <code>MatchInfo</code>.
781     * @param matchInfo the MatchInfo
782     * @return A GenericMatchInfo
783     */
784    private GenericMatchInfo createGenericMatchInfo(MatchInfo matchInfo) {
785        GenericNameType nameType = null;
786        TimeType timeType = TimeType.UNKNOWN;
787        switch (matchInfo.nameType()) {
788        case LONG_STANDARD:
789            nameType = GenericNameType.LONG;
790            timeType = TimeType.STANDARD;
791            break;
792        case LONG_GENERIC:
793            nameType = GenericNameType.LONG;
794            break;
795        case SHORT_STANDARD:
796            nameType = GenericNameType.SHORT;
797            timeType = TimeType.STANDARD;
798            break;
799        case SHORT_GENERIC:
800            nameType = GenericNameType.SHORT;
801            break;
802        default:
803            throw new IllegalArgumentException("Unexpected MatchInfo name type - " + matchInfo.nameType());
804        }
805
806        String tzID = matchInfo.tzID();
807        if (tzID == null) {
808            String mzID = matchInfo.mzID();
809            assert(mzID != null);
810            tzID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
811        }
812        assert(tzID != null);
813
814        GenericMatchInfo gmatch = new GenericMatchInfo();
815        gmatch.nameType = nameType;
816        gmatch.tzID = tzID;
817        gmatch.matchLength = matchInfo.matchLength();
818        gmatch.timeType = timeType;
819
820        return gmatch;
821    }
822
823    /**
824     * Returns a collection of time zone display name matches for the specified types in the
825     * given text at the given offset. This method only finds matches from the TimeZoneNames
826     * used by this object.
827     * @param text the text
828     * @param start the start offset in the text
829     * @param types the set of name types.
830     * @return A collection of match info.
831     */
832    private Collection<MatchInfo> findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types) {
833        Collection<MatchInfo> tznamesMatches = null;
834
835        // Check if the target name type is really in the TimeZoneNames
836        EnumSet<NameType> nameTypes = EnumSet.noneOf(NameType.class);
837        if (types.contains(GenericNameType.LONG)) {
838            nameTypes.add(NameType.LONG_GENERIC);
839            nameTypes.add(NameType.LONG_STANDARD);
840        }
841        if (types.contains(GenericNameType.SHORT)) {
842            nameTypes.add(NameType.SHORT_GENERIC);
843            nameTypes.add(NameType.SHORT_STANDARD);
844        }
845
846        if (!nameTypes.isEmpty()) {
847            // Find matches in the TimeZoneNames
848            tznamesMatches = _tznames.find(text, start, nameTypes);
849        }
850        return tznamesMatches;
851    }
852
853    /**
854     * Returns a collection of time zone display name matches for the specified types in the
855     * given text at the given offset. This method only finds matches from the local trie,
856     * that contains 1) generic location names and 2) long/short generic partial location names,
857     * used by this object.
858     * @param text the text
859     * @param start the start offset in the text
860     * @param types the set of name types.
861     * @return A collection of match info.
862     */
863    private synchronized Collection<GenericMatchInfo> findLocal(String text, int start, EnumSet<GenericNameType> types) {
864        GenericNameSearchHandler handler = new GenericNameSearchHandler(types);
865        _gnamesTrie.find(text, start, handler);
866        if (handler.getMaxMatchLen() == (text.length() - start) || _gnamesTrieFullyLoaded) {
867            // perfect match
868            return handler.getMatches();
869        }
870
871        // All names are not yet loaded into the local trie.
872        // Load all available names into the trie. This could be very heavy.
873
874        Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);
875        for (String tzID : tzIDs) {
876            loadStrings(tzID);
877        }
878        _gnamesTrieFullyLoaded = true;
879
880        // now, try it again
881        handler.resetResults();
882        _gnamesTrie.find(text, start, handler);
883        return handler.getMatches();
884    }
885
886    /**
887     * <code>TimeZoneGenericNames</code> cache implementation.
888     */
889    private static class Cache extends SoftCache<String, TimeZoneGenericNames, ULocale> {
890
891        /* (non-Javadoc)
892         * @see android.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
893         */
894        @Override
895        protected TimeZoneGenericNames createInstance(String key, ULocale data) {
896            return new TimeZoneGenericNames(data).freeze();
897        }
898
899    }
900
901    /*
902     * The custom deserialization method.
903     * This implementation only read locale used by the object.
904     */
905    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
906        in.defaultReadObject();
907        init();
908    }
909
910    /**
911     * {@inheritDoc}
912     */
913    public boolean isFrozen() {
914        return _frozen;
915    }
916
917    /**
918     * {@inheritDoc}
919     */
920    public TimeZoneGenericNames freeze() {
921        _frozen = true;
922        return this;
923    }
924
925    /**
926     * {@inheritDoc}
927     */
928    public TimeZoneGenericNames cloneAsThawed() {
929        TimeZoneGenericNames copy = null;
930        try {
931            copy = (TimeZoneGenericNames)super.clone();
932            copy._frozen = false;
933        } catch (Throwable t) {
934            // This should never happen
935        }
936        return copy;
937    }
938}
939