The fun with dates didn’t stop at the last post. This one took longer to track down but I learned alot more from it. I’m working on both sides of an Apache Axis SOAP implementation where the server side system persists events and client applications can query to find events and CRUD events against this server side application. So the server presents an event:
...
public class EventRemote {
private String name;
private String address;
private Calendar eventdate;
...
// getters & setters omitted
}
and methods that allow client applications to search for events and create / retrieve / update and delete them:
...
public class EventManagerRemote {
public void updateEvent(EventRemote event) {
...
}
public void createEvent(EventRemote event) {
...
}
public EventRemote findEventByID(long eventid) {
...
}
// other methods omitted
}
For the last couple weeks the clients and servers had no problems, create, retrieve, update, delete and search all worked as expected. But recently a user requested a change to the UI of the client system which resulted in a change to the way the client application needed to format the eventdate (a Calendar object). In the past the client would retrieve the event and then format the eventdate property using a SimpleDateFormat:
EventRemote event = manager.findEventByID(12);
Calendar eventdate = event.getEventdate();
SimpleDateFormat formatter = new SimpleDateFormat("M/d/yyyy h:m aa");
String strdate = formatter.format(eventdate.getTime());
So if my event was today (June 30, 2005 at 7:30pm), the above snippet would print:
6/30/2005 7:30 PM
The change to the client I mentioned above meant that I needed to access each element of the Calendar object separately, so instead of having one big datetime string, I need to know that the month was June, the date ’30’, the year ‘2005’ and so on. Since the getMonth()/Day()/Year() methods of the Date class have been deprecated since v1.1, I had to use the Calendar class get(int field)
methods. Not a problem, there’s some different things you have to work through in the Calendar class (ie: the month field has values that range from 0 to 11 instead of 1 to 12), but it’s doable:
EventRemote event = manager.findEventByID(12);
Calendar eventdate = event.getEventdate();
int day = eventdate.get(Calendar.DATE);
int year = eventdate.get(Calendar.YEAR);
...
int hour = eventdate.get(Calendar.HOUR);
int minute = eventdate.get(Calendar.MINUTE);
int am_pm = eventdate.get(Calendar.AM_PM);
I printed out the results (again assuming a date of June 30,2005 at 7:30pm) and instead of getting that same date I got this:
6/30/2005 11:30 PM
which was exactly 4 hours ahead of the time I wanted. I dug into Axis source code and saw that the CalendarDeserializer class explicity sets the TimeZone of the returned Calendar object to GMT (to make it UTC right?). Understandably, this causes the HOUR property of the Calendar object to jump four hours ahead (I’m in EST, GMT/UTC is currently 4 hours ahead of EST, it’ll be 5 hours during the winter months, explanation). The workaround (although I’m not sure it’s a good idea), was to set the TimeZone of the returned Calendar object back to EST. This probably doesn’t make alot of sense without seeing some sample code:
Calendar calendar = new GregorianCalendar(2005, 5, 30, 19, 30);
String utcformat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
SimpleDateFormat zulu = new SimpleDateFormat(utcformat);
String serializeddate = zulu.format(calendar.getTime());
// string gets sent along the wire via soap
// CalendarDeserializer parses date string
Calendar deserial = Calendar.getInstance();
Date deserialdate = zulu.parse(serializeddate);
deserial.setTime(deserialdate);
// CalendarDeserializer explicitly sets TZ to GMT
deserial.setTimeZone(TimeZone.getTimeZone("GMT"));
SimpleDateFormat formatter = new SimpleDateFormat(utcformat);
// will print '2005-06-30T19:30:00.000Z'
System.out.println(formatter.format(deserial.getTime()));
// will print '11', should print '7'
System.out.println(deserial.get(Calendar.HOUR));
So I understand that the date gets sent along the wire in UTC format, which strips out timezone information the date was originally encoded with, my question is this: why does the HOUR property of the Calendar object reflect UTC time (ie: 11:30pm) while the formatted time reflects EST time (7:30pm)?
Is there a way to tell axis to not change the timezone while deserializing ?
Changing the timezone doesn’t work always. There are issues related to DST… you’ll see.
A few things to consider if you want to answer the question at the end of the article:
The following time zones are used by the different calendar and formatter objects in the example:
calendar : EST
zulu : EST
formatter : EST
deserial : EST then GMT
Calendar.getTime() returns a Date object which represents an instant in time. You can change the timezone of the calendar to whatevr you want but getTime will always return the same Date value.
The values that the formatter substitutes into the format pattern are calculated using the formatter’s own time zone, which never changes from EST in the example.
To finish up I have three questions of my own to to pose :
Why does the content of the format string end in /’Z’/ and not /Z/?
Why is the format string called “utcformat”?
Did you think that the serilased dateTime sent across the wire (line 5 of the example) was in UTC?
(Hint: I’m really asking the same question 3 times…)
Be careful when playing with SimpleDateFormat, its not thread safe and could cuase you real problems.
My solution is to wrap the implementation and use the wrapper object (With synchronized method calls) to ensure thread safey.
Google it for more info..
“why does the HOUR property of the Calendar object reflect UTC time (ie: 11:30pm) while the formatted time reflects EST time (7:30pm)?”
I think SimpleDateFormat class uses system default locale to format input calendar. Calendar instance _may_ use any locale it wish, but formatter always use own locale setting.
Most likely your clients were run in EST system locale machine.
If you want explicitly use given locale, you must give it to Calendar instance _AND_ formatter instance.