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