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

Reply via email to