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