0001: /*
0002: * Copyright 2001-2006 Stephen Colebourne
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016: package org.joda.time;
0017:
0018: import java.io.IOException;
0019: import java.io.ObjectInputStream;
0020: import java.io.ObjectOutputStream;
0021: import java.io.ObjectStreamException;
0022: import java.io.Serializable;
0023: import java.lang.ref.Reference;
0024: import java.lang.ref.SoftReference;
0025: import java.util.HashMap;
0026: import java.util.Locale;
0027: import java.util.Map;
0028: import java.util.Set;
0029: import java.util.TimeZone;
0030:
0031: import org.joda.time.chrono.BaseChronology;
0032: import org.joda.time.chrono.ISOChronology;
0033: import org.joda.time.field.FieldUtils;
0034: import org.joda.time.format.DateTimeFormat;
0035: import org.joda.time.format.DateTimeFormatter;
0036: import org.joda.time.format.DateTimeFormatterBuilder;
0037: import org.joda.time.format.FormatUtils;
0038: import org.joda.time.tz.DefaultNameProvider;
0039: import org.joda.time.tz.FixedDateTimeZone;
0040: import org.joda.time.tz.NameProvider;
0041: import org.joda.time.tz.Provider;
0042: import org.joda.time.tz.UTCProvider;
0043: import org.joda.time.tz.ZoneInfoProvider;
0044:
0045: /**
0046: * DateTimeZone represents a time zone.
0047: * <p>
0048: * A time zone is a system of rules to convert time from one geographic
0049: * location to another. For example, Paris, France is one hour ahead of
0050: * London, England. Thus when it is 10:00 in London, it is 11:00 in Paris.
0051: * <p>
0052: * All time zone rules are expressed, for historical reasons, relative to
0053: * Greenwich, London. Local time in Greenwich is referred to as Greenwich Mean
0054: * Time (GMT). This is similar, but not precisely identical, to Universal
0055: * Coordinated Time, or UTC. This library only uses the term UTC.
0056: * <p>
0057: * Using this system, America/Los_Angeles is expressed as UTC-08:00, or UTC-07:00
0058: * in the summer. The offset -08:00 indicates that America/Los_Angeles time is
0059: * obtained from UTC by adding -08:00, that is, by subtracting 8 hours.
0060: * <p>
0061: * The offset differs in the summer because of daylight saving time, or DST.
0062: * The folowing definitions of time are generally used:
0063: * <ul>
0064: * <li>UTC - The reference time.
0065: * <li>Standard Time - The local time without a daylight saving time offset.
0066: * For example, in Paris, standard time is UTC+01:00.
0067: * <li>Daylight Saving Time - The local time with a daylight saving time
0068: * offset. This offset is typically one hour, but not always. It is typically
0069: * used in most countries away from the equator. In Paris, daylight saving
0070: * time is UTC+02:00.
0071: * <li>Wall Time - This is what a local clock on the wall reads. This will be
0072: * either Standard Time or Daylight Saving Time depending on the time of year
0073: * and whether the location uses Daylight Saving Time.
0074: * </ul>
0075: * <p>
0076: * Unlike the Java TimeZone class, DateTimeZone is immutable. It also only
0077: * supports long format time zone ids. Thus EST and ECT are not accepted.
0078: * However, the factory that accepts a TimeZone will attempt to convert from
0079: * the old short id to a suitable long id.
0080: * <p>
0081: * DateTimeZone is thread-safe and immutable, and all subclasses must be as
0082: * well.
0083: *
0084: * @author Brian S O'Neill
0085: * @author Stephen Colebourne
0086: * @since 1.0
0087: */
0088: public abstract class DateTimeZone implements Serializable {
0089:
0090: /** Serialization version. */
0091: private static final long serialVersionUID = 5546345482340108586L;
0092:
0093: /** The time zone for Universal Coordinated Time */
0094: public static final DateTimeZone UTC = new FixedDateTimeZone("UTC",
0095: "UTC", 0, 0);
0096:
0097: /** The instance that is providing time zones. */
0098: private static Provider cProvider;
0099: /** The instance that is providing time zone names. */
0100: private static NameProvider cNameProvider;
0101: /** The set of ID strings. */
0102: private static Set cAvailableIDs;
0103: /** The default time zone. */
0104: private static DateTimeZone cDefault;
0105: /** A formatter for printing and parsing zones. */
0106: private static DateTimeFormatter cOffsetFormatter;
0107:
0108: /** Cache that maps fixed offset strings to softly referenced DateTimeZones */
0109: private static Map iFixedOffsetCache;
0110:
0111: /** Cache of old zone IDs to new zone IDs */
0112: private static Map cZoneIdConversion;
0113:
0114: static {
0115: setProvider0(null);
0116: setNameProvider0(null);
0117:
0118: try {
0119: try {
0120: cDefault = forID(System.getProperty("user.timezone"));
0121: } catch (RuntimeException ex) {
0122: // ignored
0123: }
0124: if (cDefault == null) {
0125: cDefault = forTimeZone(TimeZone.getDefault());
0126: }
0127: } catch (IllegalArgumentException ex) {
0128: // ignored
0129: }
0130:
0131: if (cDefault == null) {
0132: cDefault = UTC;
0133: }
0134: }
0135:
0136: //-----------------------------------------------------------------------
0137: /**
0138: * Gets the default time zone.
0139: *
0140: * @return the default datetime zone object
0141: */
0142: public static DateTimeZone getDefault() {
0143: return cDefault;
0144: }
0145:
0146: /**
0147: * Sets the default time zone.
0148: *
0149: * @param zone the default datetime zone object, must not be null
0150: * @throws IllegalArgumentException if the zone is null
0151: * @throws SecurityException if the application has insufficient security rights
0152: */
0153: public static void setDefault(DateTimeZone zone)
0154: throws SecurityException {
0155: SecurityManager sm = System.getSecurityManager();
0156: if (sm != null) {
0157: sm.checkPermission(new JodaTimePermission(
0158: "DateTimeZone.setDefault"));
0159: }
0160: if (zone == null) {
0161: throw new IllegalArgumentException(
0162: "The datetime zone must not be null");
0163: }
0164: cDefault = zone;
0165: }
0166:
0167: //-----------------------------------------------------------------------
0168: /**
0169: * Gets a time zone instance for the specified time zone id.
0170: * <p>
0171: * The time zone id may be one of those returned by getAvailableIDs.
0172: * Short ids, as accepted by {@link java.util.TimeZone}, are not accepted.
0173: * All IDs must be specified in the long format.
0174: * The exception is UTC, which is an acceptable id.
0175: * <p>
0176: * Alternatively a locale independent, fixed offset, datetime zone can
0177: * be specified. The form <code>[+-]hh:mm</code> can be used.
0178: *
0179: * @param id the ID of the datetime zone, null means default
0180: * @return the DateTimeZone object for the ID
0181: * @throws IllegalArgumentException if the ID is not recognised
0182: */
0183: public static DateTimeZone forID(String id) {
0184: if (id == null) {
0185: return getDefault();
0186: }
0187: if (id.equals("UTC")) {
0188: return DateTimeZone.UTC;
0189: }
0190: DateTimeZone zone = cProvider.getZone(id);
0191: if (zone != null) {
0192: return zone;
0193: }
0194: if (id.startsWith("+") || id.startsWith("-")) {
0195: int offset = parseOffset(id);
0196: if (offset == 0L) {
0197: return DateTimeZone.UTC;
0198: } else {
0199: id = printOffset(offset);
0200: return fixedOffsetZone(id, offset);
0201: }
0202: }
0203: throw new IllegalArgumentException(
0204: "The datetime zone id is not recognised: " + id);
0205: }
0206:
0207: /**
0208: * Gets a time zone instance for the specified offset to UTC in hours.
0209: * This method assumes standard length hours.
0210: * <p>
0211: * This factory is a convenient way of constructing zones with a fixed offset.
0212: *
0213: * @param hoursOffset the offset in hours from UTC
0214: * @return the DateTimeZone object for the offset
0215: * @throws IllegalArgumentException if the offset is too large or too small
0216: */
0217: public static DateTimeZone forOffsetHours(int hoursOffset)
0218: throws IllegalArgumentException {
0219: return forOffsetHoursMinutes(hoursOffset, 0);
0220: }
0221:
0222: /**
0223: * Gets a time zone instance for the specified offset to UTC in hours and minutes.
0224: * This method assumes 60 minutes in an hour, and standard length minutes.
0225: * <p>
0226: * This factory is a convenient way of constructing zones with a fixed offset.
0227: * The minutes value is always positive and in the range 0 to 59.
0228: * If constructed with the values (-2, 30), the resultiong zone is '-02:30'.
0229: *
0230: * @param hoursOffset the offset in hours from UTC
0231: * @param minutesOffset the offset in minutes from UTC, must be between 0 and 59 inclusive
0232: * @return the DateTimeZone object for the offset
0233: * @throws IllegalArgumentException if the offset or minute is too large or too small
0234: */
0235: public static DateTimeZone forOffsetHoursMinutes(int hoursOffset,
0236: int minutesOffset) throws IllegalArgumentException {
0237: if (hoursOffset == 0 && minutesOffset == 0) {
0238: return DateTimeZone.UTC;
0239: }
0240: if (minutesOffset < 0 || minutesOffset > 59) {
0241: throw new IllegalArgumentException("Minutes out of range: "
0242: + minutesOffset);
0243: }
0244: int offset = 0;
0245: try {
0246: int hoursInMinutes = FieldUtils.safeMultiply(hoursOffset,
0247: 60);
0248: if (hoursInMinutes < 0) {
0249: minutesOffset = FieldUtils.safeAdd(hoursInMinutes,
0250: -minutesOffset);
0251: } else {
0252: minutesOffset = FieldUtils.safeAdd(hoursInMinutes,
0253: minutesOffset);
0254: }
0255: offset = FieldUtils.safeMultiply(minutesOffset,
0256: DateTimeConstants.MILLIS_PER_MINUTE);
0257: } catch (ArithmeticException ex) {
0258: throw new IllegalArgumentException("Offset is too large");
0259: }
0260: return forOffsetMillis(offset);
0261: }
0262:
0263: /**
0264: * Gets a time zone instance for the specified offset to UTC in milliseconds.
0265: *
0266: * @param millisOffset the offset in millis from UTC
0267: * @return the DateTimeZone object for the offset
0268: */
0269: public static DateTimeZone forOffsetMillis(int millisOffset) {
0270: String id = printOffset(millisOffset);
0271: return fixedOffsetZone(id, millisOffset);
0272: }
0273:
0274: /**
0275: * Gets a time zone instance for a JDK TimeZone.
0276: * <p>
0277: * DateTimeZone only accepts a subset of the IDs from TimeZone. The
0278: * excluded IDs are the short three letter form (except UTC). This
0279: * method will attempt to convert between time zones created using the
0280: * short IDs and the full version.
0281: * <p>
0282: * This method is not designed to parse time zones with rules created by
0283: * applications using <code>SimpleTimeZone</code> directly.
0284: *
0285: * @param zone the zone to convert, null means default
0286: * @return the DateTimeZone object for the zone
0287: * @throws IllegalArgumentException if the zone is not recognised
0288: */
0289: public static DateTimeZone forTimeZone(TimeZone zone) {
0290: if (zone == null) {
0291: return getDefault();
0292: }
0293: final String id = zone.getID();
0294: if (id.equals("UTC")) {
0295: return DateTimeZone.UTC;
0296: }
0297:
0298: // Convert from old alias before consulting provider since they may differ.
0299: DateTimeZone dtz = null;
0300: String convId = getConvertedId(id);
0301: if (convId != null) {
0302: dtz = cProvider.getZone(convId);
0303: }
0304: if (dtz == null) {
0305: dtz = cProvider.getZone(id);
0306: }
0307: if (dtz != null) {
0308: return dtz;
0309: }
0310:
0311: // Support GMT+/-hh:mm formats
0312: if (convId == null) {
0313: convId = zone.getDisplayName();
0314: if (convId.startsWith("GMT+") || convId.startsWith("GMT-")) {
0315: convId = convId.substring(3);
0316: int offset = parseOffset(convId);
0317: if (offset == 0L) {
0318: return DateTimeZone.UTC;
0319: } else {
0320: convId = printOffset(offset);
0321: return fixedOffsetZone(convId, offset);
0322: }
0323: }
0324: }
0325:
0326: throw new IllegalArgumentException(
0327: "The datetime zone id is not recognised: " + id);
0328: }
0329:
0330: //-----------------------------------------------------------------------
0331: /**
0332: * Gets the zone using a fixed offset amount.
0333: *
0334: * @param id the zone id
0335: * @param offset the offset in millis
0336: * @return the zone
0337: */
0338: private static synchronized DateTimeZone fixedOffsetZone(String id,
0339: int offset) {
0340: if (offset == 0) {
0341: return DateTimeZone.UTC;
0342: }
0343: if (iFixedOffsetCache == null) {
0344: iFixedOffsetCache = new HashMap();
0345: }
0346: DateTimeZone zone;
0347: Reference ref = (Reference) iFixedOffsetCache.get(id);
0348: if (ref != null) {
0349: zone = (DateTimeZone) ref.get();
0350: if (zone != null) {
0351: return zone;
0352: }
0353: }
0354: zone = new FixedDateTimeZone(id, null, offset, offset);
0355: iFixedOffsetCache.put(id, new SoftReference(zone));
0356: return zone;
0357: }
0358:
0359: /**
0360: * Gets all the available IDs supported.
0361: *
0362: * @return an unmodifiable Set of String IDs
0363: */
0364: public static Set getAvailableIDs() {
0365: return cAvailableIDs;
0366: }
0367:
0368: //-----------------------------------------------------------------------
0369: /**
0370: * Gets the zone provider factory.
0371: * <p>
0372: * The zone provider is a pluggable instance factory that supplies the
0373: * actual instances of DateTimeZone.
0374: *
0375: * @return the provider
0376: */
0377: public static Provider getProvider() {
0378: return cProvider;
0379: }
0380:
0381: /**
0382: * Sets the zone provider factory.
0383: * <p>
0384: * The zone provider is a pluggable instance factory that supplies the
0385: * actual instances of DateTimeZone.
0386: *
0387: * @param provider provider to use, or null for default
0388: * @throws SecurityException if you do not have the permission DateTimeZone.setProvider
0389: * @throws IllegalArgumentException if the provider is invalid
0390: */
0391: public static void setProvider(Provider provider)
0392: throws SecurityException {
0393: SecurityManager sm = System.getSecurityManager();
0394: if (sm != null) {
0395: sm.checkPermission(new JodaTimePermission(
0396: "DateTimeZone.setProvider"));
0397: }
0398: setProvider0(provider);
0399: }
0400:
0401: /**
0402: * Sets the zone provider factory without performing the security check.
0403: *
0404: * @param provider provider to use, or null for default
0405: * @throws IllegalArgumentException if the provider is invalid
0406: */
0407: private static void setProvider0(Provider provider) {
0408: if (provider == null) {
0409: provider = getDefaultProvider();
0410: }
0411: Set ids = provider.getAvailableIDs();
0412: if (ids == null || ids.size() == 0) {
0413: throw new IllegalArgumentException(
0414: "The provider doesn't have any available ids");
0415: }
0416: if (!ids.contains("UTC")) {
0417: throw new IllegalArgumentException(
0418: "The provider doesn't support UTC");
0419: }
0420: if (!UTC.equals(provider.getZone("UTC"))) {
0421: throw new IllegalArgumentException(
0422: "Invalid UTC zone provided");
0423: }
0424: cProvider = provider;
0425: cAvailableIDs = ids;
0426: }
0427:
0428: /**
0429: * Gets the default zone provider.
0430: * <p>
0431: * Tries the system property <code>org.joda.time.DateTimeZone.Provider</code>.
0432: * Then tries a <code>ZoneInfoProvider</code> using the data in <code>org/joda/time/tz/data</code>.
0433: * Then uses <code>UTCProvider</code>.
0434: *
0435: * @return the default name provider
0436: */
0437: private static Provider getDefaultProvider() {
0438: Provider provider = null;
0439:
0440: try {
0441: String providerClass = System
0442: .getProperty("org.joda.time.DateTimeZone.Provider");
0443: if (providerClass != null) {
0444: try {
0445: provider = (Provider) Class.forName(providerClass)
0446: .newInstance();
0447: } catch (Exception ex) {
0448: Thread thread = Thread.currentThread();
0449: thread.getThreadGroup().uncaughtException(thread,
0450: ex);
0451: }
0452: }
0453: } catch (SecurityException ex) {
0454: // ignored
0455: }
0456:
0457: if (provider == null) {
0458: try {
0459: provider = new ZoneInfoProvider("org/joda/time/tz/data");
0460: } catch (Exception ex) {
0461: Thread thread = Thread.currentThread();
0462: thread.getThreadGroup().uncaughtException(thread, ex);
0463: }
0464: }
0465:
0466: if (provider == null) {
0467: provider = new UTCProvider();
0468: }
0469:
0470: return provider;
0471: }
0472:
0473: //-----------------------------------------------------------------------
0474: /**
0475: * Gets the name provider factory.
0476: * <p>
0477: * The name provider is a pluggable instance factory that supplies the
0478: * names of each DateTimeZone.
0479: *
0480: * @return the provider
0481: */
0482: public static NameProvider getNameProvider() {
0483: return cNameProvider;
0484: }
0485:
0486: /**
0487: * Sets the name provider factory.
0488: * <p>
0489: * The name provider is a pluggable instance factory that supplies the
0490: * names of each DateTimeZone.
0491: *
0492: * @param nameProvider provider to use, or null for default
0493: * @throws SecurityException if you do not have the permission DateTimeZone.setNameProvider
0494: * @throws IllegalArgumentException if the provider is invalid
0495: */
0496: public static void setNameProvider(NameProvider nameProvider)
0497: throws SecurityException {
0498: SecurityManager sm = System.getSecurityManager();
0499: if (sm != null) {
0500: sm.checkPermission(new JodaTimePermission(
0501: "DateTimeZone.setNameProvider"));
0502: }
0503: setNameProvider0(nameProvider);
0504: }
0505:
0506: /**
0507: * Sets the name provider factory without performing the security check.
0508: *
0509: * @param nameProvider provider to use, or null for default
0510: * @throws IllegalArgumentException if the provider is invalid
0511: */
0512: private static void setNameProvider0(NameProvider nameProvider) {
0513: if (nameProvider == null) {
0514: nameProvider = getDefaultNameProvider();
0515: }
0516: cNameProvider = nameProvider;
0517: }
0518:
0519: /**
0520: * Gets the default name provider.
0521: * <p>
0522: * Tries the system property <code>org.joda.time.DateTimeZone.NameProvider</code>.
0523: * Then uses <code>DefaultNameProvider</code>.
0524: *
0525: * @return the default name provider
0526: */
0527: private static NameProvider getDefaultNameProvider() {
0528: NameProvider nameProvider = null;
0529: try {
0530: String providerClass = System
0531: .getProperty("org.joda.time.DateTimeZone.NameProvider");
0532: if (providerClass != null) {
0533: try {
0534: nameProvider = (NameProvider) Class.forName(
0535: providerClass).newInstance();
0536: } catch (Exception ex) {
0537: Thread thread = Thread.currentThread();
0538: thread.getThreadGroup().uncaughtException(thread,
0539: ex);
0540: }
0541: }
0542: } catch (SecurityException ex) {
0543: // ignore
0544: }
0545:
0546: if (nameProvider == null) {
0547: nameProvider = new DefaultNameProvider();
0548: }
0549:
0550: return nameProvider;
0551: }
0552:
0553: //-----------------------------------------------------------------------
0554: /**
0555: * Converts an old style id to a new style id.
0556: *
0557: * @param id the old style id
0558: * @return the new style id, null if not found
0559: */
0560: private static synchronized String getConvertedId(String id) {
0561: Map map = cZoneIdConversion;
0562: if (map == null) {
0563: // Backwards compatibility with TimeZone.
0564: map = new HashMap();
0565: map.put("GMT", "UTC");
0566: map.put("MIT", "Pacific/Apia");
0567: map.put("HST", "Pacific/Honolulu");
0568: map.put("AST", "America/Anchorage");
0569: map.put("PST", "America/Los_Angeles");
0570: map.put("MST", "America/Denver");
0571: map.put("PNT", "America/Phoenix");
0572: map.put("CST", "America/Chicago");
0573: map.put("EST", "America/New_York");
0574: map.put("IET", "America/Indianapolis");
0575: map.put("PRT", "America/Puerto_Rico");
0576: map.put("CNT", "America/St_Johns");
0577: map.put("AGT", "America/Buenos_Aires");
0578: map.put("BET", "America/Sao_Paulo");
0579: map.put("WET", "Europe/London");
0580: map.put("ECT", "Europe/Paris");
0581: map.put("ART", "Africa/Cairo");
0582: map.put("CAT", "Africa/Harare");
0583: map.put("EET", "Europe/Bucharest");
0584: map.put("EAT", "Africa/Addis_Ababa");
0585: map.put("MET", "Asia/Tehran");
0586: map.put("NET", "Asia/Yerevan");
0587: map.put("PLT", "Asia/Karachi");
0588: map.put("IST", "Asia/Calcutta");
0589: map.put("BST", "Asia/Dhaka");
0590: map.put("VST", "Asia/Saigon");
0591: map.put("CTT", "Asia/Shanghai");
0592: map.put("JST", "Asia/Tokyo");
0593: map.put("ACT", "Australia/Darwin");
0594: map.put("AET", "Australia/Sydney");
0595: map.put("SST", "Pacific/Guadalcanal");
0596: map.put("NST", "Pacific/Auckland");
0597: cZoneIdConversion = map;
0598: }
0599: return (String) map.get(id);
0600: }
0601:
0602: private static int parseOffset(String str) {
0603: Chronology chrono;
0604: if (cDefault != null) {
0605: chrono = ISOChronology.getInstanceUTC();
0606: } else {
0607: // Can't use a real chronology if called during class
0608: // initialization. Offset parser doesn't need it anyhow.
0609: chrono = new BaseChronology() {
0610: public DateTimeZone getZone() {
0611: return null;
0612: }
0613:
0614: public Chronology withUTC() {
0615: return this ;
0616: }
0617:
0618: public Chronology withZone(DateTimeZone zone) {
0619: return this ;
0620: }
0621:
0622: public String toString() {
0623: return getClass().getName();
0624: }
0625: };
0626: }
0627:
0628: return -(int) offsetFormatter().withChronology(chrono)
0629: .parseMillis(str);
0630: }
0631:
0632: /**
0633: * Formats a timezone offset string.
0634: * <p>
0635: * This method is kept separate from the formatting classes to speed and
0636: * simplify startup and classloading.
0637: *
0638: * @param offset the offset in milliseconds
0639: * @return the time zone string
0640: */
0641: private static String printOffset(int offset) {
0642: StringBuffer buf = new StringBuffer();
0643: if (offset >= 0) {
0644: buf.append('+');
0645: } else {
0646: buf.append('-');
0647: offset = -offset;
0648: }
0649:
0650: int hours = offset / DateTimeConstants.MILLIS_PER_HOUR;
0651: FormatUtils.appendPaddedInteger(buf, hours, 2);
0652: offset -= hours * (int) DateTimeConstants.MILLIS_PER_HOUR;
0653:
0654: int minutes = offset / DateTimeConstants.MILLIS_PER_MINUTE;
0655: buf.append(':');
0656: FormatUtils.appendPaddedInteger(buf, minutes, 2);
0657: offset -= minutes * DateTimeConstants.MILLIS_PER_MINUTE;
0658: if (offset == 0) {
0659: return buf.toString();
0660: }
0661:
0662: int seconds = offset / DateTimeConstants.MILLIS_PER_SECOND;
0663: buf.append(':');
0664: FormatUtils.appendPaddedInteger(buf, seconds, 2);
0665: offset -= seconds * DateTimeConstants.MILLIS_PER_SECOND;
0666: if (offset == 0) {
0667: return buf.toString();
0668: }
0669:
0670: buf.append('.');
0671: FormatUtils.appendPaddedInteger(buf, offset, 3);
0672: return buf.toString();
0673: }
0674:
0675: /**
0676: * Gets a printer/parser for managing the offset id formatting.
0677: *
0678: * @return the formatter
0679: */
0680: private static synchronized DateTimeFormatter offsetFormatter() {
0681: if (cOffsetFormatter == null) {
0682: cOffsetFormatter = new DateTimeFormatterBuilder()
0683: .appendTimeZoneOffset(null, true, 2, 4)
0684: .toFormatter();
0685: }
0686: return cOffsetFormatter;
0687: }
0688:
0689: // Instance fields and methods
0690: //--------------------------------------------------------------------
0691:
0692: private final String iID;
0693:
0694: /**
0695: * Constructor.
0696: *
0697: * @param id the id to use
0698: * @throws IllegalArgumentException if the id is null
0699: */
0700: protected DateTimeZone(String id) {
0701: if (id == null) {
0702: throw new IllegalArgumentException("Id must not be null");
0703: }
0704: iID = id;
0705: }
0706:
0707: // Principal methods
0708: //--------------------------------------------------------------------
0709:
0710: /**
0711: * Gets the ID of this datetime zone.
0712: *
0713: * @return the ID of this datetime zone
0714: */
0715: public final String getID() {
0716: return iID;
0717: }
0718:
0719: /**
0720: * Returns a non-localized name that is unique to this time zone. It can be
0721: * combined with id to form a unique key for fetching localized names.
0722: *
0723: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
0724: * @return name key or null if id should be used for names
0725: */
0726: public abstract String getNameKey(long instant);
0727:
0728: /**
0729: * Gets the short name of this datetime zone suitable for display using
0730: * the default locale.
0731: * <p>
0732: * If the name is not available for the locale, then this method returns a
0733: * string in the format <code>[+-]hh:mm</code>.
0734: *
0735: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
0736: * @return the human-readable short name in the default locale
0737: */
0738: public final String getShortName(long instant) {
0739: return getShortName(instant, null);
0740: }
0741:
0742: /**
0743: * Gets the short name of this datetime zone suitable for display using
0744: * the specified locale.
0745: * <p>
0746: * If the name is not available for the locale, then this method returns a
0747: * string in the format <code>[+-]hh:mm</code>.
0748: *
0749: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
0750: * @param locale the locale to get the name for
0751: * @return the human-readable short name in the specified locale
0752: */
0753: public String getShortName(long instant, Locale locale) {
0754: if (locale == null) {
0755: locale = Locale.getDefault();
0756: }
0757: String nameKey = getNameKey(instant);
0758: if (nameKey == null) {
0759: return iID;
0760: }
0761: String name = cNameProvider.getShortName(locale, iID, nameKey);
0762: if (name != null) {
0763: return name;
0764: }
0765: return printOffset(getOffset(instant));
0766: }
0767:
0768: /**
0769: * Gets the long name of this datetime zone suitable for display using
0770: * the default locale.
0771: * <p>
0772: * If the name is not available for the locale, then this method returns a
0773: * string in the format <code>[+-]hh:mm</code>.
0774: *
0775: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
0776: * @return the human-readable long name in the default locale
0777: */
0778: public final String getName(long instant) {
0779: return getName(instant, null);
0780: }
0781:
0782: /**
0783: * Gets the long name of this datetime zone suitable for display using
0784: * the specified locale.
0785: * <p>
0786: * If the name is not available for the locale, then this method returns a
0787: * string in the format <code>[+-]hh:mm</code>.
0788: *
0789: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
0790: * @param locale the locale to get the name for
0791: * @return the human-readable long name in the specified locale
0792: */
0793: public String getName(long instant, Locale locale) {
0794: if (locale == null) {
0795: locale = Locale.getDefault();
0796: }
0797: String nameKey = getNameKey(instant);
0798: if (nameKey == null) {
0799: return iID;
0800: }
0801: String name = cNameProvider.getName(locale, iID, nameKey);
0802: if (name != null) {
0803: return name;
0804: }
0805: return printOffset(getOffset(instant));
0806: }
0807:
0808: /**
0809: * Gets the millisecond offset to add to UTC to get local time.
0810: *
0811: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the offset for
0812: * @return the millisecond offset to add to UTC to get local time
0813: */
0814: public abstract int getOffset(long instant);
0815:
0816: /**
0817: * Gets the millisecond offset to add to UTC to get local time.
0818: *
0819: * @param instant instant to get the offset for, null means now
0820: * @return the millisecond offset to add to UTC to get local time
0821: */
0822: public final int getOffset(ReadableInstant instant) {
0823: if (instant == null) {
0824: return getOffset(DateTimeUtils.currentTimeMillis());
0825: }
0826: return getOffset(instant.getMillis());
0827: }
0828:
0829: /**
0830: * Gets the standard millisecond offset to add to UTC to get local time,
0831: * when standard time is in effect.
0832: *
0833: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the offset for
0834: * @return the millisecond offset to add to UTC to get local time
0835: */
0836: public abstract int getStandardOffset(long instant);
0837:
0838: /**
0839: * Checks whether, at a particular instant, the offset is standard or not.
0840: * <p>
0841: * This method can be used to determine whether Summer Time (DST) applies.
0842: * As a general rule, if the offset at the specified instant is standard,
0843: * then either Winter time applies, or there is no Summer Time. If the
0844: * instant is not standard, then Summer Time applies.
0845: * <p>
0846: * The implementation of the method is simply whether {@link #getOffset(long)}
0847: * equals {@link #getStandardOffset(long)} at the specified instant.
0848: *
0849: * @param instant milliseconds from 1970-01-01T00:00:00Z to get the offset for
0850: * @return true if the offset at the given instant is the standard offset
0851: * @since 1.5
0852: */
0853: public boolean isStandardOffset(long instant) {
0854: return getOffset(instant) == getStandardOffset(instant);
0855: }
0856:
0857: /**
0858: * Gets the millisecond offset to subtract from local time to get UTC time.
0859: * This offset can be used to undo adding the offset obtained by getOffset.
0860: *
0861: * <pre>
0862: * millisLocal == millisUTC + getOffset(millisUTC)
0863: * millisUTC == millisLocal - getOffsetFromLocal(millisLocal)
0864: * </pre>
0865: *
0866: * NOTE: After calculating millisLocal, some error may be introduced. At
0867: * offset transitions (due to DST or other historical changes), ranges of
0868: * local times may map to different UTC times.
0869: * <p>
0870: * This method will return an offset suitable for calculating an instant
0871: * after any DST gap. For example, consider a zone with a cutover
0872: * from 01:00 to 01:59:<br />
0873: * Input: 00:00 Output: 00:00<br />
0874: * Input: 00:30 Output: 00:30<br />
0875: * Input: 01:00 Output: 02:00<br />
0876: * Input: 01:30 Output: 02:30<br />
0877: * Input: 02:00 Output: 02:00<br />
0878: * Input: 02:30 Output: 02:30<br />
0879: * <p>
0880: * NOTE: The behaviour of this method changed in v1.5, with the emphasis
0881: * on returning a consistent result later along the time-line (shown above).
0882: *
0883: * @param instantLocal the millisecond instant, relative to this time zone, to
0884: * get the offset for
0885: * @return the millisecond offset to subtract from local time to get UTC time
0886: */
0887: public int getOffsetFromLocal(long instantLocal) {
0888: // get the offset at instantLocal (first estimate)
0889: int offsetLocal = getOffset(instantLocal);
0890: // adjust instantLocal using the estimate and recalc the offset
0891: int offsetAdjusted = getOffset(instantLocal - offsetLocal);
0892: // if the offsets differ, we must be near a DST boundary
0893: if (offsetLocal != offsetAdjusted) {
0894: // we need to ensure that time is always after the DST gap
0895: // this happens naturally for positive offsets, but not for negative
0896: if ((offsetLocal - offsetAdjusted) < 0) {
0897: // if we just return offsetAdjusted then the time is pushed
0898: // back before the transition, whereas it should be
0899: // on or after the transition
0900: long nextLocal = nextTransition(instantLocal
0901: - offsetLocal);
0902: long nextAdjusted = nextTransition(instantLocal
0903: - offsetAdjusted);
0904: if (nextLocal != nextAdjusted) {
0905: return offsetLocal;
0906: }
0907: }
0908: }
0909: return offsetAdjusted;
0910: }
0911:
0912: /**
0913: * Converts a standard UTC instant to a local instant with the same
0914: * local time. This conversion is used before performing a calculation
0915: * so that the calculation can be done using a simple local zone.
0916: *
0917: * @param instantUTC the UTC instant to convert to local
0918: * @return the local instant with the same local time
0919: * @throws ArithmeticException if the result overflows a long
0920: * @since 1.5
0921: */
0922: public long convertUTCToLocal(long instantUTC) {
0923: int offset = getOffset(instantUTC);
0924: long instantLocal = instantUTC + offset;
0925: // If there is a sign change, but the two values have the same sign...
0926: if ((instantUTC ^ instantLocal) < 0
0927: && (instantUTC ^ offset) >= 0) {
0928: throw new ArithmeticException(
0929: "Adding time zone offset caused overflow");
0930: }
0931: return instantLocal;
0932: }
0933:
0934: /**
0935: * Converts a local instant to a standard UTC instant with the same
0936: * local time. This conversion is used after performing a calculation
0937: * where the calculation was done using a simple local zone.
0938: *
0939: * @param instantLocal the local instant to convert to UTC
0940: * @param strict whether the conversion should reject non-existent local times
0941: * @return the UTC instant with the same local time,
0942: * @throws ArithmeticException if the result overflows a long
0943: * @throws IllegalArgumentException if the zone has no eqivalent local time
0944: * @since 1.5
0945: */
0946: public long convertLocalToUTC(long instantLocal, boolean strict) {
0947: // get the offset at instantLocal (first estimate)
0948: int offsetLocal = getOffset(instantLocal);
0949: // adjust instantLocal using the estimate and recalc the offset
0950: int offset = getOffset(instantLocal - offsetLocal);
0951: // if the offsets differ, we must be near a DST boundary
0952: if (offsetLocal != offset) {
0953: // if strict then always check if in DST gap
0954: // otherwise only check if zone in Western hemisphere (as the
0955: // value of offset is already correct for Eastern hemisphere)
0956: if (strict || offsetLocal < 0) {
0957: // determine if we are in the DST gap
0958: long nextLocal = nextTransition(instantLocal
0959: - offsetLocal);
0960: long nextAdjusted = nextTransition(instantLocal
0961: - offset);
0962: if (nextLocal != nextAdjusted) {
0963: // yes we are in the DST gap
0964: if (strict) {
0965: // DST gap is not acceptable
0966: throw new IllegalArgumentException(
0967: "Illegal instant due to time zone offset transition: "
0968: + DateTimeFormat
0969: .forPattern(
0970: "yyyy-MM-dd'T'HH:mm:ss.SSS")
0971: .print(
0972: new Instant(
0973: instantLocal))
0974: + " (" + getID() + ")");
0975: } else {
0976: // DST gap is acceptable, but for the Western hemisphere
0977: // the offset is wrong and will result in local times
0978: // before the cutover so use the offsetLocal instead
0979: offset = offsetLocal;
0980: }
0981: }
0982: }
0983: }
0984: // check for overflow
0985: long instantUTC = instantLocal - offset;
0986: // If there is a sign change, but the two values have different signs...
0987: if ((instantLocal ^ instantUTC) < 0
0988: && (instantLocal ^ offset) < 0) {
0989: throw new ArithmeticException(
0990: "Subtracting time zone offset caused overflow");
0991: }
0992: return instantUTC;
0993: }
0994:
0995: /**
0996: * Gets the millisecond instant in another zone keeping the same local time.
0997: * <p>
0998: * The conversion is performed by converting the specified UTC millis to local
0999: * millis in this zone, then converting back to UTC millis in the new zone.
1000: *
1001: * @param newZone the new zone, null means default
1002: * @param oldInstant the UTC millisecond instant to convert
1003: * @return the UTC millisecond instant with the same local time in the new zone
1004: */
1005: public long getMillisKeepLocal(DateTimeZone newZone, long oldInstant) {
1006: if (newZone == null) {
1007: newZone = DateTimeZone.getDefault();
1008: }
1009: if (newZone == this ) {
1010: return oldInstant;
1011: }
1012: long instantLocal = oldInstant + getOffset(oldInstant);
1013: return instantLocal - newZone.getOffsetFromLocal(instantLocal);
1014: }
1015:
1016: /**
1017: * Returns true if this time zone has no transitions.
1018: *
1019: * @return true if no transitions
1020: */
1021: public abstract boolean isFixed();
1022:
1023: /**
1024: * Advances the given instant to where the time zone offset or name changes.
1025: * If the instant returned is exactly the same as passed in, then
1026: * no changes occur after the given instant.
1027: *
1028: * @param instant milliseconds from 1970-01-01T00:00:00Z
1029: * @return milliseconds from 1970-01-01T00:00:00Z
1030: */
1031: public abstract long nextTransition(long instant);
1032:
1033: /**
1034: * Retreats the given instant to where the time zone offset or name changes.
1035: * If the instant returned is exactly the same as passed in, then
1036: * no changes occur before the given instant.
1037: *
1038: * @param instant milliseconds from 1970-01-01T00:00:00Z
1039: * @return milliseconds from 1970-01-01T00:00:00Z
1040: */
1041: public abstract long previousTransition(long instant);
1042:
1043: // Basic methods
1044: //--------------------------------------------------------------------
1045:
1046: /**
1047: * Get the datetime zone as a {@link java.util.TimeZone}.
1048: *
1049: * @return the closest matching TimeZone object
1050: */
1051: public java.util.TimeZone toTimeZone() {
1052: return java.util.TimeZone.getTimeZone(iID);
1053: }
1054:
1055: /**
1056: * Compare this datetime zone with another.
1057: *
1058: * @param object the object to compare with
1059: * @return true if equal, based on the ID and all internal rules
1060: */
1061: public abstract boolean equals(Object object);
1062:
1063: /**
1064: * Gets a hash code compatable with equals.
1065: *
1066: * @return suitable hashcode
1067: */
1068: public int hashCode() {
1069: return 57 + getID().hashCode();
1070: }
1071:
1072: /**
1073: * Gets the datetime zone as a string, which is simply its ID.
1074: * @return the id of the zone
1075: */
1076: public String toString() {
1077: return getID();
1078: }
1079:
1080: /**
1081: * By default, when DateTimeZones are serialized, only a "stub" object
1082: * referring to the id is written out. When the stub is read in, it
1083: * replaces itself with a DateTimeZone object.
1084: * @return a stub object to go in the stream
1085: */
1086: protected Object writeReplace() throws ObjectStreamException {
1087: return new Stub(iID);
1088: }
1089:
1090: /**
1091: * Used to serialize DateTimeZones by id.
1092: */
1093: private static final class Stub implements Serializable {
1094: /** Serialization lock. */
1095: private static final long serialVersionUID = -6471952376487863581L;
1096: /** The ID of the zone. */
1097: private transient String iID;
1098:
1099: /**
1100: * Constructor.
1101: * @param id the id of the zone
1102: */
1103: Stub(String id) {
1104: iID = id;
1105: }
1106:
1107: private void writeObject(ObjectOutputStream out)
1108: throws IOException {
1109: out.writeUTF(iID);
1110: }
1111:
1112: private void readObject(ObjectInputStream in)
1113: throws IOException {
1114: iID = in.readUTF();
1115: }
1116:
1117: private Object readResolve() throws ObjectStreamException {
1118: return forID(iID);
1119: }
1120: }
1121: }
|