]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-52_1/main/classes/core/src/com/ibm/icu/impl/TimeZoneGenericNames.java
Upgrade ICU4J.
[Dictionary.git] / jars / icu4j-52_1 / main / classes / core / src / com / ibm / icu / impl / TimeZoneGenericNames.java
1 /*
2  *******************************************************************************
3  * Copyright (C) 2011-2013, International Business Machines Corporation and    *
4  * others. All Rights Reserved.                                                *
5  *******************************************************************************
6  */
7 package com.ibm.icu.impl;
8
9 import java.io.IOException;
10 import java.io.ObjectInputStream;
11 import java.io.Serializable;
12 import java.lang.ref.WeakReference;
13 import java.text.MessageFormat;
14 import java.util.Collection;
15 import java.util.EnumSet;
16 import java.util.Iterator;
17 import java.util.LinkedList;
18 import java.util.MissingResourceException;
19 import java.util.Set;
20 import java.util.concurrent.ConcurrentHashMap;
21
22 import com.ibm.icu.impl.TextTrieMap.ResultHandler;
23 import com.ibm.icu.text.LocaleDisplayNames;
24 import com.ibm.icu.text.TimeZoneFormat.TimeType;
25 import com.ibm.icu.text.TimeZoneNames;
26 import com.ibm.icu.text.TimeZoneNames.MatchInfo;
27 import com.ibm.icu.text.TimeZoneNames.NameType;
28 import com.ibm.icu.util.BasicTimeZone;
29 import com.ibm.icu.util.Freezable;
30 import com.ibm.icu.util.Output;
31 import com.ibm.icu.util.TimeZone;
32 import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
33 import com.ibm.icu.util.TimeZoneTransition;
34 import 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  */
42 public 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 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 }