• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* GENERATED SOURCE. DO NOT MODIFY. */
2 // © 2016 and later: Unicode, Inc. and others.
3 // License & terms of use: http://www.unicode.org/copyright.html#License
4 /*
5  *******************************************************************************
6  * Copyright (C) 2011-2016, International Business Machines Corporation and
7  * others. All Rights Reserved.
8  *******************************************************************************
9  */
10 package android.icu.impl;
11 
12 import java.io.IOException;
13 import java.io.ObjectInputStream;
14 import java.io.Serializable;
15 import java.lang.ref.WeakReference;
16 import java.text.MessageFormat;
17 import java.util.Collection;
18 import java.util.EnumSet;
19 import java.util.Iterator;
20 import java.util.LinkedList;
21 import java.util.MissingResourceException;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24 
25 import android.icu.impl.TextTrieMap.ResultHandler;
26 import android.icu.text.LocaleDisplayNames;
27 import android.icu.text.TimeZoneFormat.TimeType;
28 import android.icu.text.TimeZoneNames;
29 import android.icu.text.TimeZoneNames.MatchInfo;
30 import android.icu.text.TimeZoneNames.NameType;
31 import android.icu.util.BasicTimeZone;
32 import android.icu.util.Freezable;
33 import android.icu.util.Output;
34 import android.icu.util.TimeZone;
35 import android.icu.util.TimeZone.SystemTimeZoneType;
36 import android.icu.util.TimeZoneTransition;
37 import android.icu.util.ULocale;
38 
39 /**
40  * This class interact with TimeZoneNames and LocaleDisplayNames
41  * to format and parse time zone's generic display names.
42  * It is not recommended to use this class directly, instead
43  * use android.icu.text.TimeZoneFormat.
44  * @hide Only a subset of ICU is exposed in Android
45  */
46 public class TimeZoneGenericNames implements Serializable, Freezable<TimeZoneGenericNames> {
47 
48     // Note: This class implements Serializable, but we no longer serialize instance of
49     // TimeZoneGenericNames in ICU 49. ICU 4.8 android.icu.text.TimeZoneFormat used to
50     // serialize TimeZoneGenericNames field. TimeZoneFormat no longer read TimeZoneGenericNames
51     // field, we have to keep TimeZoneGenericNames Serializable. Otherwise it fails to read
52     // (unused) TimeZoneGenericNames serialized data.
53 
54     private static final long serialVersionUID = 2729910342063468417L;
55 
56     /**
57      * Generic name type enum
58      */
59     public enum GenericNameType {
60         LOCATION ("LONG", "SHORT"),
61         LONG (),
62         SHORT ();
63 
64         String[] _fallbackTypeOf;
GenericNameType(String... fallbackTypeOf)65         GenericNameType(String... fallbackTypeOf) {
66             _fallbackTypeOf = fallbackTypeOf;
67         }
68 
isFallbackTypeOf(GenericNameType type)69         public boolean isFallbackTypeOf(GenericNameType type) {
70             String typeStr = type.toString();
71             for (String t : _fallbackTypeOf) {
72                 if (t.equals(typeStr)) {
73                     return true;
74                 }
75             }
76             return false;
77         }
78     }
79 
80     /**
81      * Format pattern enum used for composing location and partial location names
82      */
83     public enum Pattern {
84         // The format pattern such as "{0} Time", where {0} is the country or city.
85         REGION_FORMAT("regionFormat", "({0})"),
86 
87         // Note: FALLBACK_REGION_FORMAT is no longer used since ICU 50/CLDR 22.1
88         // The format pattern such as "{1} Time ({0})", where {1} is the country and {0} is a city.
89         //FALLBACK_REGION_FORMAT("fallbackRegionFormat", "{1} ({0})"),
90 
91         // The format pattern such as "{1} ({0})", where {1} is the metazone, and {0} is the country or city.
92         FALLBACK_FORMAT("fallbackFormat", "{1} ({0})");
93 
94         String _key;
95         String _defaultVal;
96 
Pattern(String key, String defaultVal)97         Pattern(String key, String defaultVal) {
98             _key = key;
99             _defaultVal = defaultVal;
100         }
101 
key()102         String key() {
103             return _key;
104         }
105 
defaultValue()106         String defaultValue() {
107             return _defaultVal;
108         }
109     }
110 
111     private final ULocale _locale;
112     private TimeZoneNames _tznames;
113 
114     private transient volatile boolean _frozen;
115     private transient String _region;
116     private transient WeakReference<LocaleDisplayNames> _localeDisplayNamesRef;
117     private transient MessageFormat[] _patternFormatters;
118 
119     private transient ConcurrentHashMap<String, String> _genericLocationNamesMap;
120     private transient ConcurrentHashMap<String, String> _genericPartialLocationNamesMap;
121     private transient TextTrieMap<NameInfo> _gnamesTrie;
122     private transient boolean _gnamesTrieFullyLoaded;
123 
124     private static Cache GENERIC_NAMES_CACHE = new Cache();
125 
126     // Window size used for DST check for a zone in a metazone (about a half year)
127     private static final long DST_CHECK_RANGE = 184L*(24*60*60*1000);
128 
129     private static final NameType[] GENERIC_NON_LOCATION_TYPES =
130                                 {NameType.LONG_GENERIC, NameType.SHORT_GENERIC};
131 
132 
133     /**
134      * Constructs a <code>TimeZoneGenericNames</code> with the given locale
135      * and the <code>TimeZoneNames</code>.
136      * @param locale the locale
137      * @param tznames the TimeZoneNames
138      */
TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames)139     public TimeZoneGenericNames(ULocale locale, TimeZoneNames tznames) {
140         _locale = locale;
141         _tznames = tznames;
142         init();
143     }
144 
145     /**
146      * Private method initializing the instance of <code>TimeZoneGenericName</code>.
147      * This method should be called from a constructor and readObject.
148      */
init()149     private void init() {
150         if (_tznames == null) {
151             _tznames = TimeZoneNames.getInstance(_locale);
152         }
153         _genericLocationNamesMap = new ConcurrentHashMap<String, String>();
154         _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>();
155 
156         _gnamesTrie = new TextTrieMap<NameInfo>(true);
157         _gnamesTrieFullyLoaded = false;
158 
159         // Preload zone strings for the default time zone
160         TimeZone tz = TimeZone.getDefault();
161         String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
162         if (tzCanonicalID != null) {
163             loadStrings(tzCanonicalID);
164         }
165     }
166 
167     /**
168      * Constructs a <code>TimeZoneGenericNames</code> with the given locale.
169      * This constructor is private and called from {@link #getInstance(ULocale)}.
170      * @param locale the locale
171      */
TimeZoneGenericNames(ULocale locale)172     private TimeZoneGenericNames(ULocale locale) {
173         this(locale, null);
174     }
175 
176     /**
177      * The factory method of <code>TimeZoneGenericNames</code>. This static method
178      * returns a frozen instance of cached <code>TimeZoneGenericNames</code>.
179      * @param locale the locale
180      * @return A frozen <code>TimeZoneGenericNames</code>.
181      */
getInstance(ULocale locale)182     public static TimeZoneGenericNames getInstance(ULocale locale) {
183         String key = locale.getBaseName();
184         return GENERIC_NAMES_CACHE.getInstance(key, locale);
185     }
186 
187     /**
188      * Returns the display name of the time zone for the given name type
189      * at the given date, or null if the display name is not available.
190      *
191      * @param tz the time zone
192      * @param type the generic name type - see {@link GenericNameType}
193      * @param date the date
194      * @return the display name of the time zone for the given name type
195      * at the given date, or null.
196      */
getDisplayName(TimeZone tz, GenericNameType type, long date)197     public String getDisplayName(TimeZone tz, GenericNameType type, long date) {
198         String name = null;
199         String tzCanonicalID = null;
200         switch (type) {
201         case LOCATION:
202             tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
203             if (tzCanonicalID != null) {
204                 name = getGenericLocationName(tzCanonicalID);
205             }
206             break;
207         case LONG:
208         case SHORT:
209             name = formatGenericNonLocationName(tz, type, date);
210             if (name == null) {
211                 tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
212                 if (tzCanonicalID != null) {
213                     name = getGenericLocationName(tzCanonicalID);
214                 }
215             }
216             break;
217         }
218         return name;
219     }
220 
221     /**
222      * Returns the generic location name for the given canonical time zone ID.
223      *
224      * @param canonicalTzID the canonical time zone ID
225      * @return the generic location name for the given canonical time zone ID.
226      */
getGenericLocationName(String canonicalTzID)227     public String getGenericLocationName(String canonicalTzID) {
228         if (canonicalTzID == null || canonicalTzID.length() == 0) {
229             return null;
230         }
231         String name = _genericLocationNamesMap.get(canonicalTzID);
232         if (name != null) {
233             if (name.length() == 0) {
234                 // empty string to indicate the name is not available
235                 return null;
236             }
237             return name;
238         }
239 
240         Output<Boolean> isPrimary = new Output<Boolean>();
241         String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID, isPrimary);
242         if (countryCode != null) {
243             if (isPrimary.value) {
244                 // If this is only the single zone in the country, use the country name
245                 String country = getLocaleDisplayNames().regionDisplayName(countryCode);
246                 name = formatPattern(Pattern.REGION_FORMAT, country);
247             } else {
248                 // If there are multiple zones including this in the country,
249                 // use the exemplar city name
250 
251                 // getExemplarLocationName should return non-empty String
252                 // if the time zone is associated with a location
253                 String city = _tznames.getExemplarLocationName(canonicalTzID);
254                 name = formatPattern(Pattern.REGION_FORMAT, city);
255             }
256         }
257 
258         if (name == null) {
259             _genericLocationNamesMap.putIfAbsent(canonicalTzID.intern(), "");
260         } else {
261             synchronized (this) {   // we have to sync the name map and the trie
262                 canonicalTzID = canonicalTzID.intern();
263                 String tmp = _genericLocationNamesMap.putIfAbsent(canonicalTzID, name.intern());
264                 if (tmp == null) {
265                     // Also put the name info the to trie
266                     NameInfo info = new NameInfo(canonicalTzID, 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      */
setFormatPattern(Pattern patType, String patStr)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      */
formatGenericNonLocationName(TimeZone tz, GenericNameType type, long date)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      */
formatPattern(Pattern pat, String... args)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                     ICUData.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      */
getLocaleDisplayNames()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 
loadStrings(String tzCanonicalID)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      */
getTargetRegion()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      */
getPartialLocationName(String tzID, String mzID, boolean isLong, String mzDisplayName)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(tzID.intern(),
576                         isLong ? GenericNameType.LONG : GenericNameType.SHORT);
577                 _gnamesTrie.put(name, info);
578             } else {
579                 name = tmp;
580             }
581         }
582         return name;
583     }
584 
585     /**
586      * A private class used for storing the name information in the local trie.
587      */
588     private static class NameInfo {
589         final String tzID;
590         final GenericNameType type;
591 
NameInfo(String tzID, GenericNameType type)592         NameInfo(String tzID, GenericNameType type) {
593             this.tzID = tzID;
594             this.type = type;
595         }
596     }
597 
598     /**
599      * A class used for returning the name search result used by
600      * {@link TimeZoneGenericNames#find(String, int, EnumSet)}.
601      */
602     public static class GenericMatchInfo {
603         final GenericNameType nameType;
604         final String tzID;
605         final int matchLength;
606         final TimeType timeType;
607 
GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength)608         private GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength) {
609             this(nameType, tzID, matchLength, TimeType.UNKNOWN);
610         }
611 
GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength, TimeType timeType)612         private GenericMatchInfo(GenericNameType nameType, String tzID, int matchLength, TimeType timeType) {
613             this.nameType = nameType;
614             this.tzID = tzID;
615             this.matchLength = matchLength;
616             this.timeType = timeType;
617         }
618 
nameType()619         public GenericNameType nameType() {
620             return nameType;
621         }
622 
tzID()623         public String tzID() {
624             return tzID;
625         }
626 
timeType()627         public TimeType timeType() {
628             return timeType;
629         }
630 
matchLength()631         public int matchLength() {
632             return matchLength;
633         }
634     }
635 
636     /**
637      * A private class implementing the search callback interface in
638      * <code>TextTrieMap</code> for collecting match results.
639      */
640     private static class GenericNameSearchHandler implements ResultHandler<NameInfo> {
641         private EnumSet<GenericNameType> _types;
642         private Collection<GenericMatchInfo> _matches;
643         private int _maxMatchLen;
644 
GenericNameSearchHandler(EnumSet<GenericNameType> types)645         GenericNameSearchHandler(EnumSet<GenericNameType> types) {
646             _types = types;
647         }
648 
649         /* (non-Javadoc)
650          * @see android.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator)
651          */
652         @Override
handlePrefixMatch(int matchLength, Iterator<NameInfo> values)653         public boolean handlePrefixMatch(int matchLength, Iterator<NameInfo> values) {
654             while (values.hasNext()) {
655                 NameInfo info = values.next();
656                 if (_types != null && !_types.contains(info.type)) {
657                     continue;
658                 }
659                 GenericMatchInfo matchInfo = new GenericMatchInfo(info.type, info.tzID, matchLength);
660                 if (_matches == null) {
661                     _matches = new LinkedList<GenericMatchInfo>();
662                 }
663                 _matches.add(matchInfo);
664                 if (matchLength > _maxMatchLen) {
665                     _maxMatchLen = matchLength;
666                 }
667             }
668             return true;
669         }
670 
671         /**
672          * Returns the match results
673          * @return the match results
674          */
getMatches()675         public Collection<GenericMatchInfo> getMatches() {
676             return _matches;
677         }
678 
679         /**
680          * Returns the maximum match length, or 0 if no match was found
681          * @return the maximum match length
682          */
getMaxMatchLen()683         public int getMaxMatchLen() {
684             return _maxMatchLen;
685         }
686 
687         /**
688          * Resets the match results
689          */
resetResults()690         public void resetResults() {
691             _matches = null;
692             _maxMatchLen = 0;
693         }
694     }
695 
696     /**
697      * Returns the best match of time zone display name for the specified types in the
698      * given text at the given offset.
699      * @param text the text
700      * @param start the start offset in the text
701      * @param genericTypes the set of name types.
702      * @return the best matching name info.
703      */
findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes)704     public GenericMatchInfo findBestMatch(String text, int start, EnumSet<GenericNameType> genericTypes) {
705         if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
706             throw new IllegalArgumentException("bad input text or range");
707         }
708         GenericMatchInfo bestMatch = null;
709 
710         // Find matches in the TimeZoneNames first
711         Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes);
712         if (tznamesMatches != null) {
713             MatchInfo longestMatch = null;
714             for (MatchInfo match : tznamesMatches) {
715                 if (longestMatch == null || match.matchLength() > longestMatch.matchLength()) {
716                     longestMatch = match;
717                 }
718             }
719             if (longestMatch != null) {
720                 bestMatch = createGenericMatchInfo(longestMatch);
721                 if (bestMatch.matchLength() == (text.length() - start)) {
722                     // Full match
723                     //return bestMatch;
724 
725                     // TODO Some time zone uses a same name for the long standard name
726                     // and the location name. When the match is a long standard name,
727                     // then we need to check if the name is same with the location name.
728                     // This is probably a data error or a design bug.
729 //                    if (bestMatch.nameType != GenericNameType.LONG || bestMatch.timeType != TimeType.STANDARD) {
730 //                        return bestMatch;
731 //                    }
732 
733                     // TODO The deprecation of commonlyUsed flag introduced the name
734                     // conflict not only for long standard names, but short standard names too.
735                     // These short names (found in zh_Hant) should be gone once we clean
736                     // up CLDR time zone display name data. Once the short name conflict
737                     // problem (with location name) is resolved, we should change the condition
738                     // below back to the original one above. -Yoshito (2011-09-14)
739                     if (bestMatch.timeType != TimeType.STANDARD) {
740                         return bestMatch;
741                     }
742                 }
743             }
744         }
745 
746         // Find matches in the local trie
747         Collection<GenericMatchInfo> localMatches = findLocal(text, start, genericTypes);
748         if (localMatches != null) {
749             for (GenericMatchInfo match : localMatches) {
750                 // TODO See the above TODO. We use match.matchLength() >= bestMatch.matcheLength()
751                 // for the reason described above.
752                 //if (bestMatch == null || match.matchLength() > bestMatch.matchLength()) {
753                 if (bestMatch == null || match.matchLength() >= bestMatch.matchLength()) {
754                     bestMatch = match;
755                 }
756             }
757         }
758 
759         return bestMatch;
760     }
761 
762     /**
763      * Returns a collection of time zone display name matches for the specified types in the
764      * given text at the given offset.
765      * @param text the text
766      * @param start the start offset in the text
767      * @param genericTypes the set of name types.
768      * @return A collection of match info.
769      */
find(String text, int start, EnumSet<GenericNameType> genericTypes)770     public Collection<GenericMatchInfo> find(String text, int start, EnumSet<GenericNameType> genericTypes) {
771         if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
772             throw new IllegalArgumentException("bad input text or range");
773         }
774         // Find matches in the local trie
775         Collection<GenericMatchInfo> results = findLocal(text, start, genericTypes);
776 
777         // Also find matches in the TimeZoneNames
778         Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes);
779         if (tznamesMatches != null) {
780             // transform matches and append them to local matches
781             for (MatchInfo match : tznamesMatches) {
782                 if (results == null) {
783                     results = new LinkedList<GenericMatchInfo>();
784                 }
785                 results.add(createGenericMatchInfo(match));
786             }
787         }
788         return results;
789     }
790 
791     /**
792      * Returns a <code>GenericMatchInfo</code> for the given <code>MatchInfo</code>.
793      * @param matchInfo the MatchInfo
794      * @return A GenericMatchInfo
795      */
createGenericMatchInfo(MatchInfo matchInfo)796     private GenericMatchInfo createGenericMatchInfo(MatchInfo matchInfo) {
797         GenericNameType nameType = null;
798         TimeType timeType = TimeType.UNKNOWN;
799         switch (matchInfo.nameType()) {
800         case LONG_STANDARD:
801             nameType = GenericNameType.LONG;
802             timeType = TimeType.STANDARD;
803             break;
804         case LONG_GENERIC:
805             nameType = GenericNameType.LONG;
806             break;
807         case SHORT_STANDARD:
808             nameType = GenericNameType.SHORT;
809             timeType = TimeType.STANDARD;
810             break;
811         case SHORT_GENERIC:
812             nameType = GenericNameType.SHORT;
813             break;
814         default:
815             throw new IllegalArgumentException("Unexpected MatchInfo name type - " + matchInfo.nameType());
816         }
817 
818         String tzID = matchInfo.tzID();
819         if (tzID == null) {
820             String mzID = matchInfo.mzID();
821             assert(mzID != null);
822             tzID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
823         }
824         assert(tzID != null);
825 
826         GenericMatchInfo gmatch = new GenericMatchInfo(nameType, tzID, matchInfo.matchLength(), timeType);
827 
828         return gmatch;
829     }
830 
831     /**
832      * Returns a collection of time zone display name matches for the specified types in the
833      * given text at the given offset. This method only finds matches from the TimeZoneNames
834      * used by this object.
835      * @param text the text
836      * @param start the start offset in the text
837      * @param types the set of name types.
838      * @return A collection of match info.
839      */
findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types)840     private Collection<MatchInfo> findTimeZoneNames(String text, int start, EnumSet<GenericNameType> types) {
841         Collection<MatchInfo> tznamesMatches = null;
842 
843         // Check if the target name type is really in the TimeZoneNames
844         EnumSet<NameType> nameTypes = EnumSet.noneOf(NameType.class);
845         if (types.contains(GenericNameType.LONG)) {
846             nameTypes.add(NameType.LONG_GENERIC);
847             nameTypes.add(NameType.LONG_STANDARD);
848         }
849         if (types.contains(GenericNameType.SHORT)) {
850             nameTypes.add(NameType.SHORT_GENERIC);
851             nameTypes.add(NameType.SHORT_STANDARD);
852         }
853 
854         if (!nameTypes.isEmpty()) {
855             // Find matches in the TimeZoneNames
856             tznamesMatches = _tznames.find(text, start, nameTypes);
857         }
858         return tznamesMatches;
859     }
860 
861     /**
862      * Returns a collection of time zone display name matches for the specified types in the
863      * given text at the given offset. This method only finds matches from the local trie,
864      * that contains 1) generic location names and 2) long/short generic partial location names,
865      * used by this object.
866      * @param text the text
867      * @param start the start offset in the text
868      * @param types the set of name types.
869      * @return A collection of match info.
870      */
findLocal(String text, int start, EnumSet<GenericNameType> types)871     private synchronized Collection<GenericMatchInfo> findLocal(String text, int start, EnumSet<GenericNameType> types) {
872         GenericNameSearchHandler handler = new GenericNameSearchHandler(types);
873         _gnamesTrie.find(text, start, handler);
874         if (handler.getMaxMatchLen() == (text.length() - start) || _gnamesTrieFullyLoaded) {
875             // perfect match
876             return handler.getMatches();
877         }
878 
879         // All names are not yet loaded into the local trie.
880         // Load all available names into the trie. This could be very heavy.
881 
882         Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);
883         for (String tzID : tzIDs) {
884             loadStrings(tzID);
885         }
886         _gnamesTrieFullyLoaded = true;
887 
888         // now, try it again
889         handler.resetResults();
890         _gnamesTrie.find(text, start, handler);
891         return handler.getMatches();
892     }
893 
894     /**
895      * <code>TimeZoneGenericNames</code> cache implementation.
896      */
897     private static class Cache extends SoftCache<String, TimeZoneGenericNames, ULocale> {
898 
899         /* (non-Javadoc)
900          * @see android.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
901          */
902         @Override
createInstance(String key, ULocale data)903         protected TimeZoneGenericNames createInstance(String key, ULocale data) {
904             return new TimeZoneGenericNames(data).freeze();
905         }
906 
907     }
908 
909     /*
910      * The custom deserialization method.
911      * This implementation only read locale used by the object.
912      */
readObject(ObjectInputStream in)913     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
914         in.defaultReadObject();
915         init();
916     }
917 
918     /**
919      * {@inheritDoc}
920      */
921     @Override
isFrozen()922     public boolean isFrozen() {
923         return _frozen;
924     }
925 
926     /**
927      * {@inheritDoc}
928      */
929     @Override
freeze()930     public TimeZoneGenericNames freeze() {
931         _frozen = true;
932         return this;
933     }
934 
935     /**
936      * {@inheritDoc}
937      */
938     @Override
cloneAsThawed()939     public TimeZoneGenericNames cloneAsThawed() {
940         TimeZoneGenericNames copy = null;
941         try {
942             copy = (TimeZoneGenericNames)super.clone();
943             copy._frozen = false;
944         } catch (Throwable t) {
945             // This should never happen
946         }
947         return copy;
948     }
949 }
950