001: /*
002: **********************************************************************
003: * Copyright (c) 2003-2006, International Business Machines
004: * Corporation and others. All Rights Reserved.
005: **********************************************************************
006: * Author: Alan Liu
007: * Created: September 4 2003
008: * Since: ICU 2.8
009: **********************************************************************
010: */
011: package com.ibm.icu.impl;
012:
013: import java.text.ParsePosition;
014: import java.util.Arrays;
015: import java.util.ArrayList;
016: import java.util.Collections;
017: import java.util.HashMap;
018: import java.util.Map;
019: import java.util.MissingResourceException;
020: import java.util.Set;
021: import java.util.TreeMap;
022: import java.util.TreeSet;
023: import java.util.Vector;
024:
025: import com.ibm.icu.text.MessageFormat;
026: import com.ibm.icu.text.NumberFormat;
027: import com.ibm.icu.text.SimpleDateFormat;
028: import com.ibm.icu.util.SimpleTimeZone;
029: import com.ibm.icu.util.TimeZone;
030: import com.ibm.icu.util.ULocale;
031: import com.ibm.icu.util.UResourceBundle;
032:
033: /**
034: * This class, not to be instantiated, implements the meta-data
035: * missing from the underlying core JDK implementation of time zones.
036: * There are two missing features: Obtaining a list of available zones
037: * for a given country (as defined by the Olson database), and
038: * obtaining a list of equivalent zones for a given zone (as defined
039: * by Olson links).
040: *
041: * This class uses a data class, ZoneMetaData, which is created by the
042: * tool tz2icu.
043: *
044: * @author Alan Liu
045: * @since ICU 2.8
046: */
047: public final class ZoneMeta {
048: private static final boolean ASSERT = false;
049:
050: /**
051: * Returns a String array containing all system TimeZone IDs
052: * associated with the given country. These IDs may be passed to
053: * <code>TimeZone.getTimeZone()</code> to construct the
054: * corresponding TimeZone object.
055: * @param country a two-letter ISO 3166 country code, or <code>null</code>
056: * to return zones not associated with any country
057: * @return an array of IDs for system TimeZones in the given
058: * country. If there are none, return a zero-length array.
059: */
060: public static synchronized String[] getAvailableIDs(String country) {
061: if (!getOlsonMeta()) {
062: return EMPTY;
063: }
064: try {
065: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
066: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
067: "zoneinfo",
068: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
069: ICUResourceBundle regions = top.get(kREGIONS);
070: ICUResourceBundle names = top.get(kNAMES); // dereference Zones section
071: ICUResourceBundle temp = regions.get(country);
072: int[] vector = temp.getIntVector();
073: if (ASSERT)
074: Assert.assrt("vector.length>0", vector.length > 0);
075: String[] ret = new String[vector.length];
076: for (int i = 0; i < vector.length; ++i) {
077: if (ASSERT)
078: Assert
079: .assrt(
080: "vector[i] >= 0 && vector[i] < OLSON_ZONE_COUNT",
081: vector[i] >= 0
082: && vector[i] < OLSON_ZONE_COUNT);
083: ret[i] = names.getString(vector[i]);
084: }
085: return ret;
086: } catch (MissingResourceException ex) {
087: //throw away the exception
088: }
089: return EMPTY;
090: }
091:
092: public static synchronized String[] getAvailableIDs() {
093: if (!getOlsonMeta()) {
094: return EMPTY;
095: }
096: try {
097: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
098: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
099: "zoneinfo",
100: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
101: ICUResourceBundle names = top.get(kNAMES); // dereference Zones section
102: return names.getStringArray();
103: } catch (MissingResourceException ex) {
104: //throw away the exception
105: }
106: return EMPTY;
107: }
108:
109: public static synchronized String[] getAvailableIDs(int offset) {
110: Vector vector = new Vector();
111: for (int i = 0; i < OLSON_ZONE_COUNT; ++i) {
112: String unistr;
113: if ((unistr = getID(i)) != null) {
114: // This is VERY inefficient.
115: TimeZone z = TimeZone.getTimeZone(unistr);
116: // Make sure we get back the ID we wanted (if the ID is
117: // invalid we get back GMT).
118: if (z != null && z.getID().equals(unistr)
119: && z.getRawOffset() == offset) {
120: vector.add(unistr);
121: }
122: }
123: }
124: if (!vector.isEmpty()) {
125: String[] strings = new String[vector.size()];
126: return (String[]) vector.toArray(strings);
127: }
128: return EMPTY;
129: }
130:
131: private static String getID(int i) {
132: try {
133: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
134: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
135: "zoneinfo",
136: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
137: ICUResourceBundle names = top.get(kNAMES); // dereference Zones section
138: return names.getString(i);
139: } catch (MissingResourceException ex) {
140: //throw away the exception
141: }
142: return null;
143: }
144:
145: /**
146: * Returns the number of IDs in the equivalency group that
147: * includes the given ID. An equivalency group contains zones
148: * that behave identically to the given zone.
149: *
150: * <p>If there are no equivalent zones, then this method returns
151: * 0. This means either the given ID is not a valid zone, or it
152: * is and there are no other equivalent zones.
153: * @param id a system time zone ID
154: * @return the number of zones in the equivalency group containing
155: * 'id', or zero if there are no equivalent zones.
156: * @see #getEquivalentID
157: */
158: public static synchronized int countEquivalentIDs(String id) {
159:
160: ICUResourceBundle res = openOlsonResource(id);
161: int size = res.getSize();
162: if (size == 4 || size == 6) {
163: ICUResourceBundle r = res.get(size - 1);
164: //result = ures_getSize(&r); // doesn't work
165: int[] v = r.getIntVector();
166: return v.length;
167: }
168: return 0;
169: }
170:
171: /**
172: * Returns an ID in the equivalency group that includes the given
173: * ID. An equivalency group contains zones that behave
174: * identically to the given zone.
175: *
176: * <p>The given index must be in the range 0..n-1, where n is the
177: * value returned by <code>countEquivalentIDs(id)</code>. For
178: * some value of 'index', the returned value will be equal to the
179: * given id. If the given id is not a valid system time zone, or
180: * if 'index' is out of range, then returns an empty string.
181: * @param id a system time zone ID
182: * @param index a value from 0 to n-1, where n is the value
183: * returned by <code>countEquivalentIDs(id)</code>
184: * @return the ID of the index-th zone in the equivalency group
185: * containing 'id', or an empty string if 'id' is not a valid
186: * system ID or 'index' is out of range
187: * @see #countEquivalentIDs
188: */
189: public static synchronized String getEquivalentID(String id,
190: int index) {
191: String result = "";
192: ICUResourceBundle res = openOlsonResource(id);
193: int zone = -1;
194: int size = res.getSize();
195: if (size == 4 || size == 6) {
196: ICUResourceBundle r = res.get(size - 1);
197: int[] v = r.getIntVector();
198: if (index >= 0 && index < size && getOlsonMeta()) {
199: zone = v[index];
200: }
201: }
202: if (zone >= 0) {
203: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
204: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
205: "zoneinfo",
206: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
207: ICUResourceBundle ares = top.get(kNAMES); // dereference Zones section
208: result = ares.getString(zone);
209:
210: }
211: return result;
212: }
213:
214: /**
215: * Create the equivalency map.
216: *
217: private static void createEquivMap() {
218: EQUIV_MAP = new TreeMap();
219:
220: // try leaving all ids as valid
221: // Set valid = getValidIDs();
222:
223: ArrayList list = new ArrayList(); // reuse this below
224:
225: for (int i=0; i<ZoneMetaData.EQUIV.length; ++i) {
226: String[] z = ZoneMetaData.EQUIV[i];
227: list.clear();
228: for (int j=0; j<z.length; ++j) {
229: // if (valid.contains(z[j])) {
230: list.add(z[j]);
231: // }
232: }
233: if (list.size() > 1) {
234: String[] a = (String[]) list.toArray(EMPTY);
235: for (int j=0; j<a.length; ++j) {
236: EQUIV_MAP.put(a[j], a);
237: }
238: }
239: }
240: }
241: */
242: private static String[] getCanonicalInfo(String id) {
243: if (canonicalMap == null) {
244: Map m = new HashMap();
245: for (int i = 0; i < ZoneInfoExt.CLDR_INFO.length; ++i) {
246: String[] clist = ZoneInfoExt.CLDR_INFO[i];
247: String c = clist[0];
248: m.put(c, clist);
249: for (int j = 3; j < clist.length; ++j) {
250: m.put(clist[j], clist);
251: }
252: }
253: synchronized (ZoneMeta.class) {
254: canonicalMap = m;
255: }
256: }
257:
258: return (String[]) canonicalMap.get(id);
259: }
260:
261: private static Map canonicalMap = null;
262:
263: /**
264: * Return the canonical id for this tzid, which might be the id itself.
265: * If there is no canonical id for it, return the passed-in id.
266: */
267: public static String getCanonicalID(String tzid) {
268: String[] info = getCanonicalInfo(tzid);
269: if (info != null) {
270: return info[0];
271: }
272: return tzid;
273: }
274:
275: /**
276: * Return the canonical country code for this tzid. If we have none, or if the time zone
277: * is not associated with a country, return null.
278: */
279: public static String getCanonicalCountry(String tzid) {
280: String[] info = getCanonicalInfo(tzid);
281: if (info != null) {
282: return info[1];
283: }
284: return null;
285: }
286:
287: /**
288: * Return the country code if this is a 'single' time zone that can fallback to just
289: * the country, otherwise return null. (Note, one must also check the locale data
290: * to see that there is a localization for the country in order to implement
291: * tr#35 appendix J step 5.)
292: */
293: public static String getSingleCountry(String tzid) {
294: String[] info = getCanonicalInfo(tzid);
295: if (info != null && info[2] != null) {
296: return info[1];
297: }
298: return null;
299: }
300:
301: /**
302: * Handle fallbacks for generic time (rules E.. G)
303: */
304: public static String displayFallback(String tzid, String city,
305: ULocale locale) {
306: String[] info = getCanonicalInfo(tzid);
307: if (info == null) {
308: return null; // error
309: }
310:
311: String country_code = info[1];
312: if (country_code == null) {
313: return null; // error!
314: }
315:
316: String country = null;
317: if (country_code != null) {
318: ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle
319: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
320: locale);
321: if (rb.getLoadingStatus() != rb.FROM_ROOT
322: && rb.getLoadingStatus() != rb.FROM_DEFAULT) {
323: country = ULocale.getDisplayCountry("xx_"
324: + country_code, locale);
325: }
326: if (country == null || country.length() == 0)
327: country = country_code;
328: }
329:
330: // This is not behavior specified in tr35, but behavior added by Mark.
331: // TR35 says to display the country _only_ if there is a localization.
332: if (info[2] != null) { // single country
333: return displayRegion(country, locale);
334: }
335:
336: if (city == null) {
337: city = tzid.substring(tzid.lastIndexOf('/') + 1).replace(
338: '_', ' ');
339: }
340:
341: String flbPat = getTZLocalizationInfo(locale, FALLBACK_FORMAT);
342: MessageFormat mf = new MessageFormat(flbPat);
343:
344: return mf.format(new Object[] { city, country });
345: }
346:
347: public static String displayRegion(String cityOrCountry,
348: ULocale locale) {
349: String regPat = getTZLocalizationInfo(locale, REGION_FORMAT);
350: MessageFormat mf = new MessageFormat(regPat);
351: return mf.format(new Object[] { cityOrCountry });
352: }
353:
354: public static String displayGMT(long value, ULocale locale) {
355: String msgpat = getTZLocalizationInfo(locale, GMT);
356: String dtepat = getTZLocalizationInfo(locale, HOUR);
357:
358: int n = dtepat.indexOf(';');
359: if (n != -1) {
360: if (value < 0) {
361: value = -value;
362: dtepat = dtepat.substring(n + 1);
363: } else {
364: dtepat = dtepat.substring(0, n);
365: }
366: }
367:
368: final long mph = 3600000;
369: final long mpm = 60000;
370:
371: SimpleDateFormat sdf = new SimpleDateFormat(dtepat, locale);
372: sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
373: String res = sdf.format(new Long(value));
374: MessageFormat mf = new MessageFormat(msgpat);
375: res = mf.format(new Object[] { res });
376: return res;
377: }
378:
379: public static final String HOUR = "hourFormat", GMT = "gmtFormat",
380: REGION_FORMAT = "regionFormat",
381: FALLBACK_FORMAT = "fallbackFormat",
382: ZONE_STRINGS = "zoneStrings", FORWARD_SLASH = "/";
383:
384: /**
385: * Get the index'd tz datum for this locale. Index must be one of the
386: * values PREFIX, HOUR, GMT, REGION_FORMAT, FALLBACK_FORMAT
387: */
388: public static String getTZLocalizationInfo(ULocale locale,
389: String format) {
390: ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle
391: .getBundleInstance(locale);
392: return bundle.getStringWithFallback(ZONE_STRINGS
393: + FORWARD_SLASH + format);
394: }
395:
396: private static Set getValidIDs() {
397: // Construct list of time zones that are valid, according
398: // to the current underlying core JDK. We have to do this
399: // at runtime since we don't know what we're running on.
400: Set valid = new TreeSet();
401: valid.addAll(Arrays
402: .asList(java.util.TimeZone.getAvailableIDs()));
403: return valid;
404: }
405:
406: /**
407: * Empty string array.
408: */
409: private static final String[] EMPTY = new String[0];
410:
411: /**
412: * Given an ID, open the appropriate resource for the given time zone.
413: * Dereference aliases if necessary.
414: * @param id zone id
415: * @param res resource, which must be ready for use (initialized but not open)
416: * @return top-level resource bundle
417: */
418: public static ICUResourceBundle openOlsonResource(String id) {
419: if (!getOlsonMeta()) {
420: return null;
421: }
422: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
423: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
424: "zoneinfo",
425: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
426: ICUResourceBundle res = getZoneByName(top, id);
427: // Dereference if this is an alias. Docs say result should be 1
428: // but it is 0 in 2.8 (?).
429: if (res.getSize() <= 1 && getOlsonMeta(top)) {
430: int deref = res.getInt() + 0;
431: ICUResourceBundle ares = top.get(kZONES); // dereference Zones section
432: res = ares.get(deref);
433: }
434: return res;
435: }
436:
437: /**
438: * Fetch a specific zone by name. Replaces the getByKey call.
439: * @param top Top timezone resource
440: * @param id Time zone ID
441: * @return the zone's bundle if found, or undefined if error. Reuses oldbundle.
442: */
443: private static ICUResourceBundle getZoneByName(
444: ICUResourceBundle top, String id) {
445: // load the Rules object
446: ICUResourceBundle tmp = top.get(kNAMES);
447:
448: // search for the string
449: int idx = findInStringArray(tmp, id);
450:
451: if ((idx == -1)) {
452: // not found
453: throw new MissingResourceException(kNAMES, tmp.resPath, id);
454: //ures_close(oldbundle);
455: //oldbundle = NULL;
456: } else {
457: tmp = top.get(kZONES); // get Zones object from top
458: tmp = tmp.get(idx); // get nth Zone object
459: }
460: return tmp;
461: }
462:
463: private static int findInStringArray(ICUResourceBundle array,
464: String id) {
465: int start = 0;
466: int limit = array.getSize();
467: int mid;
468: String u = null;
469: int lastMid = Integer.MAX_VALUE;
470: if ((limit < 1)) {
471: return -1;
472: }
473: for (;;) {
474: mid = (int) ((start + limit) / 2);
475: if (lastMid == mid) { /* Have we moved? */
476: break; /* We haven't moved, and it wasn't found. */
477: }
478: lastMid = mid;
479: u = array.getString(mid);
480: if (u == null) {
481: break;
482: }
483: int r = id.compareTo(u);
484: if (r == 0) {
485: return mid;
486: } else if (r < 0) {
487: limit = mid;
488: } else {
489: start = mid;
490: }
491: }
492: return -1;
493: }
494:
495: private static final String kZONEINFO = "zoneinfo";
496: private static final String kREGIONS = "Regions";
497: private static final String kZONES = "Zones";
498: private static final String kRULES = "Rules";
499: private static final String kNAMES = "Names";
500: private static final String kDEFAULT = "Default";
501: private static final String kGMT_ID = "GMT";
502: private static final String kCUSTOM_ID = "Custom";
503: //private static ICUResourceBundle zoneBundle = null;
504: private static java.util.Enumeration idEnum = null;
505: private static SoftCache zoneCache = new SoftCache();
506: /**
507: * The Olson data is stored the "zoneinfo" resource bundle.
508: * Sub-resources are organized into three ranges of data: Zones, final
509: * rules, and country tables. There is also a meta-data resource
510: * which has 3 integers: The number of zones, rules, and countries,
511: * respectively. The country count includes the non-country 'Default'.
512: */
513: static int OLSON_ZONE_START = -1; // starting index of zones
514: static int OLSON_ZONE_COUNT = 0; // count of zones
515:
516: /**
517: * Given a pointer to an open "zoneinfo" resource, load up the Olson
518: * meta-data. Return true if successful.
519: */
520: private static boolean getOlsonMeta(ICUResourceBundle top) {
521: if (OLSON_ZONE_START < 0) {
522: ICUResourceBundle res = top.get(kZONES);
523: OLSON_ZONE_COUNT = res.getSize();
524: OLSON_ZONE_START = 0;
525: }
526: return (OLSON_ZONE_START >= 0);
527: }
528:
529: /**
530: * Load up the Olson meta-data. Return true if successful.
531: */
532: private static boolean getOlsonMeta() {
533: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
534: .getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
535: "zoneinfo",
536: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
537: if (OLSON_ZONE_START < 0) {
538: getOlsonMeta(top);
539: }
540: return (OLSON_ZONE_START >= 0);
541: }
542:
543: /**
544: * Lookup the given name in our system zone table. If found,
545: * instantiate a new zone of that name and return it. If not
546: * found, return 0.
547: */
548: public static TimeZone getSystemTimeZone(String id) {
549: TimeZone z = (TimeZone) zoneCache.get(id);
550: if (z == null) {
551: try {
552: ICUResourceBundle top = (ICUResourceBundle) ICUResourceBundle
553: .getBundleInstance(
554: ICUResourceBundle.ICU_BASE_NAME,
555: "zoneinfo",
556: ICUResourceBundle.ICU_DATA_CLASS_LOADER);
557: ICUResourceBundle res = openOlsonResource(id);
558: z = new OlsonTimeZone(top, res);
559: z.setID(id);
560: zoneCache.put(id, z);
561: } catch (Exception ex) {
562: return null;
563: }
564: }
565: return (TimeZone) z.clone();
566: }
567:
568: public static TimeZone getGMT() {
569: TimeZone z = new SimpleTimeZone(0, kGMT_ID);
570: z.setID(kGMT_ID);
571: return z;
572: }
573:
574: /**
575: * Parse a custom time zone identifier and return a corresponding zone.
576: * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
577: * GMT[+-]hh.
578: * @return a newly created SimpleTimeZone with the given offset and
579: * no Daylight Savings Time, or null if the id cannot be parsed.
580: */
581: public static TimeZone getCustomTimeZone(String id) {
582:
583: NumberFormat numberFormat = null;
584:
585: String idUppercase = id.toUpperCase();
586:
587: if (id.length() > kGMT_ID.length()
588: && idUppercase.startsWith(kGMT_ID)) {
589: ParsePosition pos = new ParsePosition(kGMT_ID.length());
590: boolean negative = false;
591: long offset;
592:
593: if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/)
594: negative = true;
595: else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/)
596: return null;
597: pos.setIndex(pos.getIndex() + 1);
598:
599: numberFormat = NumberFormat.getInstance();
600:
601: numberFormat.setParseIntegerOnly(true);
602:
603: // Look for either hh:mm, hhmm, or hh
604: int start = pos.getIndex();
605:
606: Number n = numberFormat.parse(id, pos);
607: if (pos.getIndex() == start) {
608: return null;
609: }
610: offset = n.longValue();
611:
612: if (pos.getIndex() < id.length()
613: && id.charAt(pos.getIndex()) == 0x003A /*':'*/) {
614: // hh:mm
615: offset *= 60;
616: pos.setIndex(pos.getIndex() + 1);
617: int oldPos = pos.getIndex();
618: n = numberFormat.parse(id, pos);
619: if (pos.getIndex() == oldPos) {
620: return null;
621: }
622: offset += n.longValue();
623: } else {
624: // hhmm or hh
625:
626: // Be strict about interpreting something as hh; it must be
627: // an offset < 30, and it must be one or two digits. Thus
628: // 0010 is interpreted as 00:10, but 10 is interpreted as
629: // 10:00.
630: if (offset < 30 && (pos.getIndex() - start) <= 2)
631: offset *= 60; // hh, from 00 to 29; 30 is 00:30
632: else
633: offset = offset % 100 + offset / 100 * 60; // hhmm
634: }
635:
636: if (negative)
637: offset = -offset;
638:
639: TimeZone z = new SimpleTimeZone((int) (offset * 60000),
640: kCUSTOM_ID);
641: z.setID(kCUSTOM_ID);
642: return z;
643: }
644: return null;
645: }
646: }
|