Package com.ibm.icu.impl

Source Code of com.ibm.icu.impl.TimeZoneGenericNames$Cache

/*
*******************************************************************************
* Copyright (C) 2011-2013, International Business Machines Corporation and    *
* others. All Rights Reserved.                                                *
*******************************************************************************
*/
package com.ibm.icu.impl;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.ibm.icu.impl.TextTrieMap.ResultHandler;
import com.ibm.icu.text.LocaleDisplayNames;
import com.ibm.icu.text.TimeZoneFormat.TimeType;
import com.ibm.icu.text.TimeZoneNames;
import com.ibm.icu.text.TimeZoneNames.MatchInfo;
import com.ibm.icu.text.TimeZoneNames.NameType;
import com.ibm.icu.util.BasicTimeZone;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
import com.ibm.icu.util.TimeZoneTransition;
import com.ibm.icu.util.ULocale;

/**
* This class interact with TimeZoneNames and LocaleDisplayNames to format and parse time zone's generic display names. It is not
* recommended to use this class directly, instead use com.ibm.icu.text.TimeZoneFormat.
*/
public class TimeZoneGenericNames implements Serializable, Freezable<TimeZoneGenericNames> {

  // Note: This class implements Serializable, but we no longer serialize instance of
  // TimeZoneGenericNames in ICU 49. ICU 4.8 com.ibm.icu.text.TimeZoneFormat used to
  // serialize TimeZoneGenericNames field. TimeZoneFormat no longer read TimeZoneGenericNames
  // field, we have to keep TimeZoneGenericNames Serializable. Otherwise it fails to read
  // (unused) TimeZoneGenericNames serialized data.

  private static final long serialVersionUID = 2729910342063468417L;

  /**
   * Generic name type enum
   */
  public enum GenericNameType {
    LOCATION("LONG", "SHORT"), LONG(), SHORT();

    String[] _fallbackTypeOf;

    GenericNameType(final String... fallbackTypeOf) {
      _fallbackTypeOf = fallbackTypeOf;
    }

    public boolean isFallbackTypeOf(final GenericNameType type) {
      String typeStr = type.toString();
      for (String t : _fallbackTypeOf) {
        if (t.equals(typeStr)) {
          return true;
        }
      }
      return false;
    }
  }

  /**
   * Format pattern enum used for composing location and partial location names
   */
  public enum Pattern {
    // The format pattern such as "{0} Time", where {0} is the country or city.
    REGION_FORMAT("regionFormat", "({0})"),

    // Note: FALLBACK_REGION_FORMAT is no longer used since ICU 50/CLDR 22.1
    // The format pattern such as "{1} Time ({0})", where {1} is the country and {0} is a city.
    //FALLBACK_REGION_FORMAT("fallbackRegionFormat", "{1} ({0})"),

    // The format pattern such as "{1} ({0})", where {1} is the metazone, and {0} is the country or city.
    FALLBACK_FORMAT("fallbackFormat", "{1} ({0})");

    String _key;
    String _defaultVal;

    Pattern(final String key, final String defaultVal) {
      _key = key;
      _defaultVal = defaultVal;
    }

    String key() {
      return _key;
    }

    String defaultValue() {
      return _defaultVal;
    }
  }

  private ULocale _locale;
  private TimeZoneNames _tznames;

  private transient boolean _frozen;
  private transient String _region;
  private transient WeakReference<LocaleDisplayNames> _localeDisplayNamesRef;
  private transient MessageFormat[] _patternFormatters;

  private transient ConcurrentHashMap<String, String> _genericLocationNamesMap;
  private transient ConcurrentHashMap<String, String> _genericPartialLocationNamesMap;
  private transient TextTrieMap<NameInfo> _gnamesTrie;
  private transient boolean _gnamesTrieFullyLoaded;

  private static Cache GENERIC_NAMES_CACHE = new Cache();

  // Window size used for DST check for a zone in a metazone (about a half year)
  private static final long DST_CHECK_RANGE = 184L * (24 * 60 * 60 * 1000);

  private static final NameType[] GENERIC_NON_LOCATION_TYPES = { NameType.LONG_GENERIC, NameType.SHORT_GENERIC };

  /**
   * Constructs a <code>TimeZoneGenericNames</code> with the given locale and the <code>TimeZoneNames</code>.
   *
   * @param locale
   *            the locale
   * @param tznames
   *            the TimeZoneNames
   */
  public TimeZoneGenericNames(final ULocale locale, final TimeZoneNames tznames) {
    _locale = locale;
    _tznames = tznames;
    init();
  }

  /**
   * Private method initializing the instance of <code>TimeZoneGenericName</code>. This method should be called from a constructor and
   * readObject.
   */
  private void init() {
    if (_tznames == null) {
      _tznames = TimeZoneNames.getInstance(_locale);
    }
    _genericLocationNamesMap = new ConcurrentHashMap<String, String>();
    _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>();

    _gnamesTrie = new TextTrieMap<NameInfo>(true);
    _gnamesTrieFullyLoaded = false;

    // Preload zone strings for the default time zone
    TimeZone tz = TimeZone.getDefault();
    String tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
    if (tzCanonicalID != null) {
      loadStrings(tzCanonicalID);
    }
  }

  /**
   * Constructs a <code>TimeZoneGenericNames</code> with the given locale. This constructor is private and called from
   * {@link #getInstance(ULocale)}.
   *
   * @param locale
   *            the locale
   */
  private TimeZoneGenericNames(final ULocale locale) {
    this(locale, null);
  }

  /**
   * The factory method of <code>TimeZoneGenericNames</code>. This static method returns a frozen instance of cached
   * <code>TimeZoneGenericNames</code>.
   *
   * @param locale
   *            the locale
   * @return A frozen <code>TimeZoneGenericNames</code>.
   */
  public static TimeZoneGenericNames getInstance(final ULocale locale) {
    String key = locale.getBaseName();
    return GENERIC_NAMES_CACHE.getInstance(key, locale);
  }

  /**
   * Returns the display name of the time zone for the given name type at the given date, or null if the display name is not available.
   *
   * @param tz
   *            the time zone
   * @param type
   *            the generic name type - see {@link GenericNameType}
   * @param date
   *            the date
   * @return the display name of the time zone for the given name type at the given date, or null.
   */
  public String getDisplayName(final TimeZone tz, final GenericNameType type, final long date) {
    String name = null;
    String tzCanonicalID = null;
    switch (type) {
    case LOCATION:
      tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
      if (tzCanonicalID != null) {
        name = getGenericLocationName(tzCanonicalID);
      }
      break;
    case LONG:
    case SHORT:
      name = formatGenericNonLocationName(tz, type, date);
      if (name == null) {
        tzCanonicalID = ZoneMeta.getCanonicalCLDRID(tz);
        if (tzCanonicalID != null) {
          name = getGenericLocationName(tzCanonicalID);
        }
      }
      break;
    }
    return name;
  }

  /**
   * Returns the generic location name for the given canonical time zone ID.
   *
   * @param canonicalTzID
   *            the canonical time zone ID
   * @return the generic location name for the given canonical time zone ID.
   */
  public String getGenericLocationName(String canonicalTzID) {
    if (canonicalTzID == null || canonicalTzID.length() == 0) {
      return null;
    }
    String name = _genericLocationNamesMap.get(canonicalTzID);
    if (name != null) {
      if (name.length() == 0) {
        // empty string to indicate the name is not available
        return null;
      }
      return name;
    }

    Output<Boolean> isPrimary = new Output<Boolean>();
    String countryCode = ZoneMeta.getCanonicalCountry(canonicalTzID, isPrimary);
    if (countryCode != null) {
      if (isPrimary.value) {
        // If this is only the single zone in the country, use the country name
        String country = getLocaleDisplayNames().regionDisplayName(countryCode);
        name = formatPattern(Pattern.REGION_FORMAT, country);
      } else {
        // If there are multiple zones including this in the country,
        // use the exemplar city name

        // getExemplarLocationName should return non-empty String
        // if the time zone is associated with a location
        String city = _tznames.getExemplarLocationName(canonicalTzID);
        name = formatPattern(Pattern.REGION_FORMAT, city);
      }
    }

    if (name == null) {
      _genericLocationNamesMap.putIfAbsent(canonicalTzID.intern(), "");
    } else {
      synchronized (this) {   // we have to sync the name map and the trie
        canonicalTzID = canonicalTzID.intern();
        String tmp = _genericLocationNamesMap.putIfAbsent(canonicalTzID, name.intern());
        if (tmp == null) {
          // Also put the name info the to trie
          NameInfo info = new NameInfo();
          info.tzID = canonicalTzID;
          info.type = GenericNameType.LOCATION;
          _gnamesTrie.put(name, info);
        } else {
          name = tmp;
        }
      }
    }
    return name;
  }

  /**
   * Sets the pattern string for the pattern type. Note: This method is designed for CLDR ST - not for common use.
   *
   * @param patType
   *            the pattern type
   * @param patStr
   *            the pattern string
   * @return this object.
   */
  public TimeZoneGenericNames setFormatPattern(final Pattern patType, final String patStr) {
    if (isFrozen()) {
      throw new UnsupportedOperationException("Attempt to modify frozen object");
    }

    // Changing pattern will invalidates cached names
    if (!_genericLocationNamesMap.isEmpty()) {
      _genericLocationNamesMap = new ConcurrentHashMap<String, String>();
    }
    if (!_genericPartialLocationNamesMap.isEmpty()) {
      _genericPartialLocationNamesMap = new ConcurrentHashMap<String, String>();
    }
    _gnamesTrie = null;
    _gnamesTrieFullyLoaded = false;

    if (_patternFormatters == null) {
      _patternFormatters = new MessageFormat[Pattern.values().length];
    }
    _patternFormatters[patType.ordinal()] = new MessageFormat(patStr);
    return this;
  }

  /**
   * Private method to get a generic string, with fallback logics involved, that is,
   *
   * 1. If a generic non-location string is available for the zone, return it. 2. If a generic non-location string is associated with a
   * meta zone and the zone never use daylight time around the given date, use the standard string (if available). 3. If a generic
   * non-location string is associated with a meta zone and the offset at the given time is different from the preferred zone for the
   * current locale, then return the generic partial location string (if available) 4. If a generic non-location string is not available,
   * use generic location string.
   *
   * @param tz
   *            the requested time zone
   * @param date
   *            the date
   * @param type
   *            the generic name type, either LONG or SHORT
   * @return the name used for a generic name type, which could be the generic name, or the standard name (if the zone does not observes
   *         DST around the date), or the partial location name.
   */
  private String formatGenericNonLocationName(final TimeZone tz, final GenericNameType type, final long date) {
    assert (type == GenericNameType.LONG || type == GenericNameType.SHORT);
    String tzID = ZoneMeta.getCanonicalCLDRID(tz);

    if (tzID == null) {
      return null;
    }

    // Try to get a name from time zone first
    NameType nameType = (type == GenericNameType.LONG) ? NameType.LONG_GENERIC : NameType.SHORT_GENERIC;
    String name = _tznames.getTimeZoneDisplayName(tzID, nameType);

    if (name != null) {
      return name;
    }

    // Try meta zone
    String mzID = _tznames.getMetaZoneID(tzID, date);
    if (mzID != null) {
      boolean useStandard = false;
      int[] offsets = { 0, 0 };
      tz.getOffset(date, false, offsets);

      if (offsets[1] == 0) {
        useStandard = true;
        // Check if the zone actually uses daylight saving time around the time
        if (tz instanceof BasicTimeZone) {
          BasicTimeZone btz = (BasicTimeZone) tz;
          TimeZoneTransition before = btz.getPreviousTransition(date, true);
          if (before != null && (date - before.getTime() < DST_CHECK_RANGE) && before.getFrom().getDSTSavings() != 0) {
            useStandard = false;
          } else {
            TimeZoneTransition after = btz.getNextTransition(date, false);
            if (after != null && (after.getTime() - date < DST_CHECK_RANGE) && after.getTo().getDSTSavings() != 0) {
              useStandard = false;
            }
          }
        } else {
          // If not BasicTimeZone... only if the instance is not an ICU's implementation.
          // We may get a wrong answer in edge case, but it should practically work OK.
          int[] tmpOffsets = new int[2];
          tz.getOffset(date - DST_CHECK_RANGE, false, tmpOffsets);
          if (tmpOffsets[1] != 0) {
            useStandard = false;
          } else {
            tz.getOffset(date + DST_CHECK_RANGE, false, tmpOffsets);
            if (tmpOffsets[1] != 0) {
              useStandard = false;
            }
          }
        }
      }
      if (useStandard) {
        NameType stdNameType = (nameType == NameType.LONG_GENERIC) ? NameType.LONG_STANDARD : NameType.SHORT_STANDARD;
        String stdName = _tznames.getDisplayName(tzID, stdNameType, date);
        if (stdName != null) {
          name = stdName;

          // TODO: revisit this issue later
          // In CLDR, a same display name is used for both generic and standard
          // for some meta zones in some locales.  This looks like a data bugs.
          // For now, we check if the standard name is different from its generic
          // name below.
          String mzGenericName = _tznames.getMetaZoneDisplayName(mzID, nameType);
          if (stdName.equalsIgnoreCase(mzGenericName)) {
            name = null;
          }
        }
      }

      if (name == null) {
        // Get a name from meta zone
        String mzName = _tznames.getMetaZoneDisplayName(mzID, nameType);
        if (mzName != null) {
          // Check if we need to use a partial location format.
          // This check is done by comparing offset with the meta zone's
          // golden zone at the given date.
          String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
          if (goldenID != null && !goldenID.equals(tzID)) {
            TimeZone goldenZone = TimeZone.getFrozenTimeZone(goldenID);
            int[] offsets1 = { 0, 0 };

            // Check offset in the golden zone with wall time.
            // With getOffset(date, false, offsets1),
            // you may get incorrect results because of time overlap at DST->STD
            // transition.
            goldenZone.getOffset(date + offsets[0] + offsets[1], true, offsets1);

            if (offsets[0] != offsets1[0] || offsets[1] != offsets1[1]) {
              // Now we need to use a partial location format.
              name = getPartialLocationName(tzID, mzID, (nameType == NameType.LONG_GENERIC), mzName);
            } else {
              name = mzName;
            }
          } else {
            name = mzName;
          }
        }
      }
    }
    return name;
  }

  /**
   * Private simple pattern formatter used for formatting generic location names and partial location names. We intentionally use JDK
   * MessageFormat for performance reason.
   *
   * @param pat
   *            the message pattern enum
   * @param args
   *            the format argument(s)
   * @return the formatted string
   */
  private synchronized String formatPattern(final Pattern pat, final String... args) {
    if (_patternFormatters == null) {
      _patternFormatters = new MessageFormat[Pattern.values().length];
    }

    int idx = pat.ordinal();
    if (_patternFormatters[idx] == null) {
      String patText;
      try {
        ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance(ICUResourceBundle.ICU_ZONE_BASE_NAME,
            _locale);
        patText = bundle.getStringWithFallback("zoneStrings/" + pat.key());
      } catch (MissingResourceException e) {
        patText = pat.defaultValue();
      }

      _patternFormatters[idx] = new MessageFormat(patText);
    }
    return _patternFormatters[idx].format(args);
  }

  /**
   * Private method returning LocaleDisplayNames instance for the locale of this instance. Because LocaleDisplayNames is only used for
   * generic location formant and partial location format, the LocaleDisplayNames is instantiated lazily.
   *
   * @return the instance of LocaleDisplayNames for the locale of this object.
   */
  private synchronized LocaleDisplayNames getLocaleDisplayNames() {
    LocaleDisplayNames locNames = null;
    if (_localeDisplayNamesRef != null) {
      locNames = _localeDisplayNamesRef.get();
    }
    if (locNames == null) {
      locNames = LocaleDisplayNames.getInstance(_locale);
      _localeDisplayNamesRef = new WeakReference<LocaleDisplayNames>(locNames);
    }
    return locNames;
  }

  private synchronized void loadStrings(final String tzCanonicalID) {
    if (tzCanonicalID == null || tzCanonicalID.length() == 0) {
      return;
    }
    // getGenericLocationName() formats a name and put it into the trie
    getGenericLocationName(tzCanonicalID);

    // Generic partial location format
    Set<String> mzIDs = _tznames.getAvailableMetaZoneIDs(tzCanonicalID);
    for (String mzID : mzIDs) {
      // if this time zone is not the golden zone of the meta zone,
      // partial location name (such as "PT (Los Angeles)") might be
      // available.
      String goldenID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
      if (!tzCanonicalID.equals(goldenID)) {
        for (NameType genNonLocType : GENERIC_NON_LOCATION_TYPES) {
          String mzGenName = _tznames.getMetaZoneDisplayName(mzID, genNonLocType);
          if (mzGenName != null) {
            // getPartialLocationName() formats a name and put it into the trie
            getPartialLocationName(tzCanonicalID, mzID, (genNonLocType == NameType.LONG_GENERIC), mzGenName);
          }
        }
      }
    }
  }

  /**
   * Private method returning the target region. The target regions is determined by the locale of this instance. When a generic name is
   * coming from a meta zone, this region is used for checking if the time zone is a reference zone of the meta zone.
   *
   * @return the target region
   */
  private synchronized String getTargetRegion() {
    if (_region == null) {
      _region = _locale.getCountry();
      if (_region.length() == 0) {
        ULocale tmp = ULocale.addLikelySubtags(_locale);
        _region = tmp.getCountry();
        if (_region.length() == 0) {
          _region = "001";
        }
      }
    }
    return _region;
  }

  /**
   * Private method for formatting partial location names. This format is used when a generic name of a meta zone is available, but the
   * given time zone is not a reference zone (golden zone) of the meta zone.
   *
   * @param tzID
   *            the canonical time zone ID
   * @param mzID
   *            the meta zone ID
   * @param isLong
   *            true when long generic name
   * @param mzDisplayName
   *            the meta zone generic display name
   * @return the partial location format string
   */
  private String getPartialLocationName(final String tzID, final String mzID, final boolean isLong, final String mzDisplayName) {
    String letter = isLong ? "L" : "S";
    String key = tzID + "&" + mzID + "#" + letter;
    String name = _genericPartialLocationNamesMap.get(key);
    if (name != null) {
      return name;
    }
    String location = null;
    String countryCode = ZoneMeta.getCanonicalCountry(tzID);
    if (countryCode != null) {
      // Is this the golden zone for the region?
      String regionalGolden = _tznames.getReferenceZoneID(mzID, countryCode);
      if (tzID.equals(regionalGolden)) {
        // Use country name
        location = getLocaleDisplayNames().regionDisplayName(countryCode);
      } else {
        // Otherwise, use exemplar city name
        location = _tznames.getExemplarLocationName(tzID);
      }
    } else {
      location = _tznames.getExemplarLocationName(tzID);
      if (location == null) {
        // This could happen when the time zone is not associated with a country,
        // and its ID is not hierarchical, for example, CST6CDT.
        // We use the canonical ID itself as the location for this case.
        location = tzID;
      }
    }
    name = formatPattern(Pattern.FALLBACK_FORMAT, location, mzDisplayName);
    synchronized (this) {   // we have to sync the name map and the trie
      String tmp = _genericPartialLocationNamesMap.putIfAbsent(key.intern(), name.intern());
      if (tmp == null) {
        NameInfo info = new NameInfo();
        info.tzID = tzID.intern();
        info.type = isLong ? GenericNameType.LONG : GenericNameType.SHORT;
        _gnamesTrie.put(name, info);
      } else {
        name = tmp;
      }
    }
    return name;
  }

  /**
   * A private class used for storing the name information in the local trie.
   */
  private static class NameInfo {
    String tzID;
    GenericNameType type;
  }

  /**
   * A class used for returning the name search result used by {@link TimeZoneGenericNames#find(String, int, EnumSet)}.
   */
  public static class GenericMatchInfo {
    GenericNameType nameType;
    String tzID;
    int matchLength;
    TimeType timeType = TimeType.UNKNOWN;

    public GenericNameType nameType() {
      return nameType;
    }

    public String tzID() {
      return tzID;
    }

    public TimeType timeType() {
      return timeType;
    }

    public int matchLength() {
      return matchLength;
    }
  }

  /**
   * A private class implementing the search callback interface in <code>TextTrieMap</code> for collecting match results.
   */
  private static class GenericNameSearchHandler implements ResultHandler<NameInfo> {
    private EnumSet<GenericNameType> _types;
    private Collection<GenericMatchInfo> _matches;
    private int _maxMatchLen;

    GenericNameSearchHandler(final EnumSet<GenericNameType> types) {
      _types = types;
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int, java.util.Iterator)
     */
    public boolean handlePrefixMatch(final int matchLength, final Iterator<NameInfo> values) {
      while (values.hasNext()) {
        NameInfo info = values.next();
        if (_types != null && !_types.contains(info.type)) {
          continue;
        }
        GenericMatchInfo matchInfo = new GenericMatchInfo();
        matchInfo.tzID = info.tzID;
        matchInfo.nameType = info.type;
        matchInfo.matchLength = matchLength;
        //matchInfo.timeType = TimeType.UNKNOWN;
        if (_matches == null) {
          _matches = new LinkedList<GenericMatchInfo>();
        }
        _matches.add(matchInfo);
        if (matchLength > _maxMatchLen) {
          _maxMatchLen = matchLength;
        }
      }
      return true;
    }

    /**
     * Returns the match results
     *
     * @return the match results
     */
    public Collection<GenericMatchInfo> getMatches() {
      return _matches;
    }

    /**
     * Returns the maximum match length, or 0 if no match was found
     *
     * @return the maximum match length
     */
    public int getMaxMatchLen() {
      return _maxMatchLen;
    }

    /**
     * Resets the match results
     */
    public void resetResults() {
      _matches = null;
      _maxMatchLen = 0;
    }
  }

  /**
   * Returns the best match of time zone display name for the specified types in the given text at the given offset.
   *
   * @param text
   *            the text
   * @param start
   *            the start offset in the text
   * @param genericTypes
   *            the set of name types.
   * @return the best matching name info.
   */
  public GenericMatchInfo findBestMatch(final String text, final int start, final EnumSet<GenericNameType> genericTypes) {
    if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
      throw new IllegalArgumentException("bad input text or range");
    }
    GenericMatchInfo bestMatch = null;

    // Find matches in the TimeZoneNames first
    Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes);
    if (tznamesMatches != null) {
      MatchInfo longestMatch = null;
      for (MatchInfo match : tznamesMatches) {
        if (longestMatch == null || match.matchLength() > longestMatch.matchLength()) {
          longestMatch = match;
        }
      }
      if (longestMatch != null) {
        bestMatch = createGenericMatchInfo(longestMatch);
        if (bestMatch.matchLength() == (text.length() - start)) {
          // Full match
          //return bestMatch;

          // TODO Some time zone uses a same name for the long standard name
          // and the location name. When the match is a long standard name,
          // then we need to check if the name is same with the location name.
          // This is probably a data error or a design bug.
          //                    if (bestMatch.nameType != GenericNameType.LONG || bestMatch.timeType != TimeType.STANDARD) {
          //                        return bestMatch;
          //                    }

          // TODO The deprecation of commonlyUsed flag introduced the name
          // conflict not only for long standard names, but short standard names too.
          // These short names (found in zh_Hant) should be gone once we clean
          // up CLDR time zone display name data. Once the short name conflict
          // problem (with location name) is resolved, we should change the condition
          // below back to the original one above. -Yoshito (2011-09-14)
          if (bestMatch.timeType != TimeType.STANDARD) {
            return bestMatch;
          }
        }
      }
    }

    // Find matches in the local trie
    Collection<GenericMatchInfo> localMatches = findLocal(text, start, genericTypes);
    if (localMatches != null) {
      for (GenericMatchInfo match : localMatches) {
        // TODO See the above TODO. We use match.matchLength() >= bestMatch.matcheLength()
        // for the reason described above.
        //if (bestMatch == null || match.matchLength() > bestMatch.matchLength()) {
        if (bestMatch == null || match.matchLength() >= bestMatch.matchLength()) {
          bestMatch = match;
        }
      }
    }

    return bestMatch;
  }

  /**
   * Returns a collection of time zone display name matches for the specified types in the given text at the given offset.
   *
   * @param text
   *            the text
   * @param start
   *            the start offset in the text
   * @param genericTypes
   *            the set of name types.
   * @return A collection of match info.
   */
  public Collection<GenericMatchInfo> find(final String text, final int start, final EnumSet<GenericNameType> genericTypes) {
    if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
      throw new IllegalArgumentException("bad input text or range");
    }
    // Find matches in the local trie
    Collection<GenericMatchInfo> results = findLocal(text, start, genericTypes);

    // Also find matches in the TimeZoneNames
    Collection<MatchInfo> tznamesMatches = findTimeZoneNames(text, start, genericTypes);
    if (tznamesMatches != null) {
      // transform matches and append them to local matches
      for (MatchInfo match : tznamesMatches) {
        if (results == null) {
          results = new LinkedList<GenericMatchInfo>();
        }
        results.add(createGenericMatchInfo(match));
      }
    }
    return results;
  }

  /**
   * Returns a <code>GenericMatchInfo</code> for the given <code>MatchInfo</code>.
   *
   * @param matchInfo
   *            the MatchInfo
   * @return A GenericMatchInfo
   */
  private GenericMatchInfo createGenericMatchInfo(final MatchInfo matchInfo) {
    GenericNameType nameType = null;
    TimeType timeType = TimeType.UNKNOWN;
    switch (matchInfo.nameType()) {
    case LONG_STANDARD:
      nameType = GenericNameType.LONG;
      timeType = TimeType.STANDARD;
      break;
    case LONG_GENERIC:
      nameType = GenericNameType.LONG;
      break;
    case SHORT_STANDARD:
      nameType = GenericNameType.SHORT;
      timeType = TimeType.STANDARD;
      break;
    case SHORT_GENERIC:
      nameType = GenericNameType.SHORT;
      break;
    default:
      break;
    }
    assert (nameType != null);

    String tzID = matchInfo.tzID();
    if (tzID == null) {
      String mzID = matchInfo.mzID();
      assert (mzID != null);
      tzID = _tznames.getReferenceZoneID(mzID, getTargetRegion());
    }
    assert (tzID != null);

    GenericMatchInfo gmatch = new GenericMatchInfo();
    gmatch.nameType = nameType;
    gmatch.tzID = tzID;
    gmatch.matchLength = matchInfo.matchLength();
    gmatch.timeType = timeType;

    return gmatch;
  }

  /**
   * Returns a collection of time zone display name matches for the specified types in the given text at the given offset. This method
   * only finds matches from the TimeZoneNames used by this object.
   *
   * @param text
   *            the text
   * @param start
   *            the start offset in the text
   * @param types
   *            the set of name types.
   * @return A collection of match info.
   */
  private Collection<MatchInfo> findTimeZoneNames(final String text, final int start, final EnumSet<GenericNameType> types) {
    Collection<MatchInfo> tznamesMatches = null;

    // Check if the target name type is really in the TimeZoneNames
    EnumSet<NameType> nameTypes = EnumSet.noneOf(NameType.class);
    if (types.contains(GenericNameType.LONG)) {
      nameTypes.add(NameType.LONG_GENERIC);
      nameTypes.add(NameType.LONG_STANDARD);
    }
    if (types.contains(GenericNameType.SHORT)) {
      nameTypes.add(NameType.SHORT_GENERIC);
      nameTypes.add(NameType.SHORT_STANDARD);
    }

    if (!nameTypes.isEmpty()) {
      // Find matches in the TimeZoneNames
      tznamesMatches = _tznames.find(text, start, nameTypes);
    }
    return tznamesMatches;
  }

  /**
   * Returns a collection of time zone display name matches for the specified types in the given text at the given offset. This method
   * only finds matches from the local trie, that contains 1) generic location names and 2) long/short generic partial location names,
   * used by this object.
   *
   * @param text
   *            the text
   * @param start
   *            the start offset in the text
   * @param types
   *            the set of name types.
   * @return A collection of match info.
   */
  private synchronized Collection<GenericMatchInfo> findLocal(final String text, final int start, final EnumSet<GenericNameType> types) {
    GenericNameSearchHandler handler = new GenericNameSearchHandler(types);
    _gnamesTrie.find(text, start, handler);
    if (handler.getMaxMatchLen() == (text.length() - start) || _gnamesTrieFullyLoaded) {
      // perfect match
      return handler.getMatches();
    }

    // All names are not yet loaded into the local trie.
    // Load all available names into the trie. This could be very heavy.

    Set<String> tzIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);
    for (String tzID : tzIDs) {
      loadStrings(tzID);
    }
    _gnamesTrieFullyLoaded = true;

    // now, try it again
    handler.resetResults();
    _gnamesTrie.find(text, start, handler);
    return handler.getMatches();
  }

  /**
   * <code>TimeZoneGenericNames</code> cache implementation.
   */
  private static class Cache extends SoftCache<String, TimeZoneGenericNames, ULocale> {

    /* (non-Javadoc)
     * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object)
     */
    @Override
    protected TimeZoneGenericNames createInstance(final String key, final ULocale data) {
      return new TimeZoneGenericNames(data).freeze();
    }

  }

  /*
   * The custom deserialization method.
   * This implementation only read locale used by the object.
   */
  private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    init();
  }

  /**
   * {@inheritDoc}
   */
  public boolean isFrozen() {
    return _frozen;
  }

  /**
   * {@inheritDoc}
   */
  public TimeZoneGenericNames freeze() {
    _frozen = true;
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public TimeZoneGenericNames cloneAsThawed() {
    TimeZoneGenericNames copy = null;
    try {
      copy = (TimeZoneGenericNames) super.clone();
      copy._frozen = false;
    } catch (Throwable t) {
      // This should never happen
    }
    return copy;
  }
}
TOP

Related Classes of com.ibm.icu.impl.TimeZoneGenericNames$Cache

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.
cs.com/analytics.js','ga'); ga('create', 'UA-20639858-1', 'auto'); ga('send', 'pageview');