I'm going to share what I've learned from building my own calendar
application. Some of this will seem obvious or common-knowledge, but I
want to make sure the subject is covered thoroughly and I also want to
make sure that everyone who is interested in the subject will be on the
same page.
My goal: to develop what I call a "movable calendar" - a set of date/time
services that will operate correctly no matter what time zone or locale
the user is in.
*** The concept -
OFBiz uses java.sql.Timestamp for storing/retrieving date/time values.
Timestamp is a long data type that contains milliseconds elapsed since Jan
1, 1970. The time is referenced to UTC. A particular moment in time that
is represented by a Timestamp value can be thought of as a constant, or
that the
value is immutable. The user's timezone or locale does not alter the
Timestamp's value.
In order for a user to interact with a Timezone value in a way that
reflects their timezone and locale, the Timezone value must be converted
to a user-friendly data type - typically a String. Java supplies a good
set of classes that manage Timestamp-to-String and String-to-Timestamp
conversions.
Those classes do all the work of basing the conversions on timezones and
locales - the programmer doesn't have to bother with any of those details.
As long as the services that handle date/time values always utilize the
user's timezone and locale in conversions, then the goal will be achieved
- a calendar that moves with the user. It helps to look at it this way:
Entity --> Conversion to String using the user's time zone and locale -->
UI
UI --> Conversion to Timestamp using the user's time zone and locale -->
Entity
It is very important to understand that all conversions must be run
through the same services, otherwise the date/time value presented to the
user (or stored in an entity) will be unpredictable.
*** The implementation -
I created two conversion methods:
public static String timeStampToString(Timestamp stamp, TimeZone tz,
Locale locale);
public static Timestamp stringToTimeStamp(String dateTimeString,
TimeZone tz, Locale locale);
and I made sure that all date/time data in my calendar application is
routed through those two methods. The implementation was successful. A
date/time value I create in one timezone appears in the correct time when
I switch timezones. In addition, since the conversions utilize the user's
locale, the
date/time values are displayed/edited in the format I expect to see them
(dd mmm yyyy if I'm in Europe).
*** Building out the basic implementation -
When I first mentioned I was working on a calendar application, a few
developers suggested I just use the WorkEffort component. I took a close
look at it and decided against that approach because it had one major flaw
- all date/time values are based on the server's timezone and locale. So,
any
calendar based on WorkEffort will not be movable. Plus, it would be much
faster for me to develop one from scratch instead of reverse engineering
WorkEffort and fixing its flaws. I'll describe my approach here in order
to share the lessons I learned - I am NOT trying to push my calendar
application
on the community. My hope is the lessons I learned can be applied to the
existing code base.
I looked at the various existing entities and came up with a very
fundamental data structure that many of them share. The field names that
the existing entities use may be different, but they all have the same
purpose. I called the basic structure a Timed Event:
<entity entity-name="TimedEvent" package-name="org.ofbiz.calendar"
title="Timed Event">
<field name="eventId" type="id-ne"></field>
<field name="parentEventId" type="id"></field>
<field name="eventType" type="id-ne"></field>
<field name="eventStatus" type="id"></field>
<field name="description" type="description"></field>
<field name="startDateTime" type="date-time"></field>
<field name="endDateTime" type="date-time"></field>
<field name="recurringId" type="id"></field>
<prim-key field="eventId"/>
<relation type="one" rel-entity-name="Enumeration">
<key-map field-name="eventType" rel-field-name="enumId"/>
</relation>
</entity>
Then I built a set of CRUD services around it. All of the services accept
a time zone ID and locale as parameters and they run all conversions
through the two conversion methods I created. Here's where I ran into my
first problem - I took the shortcut approach in my services.xml file:
<service name="createTimedEvent" engine="java"
location="org.ofbiz.calendar.EventCalendarServices"
invoke="createTimedEvent">
<description>Create a Timed Event</description>
<auto-attributes entity-name="TimedEvent" include="nonpk" mode="IN"
optional="true"/>
<attribute name="tzId" type="String" mode="IN" optional="true"/>
<attribute name="eventId" type="String" mode="OUT" optional="true"/>
</service>
When the createTimedEvent service is invoked with String data types,
startDateTime and endDateTime arrive in the Java code as Timestamp data
types. How nice - the service engine converted the strings to Timestamps
for me. Normally I would be appreciative, but the problem is, the service
engine did
the conversions based upon the server's timezone and locale - not the
user's. That is not acceptable. So, I had to remove the auto-attributes
entry and list the startDateTime and endDateTime attributes as Strings.
Next, I created my Calendar Event entity by specifying a few additional
event properties and relating that entity to TimedEvent.
On to the user interface. I prefer to develop with the bsh/ftl combo -
it's flexible and intuitive for me. That's where I ran into my next
problem. Ftl transforms like ${timedEvent.startDateTime} - if
startDateTime is a Timestamp data type - are converted to Strings by
Freemarker using the server's
timezone and locale. So, every date/time value that appears on the screen
had to be converted to Strings BEFORE hitting the template. One Freemarker
transform is okay to use: ${timedEvent.startDateTime?string("#")} - which
converts the Timestamp object to its milliseconds value. As I mentioned
previously, that value is a constant that isn't altered by timezone or
locale.
Having jumped those hurdles, I had a working "movable" calendar. If I
switch timezones, the calendar events move to the correct slots. If I
switch to a French locale, the week starts on Monday. Date/time values are
displayed and editied in locale-appropriate formats. It all works
flawlessly.
*** Applying this information to OFBiz -
There have been a number of Jira issues submitted that revolve around
date/time issues. My hope is those issues can be revisited with this
information and possibly find solutions to the problems.
The UtilDateTime class was created before the new Calendar and Timezone
Java classes, so many of its methods are based on the server's timezone
and locale. An updated UtilDateTime class has been submitted to Jira
(https://issues.apache.org/jira/browse/OFBIZ-2) and I am in the process of
testing it now.
By the way, I am in no way suggesting that EVERY date/time piece of data
needs to be converted. Some date/time values make better sense if they are
referenced to the server's timezone and locale. Each application needs to
be evaluated to determine which style of date/time data to use - the
server's
or the user's.
-Adrian