/* * Copyright (C) 2011 Citrix Systems, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.cloud.bridge.util; import java.text.SimpleDateFormat; import java.text.DateFormat; import java.text.FieldPosition; import java.text.ParsePosition; import java.util.Date; import java.util.Calendar; import java.util.TimeZone; /** * @author John Zucker * Format and parse a date string which is expected to be in ISO 8601 DateTimeFormat especially for * use in XML documents. * An example is for use with GMTDateTimeUserType to provide parsing of DateTime format strings into * accurate Java Date representations based on UTC. * The purpose of this class is to allow the creation of accurate date time representations following * the ISO 8601 format YYYY-MM-DDThh:MM:ss * using the letter "T" as the date/time separator * This representation may be immediately followed by a "Z" (Zulu i.e. at zero offset from GMT) to indicate UTC * or, otherwise, to a specific time zone. If a time zone (tz) is encoded then this is held as the difference * between the local time in the tz and UCT, expressed as a positive(+) or negative(-) offset (hhMM) appended * to the format. * The default case holds no tz information and assumes that a date time representation referenced to Zulu * (i.e. zero offset from GMT) is required. When formatting an existing Date transform it into the Zulu timezone * so that it is explicitly at GMT with zero offset. This provides the default representation for the encoding * of AWS datetime values. * For testing, it may be useful to note that, as at 2012, a city whose time is always in the Zulu timezone is * Reykjavik, Iceland. * The parsing and formatting methods provided by this class are GMT-referenced and locale insensitive. */ public class ISO8601SimpleDateTimeFormat extends SimpleDateFormat { private static final long serialVersionUID = 7388260211953189670L; protected static TimeZone defaultTimeZone = TimeZone.getTimeZone("Z"); /** * Construct a new ISO8601DateTimeFormat using the default time zone. * Initializes calendar inherited from java.text.DateFormat.calendar * */ public ISO8601SimpleDateTimeFormat() { setCalendar(Calendar.getInstance(defaultTimeZone)); } /** * Construct a new ISO8601DateTimeFormat using a specific time zone. * Initializes calendar inherited from java.text.DateFormat.calendar * @param tz The time zone used to format and parse the date. */ public ISO8601SimpleDateTimeFormat(TimeZone tz) { setCalendar(Calendar.getInstance(tz)); } /** * The abstract superclass DateFormat has two business methods to override. These are * public StringBuffer format(Date arg0, StringBuffer arg1, FieldPosition arg2) * public Date parse(String arg0, ParsePosition arg1) */ /** * @see DateFormat#format(Date, StringBuffer, FieldPosition) */ @Override public StringBuffer format(Date date, StringBuffer stringBuffer, FieldPosition fieldPosition) { calendar.setTime(date); calendar.setTimeZone(defaultTimeZone); writeYYYYMM(stringBuffer); stringBuffer.append('T'); writehhMMss(stringBuffer); stringBuffer.append(".000Z"); return stringBuffer; } /* @see DateFormat#parse(String, ParsePosition) * Assigns the values of YYYY-MM-DDThh:MM:ss fields between the delimiters of dateString * or a near approximation using the superclass SimpleDateFormat if not formatted exactly as ISO8601 */ @Override public Date parse(String dateString, ParsePosition pos) { ParsePosition startpos = pos; int p = pos.getIndex(); // Assign value of YYYY try { int YYYY = Integer.valueOf(dateString.substring(p, p + 4)).intValue(); p += 4; if (dateString.charAt(p) != '-') { throw new IllegalArgumentException(); } p++; // Assign value of MM int MM = Integer.valueOf(dateString.substring(p, p + 2)).intValue() - 1; p += 2; if (dateString.charAt(p) != '-') { throw new IllegalArgumentException(); } p++; // Asign value of dd int DD = Integer.valueOf(dateString.substring(p, p + 2)).intValue(); p += 2; if (dateString.charAt(p) != 'T') { throw new IllegalArgumentException(); } p++; // Assign value of hh int hh = Integer.valueOf(dateString.substring(p, p + 2)).intValue(); p += 2; if (dateString.charAt(p) != ':') { throw new IllegalArgumentException(); } p++; // Assign value of mm int mm = Integer.valueOf(dateString.substring(p, p + 2)).intValue(); p += 2; if (dateString.charAt(p) != ':') { throw new IllegalArgumentException(); } p++; // Assign value of ss int ss = 0; // if (p < dateString.length() && dateString.charAt(p) == ':') { // Allow exactly two ss digits after final : delimiter ss = Integer.valueOf(dateString.substring(p, p + 2)).intValue(); p += 2; // Set calendar inherited from java.text.DateFormat.calendar calendar.set(YYYY, MM, DD, hh, mm, ss); calendar.set(Calendar.MILLISECOND, 0); // Since java.util.Date holds none, zeroize milliseconds // process appended timezone if any or Z otherwise p = parseTZ(p, dateString); } catch (IllegalArgumentException ex) { super.setTimeZone(TimeZone.getTimeZone("GMT")); super.applyPattern("yyyy-MM-dd HH:mm:ss"); return super.parse(dateString, startpos); } catch (Exception ex) { super.setTimeZone(TimeZone.getTimeZone("GMT")); return super.parse(dateString, startpos); // default pattern } finally { pos.setIndex(p); } // Return the Calendar instance's Date representation of its value return calendar.getTime(); } /** * Write the time zone string. Remember that in the default TimeZone there is no offset and the * convention to supply the TimeZone string constant "Z" is applicable. As an optimization there * is no need to call this method where the default TimeZone has been set. * @param stringBuffer The buffer to append the time zone. */ protected final void writeTZ(StringBuffer stringBuffer) { int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); if (offset == 0) { stringBuffer.append('Z'); } else { int offsetHour = offset / 3600000; int offsetMin = (offset % 3600000) / 60000; if (offset >= 0) { stringBuffer.append('+'); } else { stringBuffer.append('-'); offsetHour = 0 - offsetHour; offsetMin = 0 - offsetMin; } appendInt(stringBuffer, offsetHour, 2); stringBuffer.append(':'); appendInt(stringBuffer, offsetMin, 2); } } /** * Write hour, minutes, and seconds. * @param stringBuffer The buffer to append the string. */ protected final void writehhMMss(StringBuffer stringBuffer) { int hh = calendar.get(Calendar.HOUR_OF_DAY); appendInt(stringBuffer, hh, 2); stringBuffer.append(':'); int mm = calendar.get(Calendar.MINUTE); appendInt(stringBuffer, mm, 2); stringBuffer.append(':'); int ss = calendar.get(Calendar.SECOND); appendInt(stringBuffer, ss, 2); } /** * Write YYYY, and MMs. * @param stringBuffer The buffer to append the string. */ protected final void writeYYYYMM(StringBuffer stringBuffer) { int YYYY = calendar.get(Calendar.YEAR); appendInt(stringBuffer, YYYY, 4); String MM; switch (calendar.get(Calendar.MONTH)) { case Calendar.JANUARY : MM = "-01-"; break; case Calendar.FEBRUARY : MM = "-02-"; break; case Calendar.MARCH : MM = "-03-"; break; case Calendar.APRIL : MM = "-04-"; break; case Calendar.MAY : MM = "-05-"; break; case Calendar.JUNE : MM = "-06-"; break; case Calendar.JULY : MM = "-07-"; break; case Calendar.AUGUST : MM = "-08-"; break; case Calendar.SEPTEMBER : MM = "-09-"; break; case Calendar.OCTOBER : MM = "-10-"; break; case Calendar.NOVEMBER : MM = "-11-"; break; case Calendar.DECEMBER : MM = "-12-"; break; default : MM = "-NA-"; break; } stringBuffer.append(MM); int DD = calendar.get(Calendar.DAY_OF_MONTH); appendInt(stringBuffer, DD, 2); } /** * Write an integer value with leading zeros. * @param stringBuffer The buffer to append the string. * @param value The value to write. * @param length The length of the string to write. */ protected final void appendInt(StringBuffer stringBuffer, int value, int length) { int len1 = stringBuffer.length(); stringBuffer.append(value); int len2 = stringBuffer.length(); for (int i = len2; i < len1 + length; ++i) { stringBuffer.insert(len1, '0'); } } /** * Parse the time zone. * @param i The position to start parsing. * @param dateString The dateString to parse. * @return The position after parsing has finished. */ protected final int parseTZ(int i, String dateString) { if (i < dateString.length()) { // check and handle the zone/dst offset int offset = 0; if (dateString.charAt(i) == 'Z') { offset = 0; i++; } else { int sign = 1; if (dateString.charAt(i) == '-') { sign = -1; } else if (dateString.charAt(i) != '+') { throw new IllegalArgumentException(); } i++; int offsetHour = Integer.valueOf(dateString.substring(i, i + 2)).intValue(); i += 2; if (dateString.charAt(i) != ':') { throw new IllegalArgumentException(); } i++; int offsetMin = Integer.valueOf(dateString.substring(i, i + 2)).intValue(); i += 2; offset = ((offsetHour * 60) + offsetMin) * 60000 * sign; } int offsetCal = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); calendar.add(Calendar.MILLISECOND, offsetCal - offset); } return i; } @Override public int hashCode() { return (calendar.get(2)+calendar.get(16)); // numberFormat (used by superclass) will not distribute, so use calendar third and penultimate fields // (i.e. dd and ss) instead // in Java 6 (Calendar.FIELD_COUNT-1) returns 16 } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; DateFormat other = (DateFormat) obj; Calendar otherCalendar = other.getCalendar(); for (int i = 0; i < Calendar.FIELD_COUNT; i++) if ( calendar.get(i) != (otherCalendar.get(i)) ) return false; return true; } }