/* ********************************************************************** * Copyright (c) 2003-2010 International Business Machines * Corporation and others. All Rights Reserved. ********************************************************************** * Author: Alan Liu * Created: September 4 2003 * Since: ICU 2.8 ********************************************************************** */ package com.ibm.icu.impl; import java.text.ParsePosition; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import com.ibm.icu.text.MessageFormat; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.util.SimpleTimeZone; import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; /** * This class, not to be instantiated, implements the meta-data * missing from the underlying core JDK implementation of time zones. * There are two missing features: Obtaining a list of available zones * for a given country (as defined by the Olson database), and * obtaining a list of equivalent zones for a given zone (as defined * by Olson links). * * This class uses a data class, ZoneMetaData, which is created by the * tool tz2icu. * * @author Alan Liu * @since ICU 2.8 */ public final class ZoneMeta { private static final boolean ASSERT = false; private static final String ZONEINFORESNAME = "zoneinfo64"; private static final String kREGIONS = "Regions"; private static final String kZONES = "Zones"; private static final String kNAMES = "Names"; private static final String kGMT_ID = "GMT"; private static final String kCUSTOM_TZ_PREFIX = "GMT"; /** * Returns a String array containing all system TimeZone IDs * associated with the given country. These IDs may be passed to * TimeZone.getTimeZone() to construct the * corresponding TimeZone object. * @param country a two-letter ISO 3166 country code, or null * to return zones not associated with any country * @return an array of IDs for system TimeZones in the given * country. If there are none, return a zero-length array. */ public static synchronized String[] getAvailableIDs(String country) { String[] ids = null; try{ UResourceBundle top = (ICUResourceBundle)ICUResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); UResourceBundle regions = top.get(kREGIONS); // Create a list of zones associated with the country List countryZones = new ArrayList(); for (int i = 0; i < regions.getSize(); i++) { if (country.equals(regions.getString(i))) { String zoneName = getZoneID(i); countryZones.add(zoneName); } } if (countryZones.size() > 0) { ids = countryZones.toArray(new String[countryZones.size()]); } } catch (MissingResourceException ex){ //throw away the exception } if (ids == null) { ids = new String[0]; } return ids; } public static synchronized String[] getAvailableIDs() { String[] ids = getZoneIDs(); if (ids == null) { return new String[0]; } return ids.clone(); } public static synchronized String[] getAvailableIDs(int offset){ String[] ids = null; String[] all = getZoneIDs(); if (all != null) { ArrayList zones = new ArrayList(); for (String zid : all) { // This is VERY inefficient. TimeZone z = TimeZone.getTimeZone(zid); // Make sure we get back the ID we wanted (if the ID is // invalid we get back GMT). if (z != null && z.getID().equals(zid) && z.getRawOffset() == offset) { zones.add(zid); } } if (zones.size() > 0) { ids = zones.toArray(new String[zones.size()]); } } if (ids == null) { ids = new String[0]; } return ids; } /** * Returns the number of IDs in the equivalency group that * includes the given ID. An equivalency group contains zones * that behave identically to the given zone. * *

If there are no equivalent zones, then this method returns * 0. This means either the given ID is not a valid zone, or it * is and there are no other equivalent zones. * @param id a system time zone ID * @return the number of zones in the equivalency group containing * 'id', or zero if there are no equivalent zones. * @see #getEquivalentID */ public static synchronized int countEquivalentIDs(String id) { int count = 0; try { UResourceBundle res = openOlsonResource(null, id); UResourceBundle links = res.get("links"); int[] v = links.getIntVector(); count = v.length; } catch (MissingResourceException ex) { // throw away } return count; } /** * Returns an ID in the equivalency group that includes the given * ID. An equivalency group contains zones that behave * identically to the given zone. * *

The given index must be in the range 0..n-1, where n is the * value returned by countEquivalentIDs(id). For * some value of 'index', the returned value will be equal to the * given id. If the given id is not a valid system time zone, or * if 'index' is out of range, then returns an empty string. * @param id a system time zone ID * @param index a value from 0 to n-1, where n is the value * returned by countEquivalentIDs(id) * @return the ID of the index-th zone in the equivalency group * containing 'id', or an empty string if 'id' is not a valid * system ID or 'index' is out of range * @see #countEquivalentIDs */ public static synchronized String getEquivalentID(String id, int index) { String result = ""; int zoneIdx = -1; if (index >= 0) { try { UResourceBundle res = openOlsonResource(null, id); UResourceBundle links = res.get("links"); int[] zones = links.getIntVector(); if (index < zones.length) { zoneIdx = zones[index]; } } catch (MissingResourceException ex) { // throw away zoneIdx = -1; } } if (zoneIdx >= 0) { String tmp = getZoneID(zoneIdx); if (tmp != null) { result = tmp; } } return result; } private static String[] ZONEIDS = null; /* * ICU frequently refers the zone ID array in zoneinfo resource */ private static synchronized String[] getZoneIDs() { if (ZONEIDS == null) { try { UResourceBundle top = UResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); UResourceBundle names = top.get(kNAMES); ZONEIDS = names.getStringArray(); } catch (MissingResourceException ex) { // throw away.. } } if (ZONEIDS == null) { ZONEIDS = new String[0]; } return ZONEIDS; } private static String getZoneID(int idx) { if (idx >= 0) { String[] ids = getZoneIDs(); if (idx < ids.length) { return ids[idx]; } } return null; } private static int getZoneIndex(String zid) { int zoneIdx = -1; String[] all = getZoneIDs(); if (all.length > 0) { int start = 0; int limit = all.length; int lastMid = Integer.MAX_VALUE; for (;;) { int mid = (start + limit) / 2; if (lastMid == mid) { /* Have we moved? */ break; /* We haven't moved, and it wasn't found. */ } lastMid = mid; int r = zid.compareTo(all[mid]); if (r == 0) { zoneIdx = mid; break; } else if(r < 0) { limit = mid; } else { start = mid; } } } return zoneIdx; } private static ICUCache CANONICAL_ID_CACHE = new SimpleCache(); private static ICUCache REGION_CACHE = new SimpleCache(); private static ICUCache SINGLE_COUNTRY_CACHE = new SimpleCache(); /** * Return the canonical id for this system tzid, which might be the id itself. * If the given system tzid is not know, return null. */ public static String getCanonicalSystemID(String tzid) { String canonical = CANONICAL_ID_CACHE.get(tzid); if (canonical == null) { int zoneIdx = getZoneIndex(tzid); if (zoneIdx >= 0) { try { UResourceBundle top = UResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); UResourceBundle zones = top.get(kZONES); UResourceBundle zone = zones.get(zoneIdx); if (zone.getType() == UResourceBundle.INT) { // resolve link String tmp = getZoneID(zone.getInt()); if (tmp != null) { canonical = tmp; } } else { canonical = tzid; } // check canonical mapping in CLDR UResourceBundle keyTypeData = UResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER); UResourceBundle typeAlias = keyTypeData.get("typeAlias"); UResourceBundle aliasesForKey = typeAlias.get("timezone"); String cldrCanonical = aliasesForKey.getString(canonical.replace('/', ':')); if (cldrCanonical != null) { canonical = cldrCanonical; } } catch (MissingResourceException e) { // fall through } } if (canonical != null) { CANONICAL_ID_CACHE.put(tzid, canonical); } } return canonical; } /** * Return the canonical country code for this tzid. If we have none, or if the time zone * is not associated with a country, return null. */ public static String getCanonicalCountry(String tzid) { String region = REGION_CACHE.get(tzid); if (region == null) { int zoneIdx = getZoneIndex(tzid); if (zoneIdx >= 0) { try { UResourceBundle top = UResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); UResourceBundle regions = top.get(kREGIONS); if (zoneIdx < regions.getSize()) { region = regions.getString(zoneIdx); } } catch (MissingResourceException e) { // throw away } if (region != null) { REGION_CACHE.put(tzid, region); } } } if (region.equals("001")) { return null; } return region; } /** * Return the country code if this is a 'single' time zone that can fallback to just * the country, otherwise return null. (Note, one must also check the locale data * to see that there is a localization for the country in order to implement * tr#35 appendix J step 5.) */ public static String getSingleCountry(String tzid) { String country = getCanonicalCountry(tzid); if (country != null) { Boolean isSingle = SINGLE_COUNTRY_CACHE.get(tzid); if (isSingle == null) { // This is not so efficient boolean isSingleCountryZone = true; String[] ids = TimeZone.getAvailableIDs(country); if (ids.length > 1) { // Check if there are multiple canonical zones included String canonical = getCanonicalSystemID(ids[0]); for (int i = 1; i < ids.length; i++) { if (!canonical.equals(getCanonicalSystemID(ids[i]))) { isSingleCountryZone = false; break; } } } isSingle = Boolean.valueOf(isSingleCountryZone); SINGLE_COUNTRY_CACHE.put(tzid, isSingle); } if (!isSingle) { country = null; } } return country; } /** * Returns a time zone location(region) format string defined by UTR#35. * e.g. "Italy Time", "United States (Los Angeles) Time" */ public static String getLocationFormat(String tzid, String city, ULocale locale) { String country_code = getCanonicalCountry(tzid); if (country_code == null) { // no location is associated return null; } String country = null; try { ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_REGION_BASE_NAME, locale); // // TODO: There is a design bug in UResourceBundle and getLoadingStatus() does not work well. // // if (rb.getLoadingStatus() != ICUResourceBundle.FROM_ROOT && rb.getLoadingStatus() != ICUResourceBundle.FROM_DEFAULT) { // country = ULocale.getDisplayCountry("xx_" + country_code, locale); // } // START WORKAROUND ULocale rbloc = rb.getULocale(); if (!rbloc.equals(ULocale.ROOT) && rbloc.getLanguage().equals(locale.getLanguage())) { country = ULocale.getDisplayCountry("xx_" + country_code, locale); } // END WORKAROUND } catch (MissingResourceException e) { // fall through } if (country == null || country.length() == 0) { country = country_code; } // This is not behavior specified in tr35, but behavior added by Mark. // TR35 says to display the country _only_ if there is a localization. if (getSingleCountry(tzid) != null) { // single country String regPat = getTZLocalizationInfo(locale, REGION_FORMAT); if (regPat == null) { regPat = DEF_REGION_FORMAT; } MessageFormat mf = new MessageFormat(regPat); return mf.format(new Object[] { country }); } if (city == null) { city = tzid.substring(tzid.lastIndexOf('/')+1).replace('_',' '); } String flbPat = getTZLocalizationInfo(locale, FALLBACK_FORMAT); if (flbPat == null) { flbPat = DEF_FALLBACK_FORMAT; } MessageFormat mf = new MessageFormat(flbPat); return mf.format(new Object[] { city, country }); } private static final String DEF_REGION_FORMAT = "{0}"; private static final String DEF_FALLBACK_FORMAT = "{1} ({0})"; public static final String HOUR = "hourFormat", GMT = "gmtFormat", REGION_FORMAT = "regionFormat", FALLBACK_FORMAT = "fallbackFormat", ZONE_STRINGS = "zoneStrings", FORWARD_SLASH = "/"; /** * Get the index'd tz datum for this locale. Index must be one of the * values PREFIX, HOUR, GMT, REGION_FORMAT, FALLBACK_FORMAT */ public static String getTZLocalizationInfo(ULocale locale, String format) { String result = null; try { ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance( ICUResourceBundle.ICU_ZONE_BASE_NAME, locale); result = bundle.getStringWithFallback(ZONE_STRINGS+FORWARD_SLASH+format); } catch (MissingResourceException e) { result = null; } return result; } // private static Set getValidIDs() { // // Construct list of time zones that are valid, according // // to the current underlying core JDK. We have to do this // // at runtime since we don't know what we're running on. // Set valid = new TreeSet(); // valid.addAll(Arrays.asList(java.util.TimeZone.getAvailableIDs())); // return valid; // } /** * Given an ID and the top-level resource of the zoneinfo resource, * open the appropriate resource for the given time zone. * Dereference links if necessary. * @param top the top level resource of the zoneinfo resource or null. * @param id zone id * @return the corresponding zone resource or null if not found */ public static UResourceBundle openOlsonResource(UResourceBundle top, String id) { UResourceBundle res = null; int zoneIdx = getZoneIndex(id); if (zoneIdx >= 0) { try { if (top == null) { top = UResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); } UResourceBundle zones = top.get(kZONES); UResourceBundle zone = zones.get(zoneIdx); if (zone.getType() == UResourceBundle.INT) { // resolve link zone = zones.get(zone.getInt()); } res = zone; } catch (MissingResourceException e) { res = null; } } return res; } private static ICUCache SYSTEM_ZONE_CACHE = new SimpleCache(); /** * Lookup the given name in our system zone table. If found, * instantiate a new zone of that name and return it. If not * found, return 0. */ public static TimeZone getSystemTimeZone(String id) { TimeZone z = SYSTEM_ZONE_CACHE.get(id); if (z == null) { try{ UResourceBundle top = UResourceBundle.getBundleInstance( ICUResourceBundle.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); UResourceBundle res = openOlsonResource(top, id); z = new OlsonTimeZone(top, res); z.setID(id); SYSTEM_ZONE_CACHE.put(id, z); }catch(Exception ex){ return null; } } return (TimeZone)z.clone(); } public static TimeZone getGMT(){ TimeZone z = new SimpleTimeZone(0, kGMT_ID); z.setID(kGMT_ID); return z; } // Maximum value of valid custom time zone hour/min private static final int kMAX_CUSTOM_HOUR = 23; private static final int kMAX_CUSTOM_MIN = 59; private static final int kMAX_CUSTOM_SEC = 59; /** * Parse a custom time zone identifier and return a corresponding zone. * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or * GMT[+-]hh. * @return a newly created SimpleTimeZone with the given offset and * no Daylight Savings Time, or null if the id cannot be parsed. */ public static TimeZone getCustomTimeZone(String id){ int[] fields = new int[4]; if (parseCustomID(id, fields)) { String zid = formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0); int offset = fields[0] * ((fields[1] * 60 + fields[2]) * 60 + fields[3]) * 1000; return new SimpleTimeZone(offset, zid); } return null; } /** * Parse a custom time zone identifier and return the normalized * custom time zone identifier for the given custom id string. * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or * GMT[+-]hh. * @return The normalized custom id string. */ public static String getCustomID(String id) { int[] fields = new int[4]; if (parseCustomID(id, fields)) { return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0); } return null; } /* * Parses the given custom time zone identifier * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or * GMT[+-]hh. * @param fields An array of int (length = 4) to receive the parsed * offset time fields. The sign is set to fields[0] (-1 or 1), * hour is set to fields[1], minute is set to fields[2] and second is * set to fields[3]. * @return Returns true when the given custom id is valid. */ static boolean parseCustomID(String id, int[] fields) { NumberFormat numberFormat = null; String idUppercase = id.toUpperCase(); if (id != null && id.length() > kGMT_ID.length() && idUppercase.startsWith(kGMT_ID)) { ParsePosition pos = new ParsePosition(kGMT_ID.length()); int sign = 1; int hour = 0; int min = 0; int sec = 0; if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) { sign = -1; } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) { return false; } pos.setIndex(pos.getIndex() + 1); numberFormat = NumberFormat.getInstance(); numberFormat.setParseIntegerOnly(true); // Look for either hh:mm, hhmm, or hh int start = pos.getIndex(); Number n = numberFormat.parse(id, pos); if (pos.getIndex() == start) { return false; } hour = n.intValue(); if (pos.getIndex() < id.length()){ if (pos.getIndex() - start > 2 || id.charAt(pos.getIndex()) != 0x003A /*':'*/) { return false; } // hh:mm pos.setIndex(pos.getIndex() + 1); int oldPos = pos.getIndex(); n = numberFormat.parse(id, pos); if ((pos.getIndex() - oldPos) != 2) { // must be 2 digits return false; } min = n.intValue(); if (pos.getIndex() < id.length()) { if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) { return false; } // [:ss] pos.setIndex(pos.getIndex() + 1); oldPos = pos.getIndex(); n = numberFormat.parse(id, pos); if (pos.getIndex() != id.length() || (pos.getIndex() - oldPos) != 2) { return false; } sec = n.intValue(); } } else { // Supported formats are below - // // HHmmss // Hmmss // HHmm // Hmm // HH // H int length = pos.getIndex() - start; if (length <= 0 || 6 < length) { // invalid length return false; } switch (length) { case 1: case 2: // already set to hour break; case 3: case 4: min = hour % 100; hour /= 100; break; case 5: case 6: sec = hour % 100; min = (hour/100) % 100; hour /= 10000; break; } } if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) { if (fields != null) { if (fields.length >= 1) { fields[0] = sign; } if (fields.length >= 2) { fields[1] = hour; } if (fields.length >= 3) { fields[2] = min; } if (fields.length >= 4) { fields[3] = sec; } } return true; } } return false; } /** * Creates a custom zone for the offset * @param offset GMT offset in milliseconds * @return A custom TimeZone for the offset with normalized time zone id */ public static TimeZone getCustomTimeZone(int offset) { boolean negative = false; int tmp = offset; if (offset < 0) { negative = true; tmp = -offset; } int hour, min, sec, millis; millis = tmp % 1000; if (ASSERT) { Assert.assrt("millis!=0", millis != 0); } tmp /= 1000; sec = tmp % 60; tmp /= 60; min = tmp % 60; hour = tmp / 60; // Note: No millisecond part included in TZID for now String zid = formatCustomID(hour, min, sec, negative); return new SimpleTimeZone(offset, zid); } /* * Returns the normalized custom TimeZone ID */ static String formatCustomID(int hour, int min, int sec, boolean negative) { // Create normalized time zone ID - GMT[+|-]hh:mm[:ss] StringBuilder zid = new StringBuilder(kCUSTOM_TZ_PREFIX); if (hour != 0 || min != 0) { if(negative) { zid.append('-'); } else { zid.append('+'); } // Always use US-ASCII digits if (hour < 10) { zid.append('0'); } zid.append(hour); zid.append(':'); if (min < 10) { zid.append('0'); } zid.append(min); if (sec != 0) { // Optional second field zid.append(':'); if (sec < 10) { zid.append('0'); } zid.append(sec); } } return zid.toString(); } /** * Returns a CLDR metazone ID for the given Olson tzid and time. */ public static String getMetazoneID(String olsonID, long date) { String mzid = null; List mappings = getOlsonToMatazones(olsonID); if (mappings != null) { for (int i = 0; i < mappings.size(); i++) { OlsonToMetaMappingEntry mzm = mappings.get(i); if (date >= mzm.from && date < mzm.to) { mzid = mzm.mzid; break; } } } return mzid; } private static ICUCache> OLSON_TO_META_CACHE = new SimpleCache>(); static class OlsonToMetaMappingEntry { String mzid; long from; long to; } static List getOlsonToMatazones(String tzid) { List mzMappings = OLSON_TO_META_CACHE.get(tzid); if (mzMappings == null) { try { UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "metaZones"); UResourceBundle metazoneInfoBundle = bundle.get("metazoneInfo"); String canonicalID = TimeZone.getCanonicalID(tzid); if (canonicalID == null) { return null; } String tzkey = canonicalID.replace('/', ':'); UResourceBundle zoneBundle = metazoneInfoBundle.get(tzkey); mzMappings = new LinkedList(); for (int idx = 0; idx < zoneBundle.getSize(); idx++) { UResourceBundle mz = zoneBundle.get(idx); String mzid = mz.getString(0); String from = "1970-01-01 00:00"; String to = "9999-12-31 23:59"; if (mz.getSize() == 3) { from = mz.getString(1); to = mz.getString(2); } OlsonToMetaMappingEntry mzmap = new OlsonToMetaMappingEntry(); mzmap.mzid = mzid.intern(); try { mzmap.from = parseDate(from); mzmap.to = parseDate(to); } catch (IllegalArgumentException baddate) { // skip this continue; } // Add this mapping to the list mzMappings.add(mzmap); } } catch (MissingResourceException mre) { // fall through } if (mzMappings != null) { OLSON_TO_META_CACHE.put(tzid, mzMappings); } } return mzMappings; } /* * Convert a date string used by metazone mappings to long. * The format used by CLDR metazone mapping is "yyyy-MM-dd HH:mm". * We do not want to use SimpleDateFormat to parse the metazone * mapping range strings in createOlsonToMeta, because it might be * called from SimpleDateFormat initialization code. */ static long parseDate (String text) throws IllegalArgumentException { int year = 0, month = 0, day = 0, hour = 0, min = 0; int idx; int n; // "yyyy" (0 - 3) for (idx = 0; idx <= 3; idx++) { n = text.charAt(idx) - '0'; if (n >= 0 && n < 10) { year = 10*year + n; } else { throw new IllegalArgumentException("Bad year"); } } // "MM" (5 - 6) for (idx = 5; idx <= 6; idx++) { n = text.charAt(idx) - '0'; if (n >= 0 && n < 10) { month = 10*month + n; } else { throw new IllegalArgumentException("Bad month"); } } // "dd" (8 - 9) for (idx = 8; idx <= 9; idx++) { n = text.charAt(idx) - '0'; if (n >= 0 && n < 10) { day = 10*day + n; } else { throw new IllegalArgumentException("Bad day"); } } // "HH" (11 - 12) for (idx = 11; idx <= 12; idx++) { n = text.charAt(idx) - '0'; if (n >= 0 && n < 10) { hour = 10*hour + n; } else { throw new IllegalArgumentException("Bad hour"); } } // "mm" (14 - 15) for (idx = 14; idx <= 15; idx++) { n = text.charAt(idx) - '0'; if (n >= 0 && n < 10) { min = 10*min + n; } else { throw new IllegalArgumentException("Bad minute"); } } long date = Grego.fieldsToDay(year, month - 1, day) * Grego.MILLIS_PER_DAY + hour * Grego.MILLIS_PER_HOUR + min * Grego.MILLIS_PER_MINUTE; return date; } private static ICUCache> META_TO_OLSON_CACHE = new SimpleCache>(); /** * Returns an Olson ID for the ginve metazone and region */ public static String getZoneIdByMetazone(String metazoneID, String region) { String tzid = null; // look up in the cache first Map zoneMap = META_TO_OLSON_CACHE.get(metazoneID); if (zoneMap == null) { try { // Create zone mappings for the metazone UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "metaZones"); UResourceBundle mapTimezones = bundle.get("mapTimezones"); UResourceBundle territoryMap = mapTimezones.get(metazoneID); zoneMap = new HashMap(); Set territories = territoryMap.keySet(); for (String territory : territories) { String zone = territoryMap.getString(territory); zoneMap.put(territory, zone); } // cache this META_TO_OLSON_CACHE.put(metazoneID, zoneMap); } catch (MissingResourceException e) { // ignore } } if (zoneMap != null) { tzid = zoneMap.get(region); if (tzid == null) { tzid = zoneMap.get("001"); // use the mapping for world as fallback } } return tzid; } }