Wednesday, June 28, 2006

Trying out Dojo AJAX

As I have alluded to sometime back, I wanted to try out Dojo. Boy, was Dojo hard. I wanted to have my users to be able to edit fragments of a bigger page inline. The lack of documentation was almost debilitating. I almost gave up and went for alternative richtext editor like FCKeditor or TinyMCE. You really need to be patient enough to sift through the javascript for the demo and Dojo widgets. I must admit, Dojo javascript code is highly structured and readable. Also, don't forget to subscribe to the very active dojo-interest mailing list.

I like Dojo richtext editor (dojo.widget.Editor2) mainly because of its "grow-as-you-write" feature. I believe the scrollbar in the browser window is enough for the user. In addition, it doesn't limit what HTML element we can make editable. Furthermore, while the javascript API to the editor has not completely matured yet as compared to, say, TinyMCE API, it was not too bad. Dojo seems to allow for more flexibility on what we can do to its widgets through javascript.

For what I am trying to do, I have a button that is used to turn an innocuous HTML element into becoming editable. Example HTML fragment is given below:

<div style="float:left">
<button type="button" item="h1"
onclick="doEdit(this)">edit</button>
</div>
<div id="h1"><h1>Inlinely editable, yum!</h1></div>

<div style="float:left">
<button type="button" item="p1"
onclick="doEdit(this)">edit</button>
</div>
<div id="p1"><p>This is also editable</p></div>

<div style="float:left">
<button type="button" item="p2"
onclick="doEdit(this)">edit</button>
</div>
<div id="p2"><p>And this too.</p></div>

<div style="float:left">
<button type="button" item="p3"
onclick="doEdit(this)">edit</button>
</div>
<div id="p3"><p>And this too, yeah.</p></div>

Each button has a custom item attribute to point to the element to be edited (the <div> next to it). When the button is clicked the doEdit is called, its item attribute is retrieved and the element it refers to is converted into a richtext editor. Note that if the editor has already been instantiated before, it is kept in variable last. But first, we check whether we need to save the content from the last editor:

dojo.require("dojo.widget.Editor2");

var last;
function doEdit(button) {
var save, lastbutton;
if (last) {
if (changed(last)) save = confirm("Save?");
last.close(save);
if (save) {
// so call server to update. Use last.getEditorContent().
}
lastbutton = last.button;
lastbutton.innerHTML = "edit";
last.destroy();
last = null;
}

Since the Dojo richtext editor does not have a method to check whether its content has changed, a separate changed function has been written for that. If the content has changed, the user is asked whether or not she wants to save it. Ideally, the server will be called using some AJAX RPC to save it. Finally, the editor is transformed back to normal HTML element by closing and destroying it.

Now if the button is the same button used to create the editor, we are done. If not, we need to create the editor:

if (lastbutton != button) {
var id = button.getAttribute("item");
var t = document.getElementById(id);
last = dojo.widget.createWidget(
"Editor2",
{
shareToolbar: false,
toolbarAlwaysVisible: false,
focusOnLoad: true,
closeOnSave: true
}, t);
last.button = button;
button.innerHTML = "close";
dojo.event.connect(last, "onLoad", onEdLoad);
}
}

Note that a custom property (button) has been added to the editor object to remember which button used to create it. And finally the onLoad event for the editor is connected to the onEdLoad. This is important if we have some initialization we need to do on the editor once it has fully been created. The editor seems to be created asynchronously.

That is it. Below is the complete HTML file. Put it in your Dojo installation directory (where dojo.js) is:

<html>
<head>
<title>Dojo is cool</title>
<script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
dojo.require("dojo.widget.Editor2");

var last;
function doEdit(button) {
var save, lastbutton;
if (last) {
if (changed(last)) save = confirm("Save?");
last.close(save);
if (save) {
// so call server to update. Use
// last.getEditorContent().
}
lastbutton = last.button;
lastbutton.innerHTML = "edit";
last.destroy();
last = null;
}
if (lastbutton != button) {
var id = button.getAttribute("item");
var t = document.getElementById(id);
last = dojo.widget.createWidget(
"Editor2",
{
shareToolbar: false,
toolbarAlwaysVisible: false,
focusOnLoad: true,
closeOnSave: true
}, t);
last.button = button;
button.innerHTML = "close";
dojo.event.connect(last, "onLoad", onEdLoad);
}
}

function onEdLoad() {
// now editor is completely loaded. Do whatever
// we want when editor is loaded here.
}

function changed(ed) {
return ed.editNode.innerHTML
!= last.savedContent.innerHTML;
}

</script>
</head>
<body>

<div style="float:left">
<button type="button" item="h1"
onclick="doEdit(this)">edit</button>
</div>
<div id="h1">
<h1>Inlinely editable, yum!</h1>
</div>

<div style="float:left">
<button type="button" item="p1"
onclick="doEdit(this)">edit</button>
</div>
<div id="p1">
<p>This is also editable</p>
</div>

<div style="float:left">
<button type="button" item="p2"
onclick="doEdit(this)">edit</button>
</div>
<div id="p2"><p>And this too.</p></div>

<div style="float:left">
<button type="button" item="p3"
onclick="doEdit(this)">edit</button>
</div>
<div id="p3"><p>And this too, yeah.</p></div>

</body>
</html>

Sunday, June 18, 2006

Scheduling durable, non-volatile job in OpenSymphony Quartz

Previously, I managed to get Quartz to run when Tomcat runs. I am also using Postgresql as the jobstore. Now I need to schedule a job to run in Quartz. Since I want the job to persist between Quartz runs, I have to tag the job with the empty interface StatefulJob:
package my.com.petah.common.util.cron;

import org.apache.commons.logging.*;
import org.quartz.*;
import java.text.*;
import java.util.*;
import java.util.Calendar;

public class ConsolidateAttendance
implements Job, StatefulJob {

private static Log log
= LogFactory.getLog(ConsolidateAttendance.class);

private static DateFormat df
= new SimpleDateFormat("yyyymmdd HH:mm");

private static Date parse(String val) {
if (val == null) return null;
try { return df.parse(val); }
catch (ParseException e) { return null; }
}

public void execute(JobExecutionContext ctx)
throws JobExecutionException {
JobDetail job = ctx.getJobDetail();
JobDataMap props = job.getJobDataMap();
Date last = parse(props.getString("last-run"));
Calendar cal = Calendar.getInstance();
cal.clear(Calendar.SECOND);
cal.clear(Calendar.MILLISECOND);
Date curr = cal.getTime();
if (log.isInfoEnabled()) {
log.info(
job.getFullName()
+ " executes. Last run:"
+ last + " current:" + curr);
}
doExecute(ctx, last, curr);
props.put("last-run", df.format(curr));
}

public void doExecute(
JobExecutionContext ctx, Date last, Date curr) {

// job logic here
}
}
Note that I am using the Jakarta commons logging. I also need to convert the time when the job ran previously into a String because, through configuration parameter org.quartz.jobStore.useProperties , I have chosen to store job property values as Strings to avoid class versioning problems. The main logic of the job should be placed in the doExecuteMethod.

In creating the job and scheduling it, it seems reasonable to do it outside of the webapp in Tomcat. This can be done through RMI. The additional parameters needed in quartz.properties for this purpose is given below:

org.quartz.scheduler.rmi.export: true
org.quartz.scheduler.rmi.registryHost: localhost
org.quartz.scheduler.rmi.registryPort: 1099
org.quartz.scheduler.rmi.createRegistry: true

Now to get the RMI client program to work properly requires a bit of experimentation because Quartz documentation is a bit inadequate. For example, in all of the examples, Scheduler class has been used. But with RMI, we actually need to use QuartzScheduler_Stub class instead. This class is method-for-method equivalent to QuartzScheduler class. Methods signatures between Scheduler and QuartzScheduler are different because the latter typically would require a mysterious SchedulingContext as its first parameter. We also need to bind with QuartzScheduler_Stub with name "QuartzScheduler_$_NON_CLUSTERED" in the RMI registry. See the start of the main function below:

package my.com.petah.common.util.cron;

import org.apache.commons.logging.*;
import org.quartz.core.*;
import org.quartz.*;
import java.rmi.registry.*;

public class CronInit {

static Log log
= LogFactory.getLog(CronInit.class);

public static void main(String args[])
throws Exception {
Registry reg = LocateRegistry.getRegistry();
if (log.isInfoEnabled()) {
String n[] = reg.list();
for (int i = 0; i < n.length; ++i)
log.info("rmi registry object:" + n[i]);
}
SchedulingContext ctx = new SchedulingContext();
ctx.setInstanceId("petah.scheduler");
QuartzScheduler_Stub sched
= (QuartzScheduler_Stub) reg.lookup(
"QuartzScheduler_$_NON_CLUSTERED");
if (log.isInfoEnabled()) {
log.info(
"scheduler retrieved:"
+ sched.getSchedulerName());
String g[] = sched.getJobGroupNames(ctx);
for (int i = 0; i < g.length; ++i) {
String n[] = sched.getJobNames(ctx, g[i]);
for (int j = 0; j < n.length; ++j)
log.info("defined job: " + g[i] + "." + n[j]);
}
}

Note that you can give any string as the instance id for the mysterious SchedulingContext object. Once the scheduler has been successfully retrieved, the code prints the jobs already defined in the scheduler.

Next, I need to create the job. I want the job to still exist even when it is not scheduled, i.e., it does not have a trigger attached to it. The job is said to be durable. Also, I want the job to persist between Quartz runs. It is said to be non-volatile in this case. However, before I create a job, I check whether the job is already there.


JobDetail job = sched.getJobDetail(
ctx, "consolidate-attendance",
"daily-attendance-group");
if (job != null) {
if (log.isInfoEnabled())
log.info(job.getFullName()
+ " already defined.");
} else {
job = new JobDetail("consolidate-attendance",
"daily-attendance-group", ConsolidateAttendance.class,
/* volatile */ false, /* durable */ true,
/* recover */ false);
sched.addJob(ctx, job, false);
}

The JobDetail object created above ties the ConsolidateAttendance class defined earlier with a job with name consolidate-attendance in the group daily-attendance-group. Actually, you do not have to explicitly add the job the scheduler. You could have schedule it with the trigger to be defined shortly. But I want to test whether I could add a "dangling" job and then schedule it later.

Next, the trigger. Again, I check whether trigger by the given name is there already before creating it.


Trigger trig = sched.getTrigger(
ctx, "every-some-minutes",
"debug-trigger-group");
if (trig != null) {
if (log.isInfoEnabled())
log.info(trig.getFullName()
+ " already defined.");
} else {
trig = new CronTrigger(
"every-some-minutes",
"debug-trigger-group", "0 0/5 * * * ?");
}

Note that a trigger has a name and a group too. In the above, the name is "every-some-minutes" and the group is "debug-trigger-group". The cron expression "0 0/5 * * * ?" says that the trigger will fire every 5 minutes starting from the zeroth minute of the hour. I was hoping I could add a trigger to the scheduler similar to what is possible with a job but there is no way to do this.

Now the job could be scheduled by attaching it to the trigger. I first check whether the trigger has already got a job tied to it already.


String jobGroup = trig.getJobGroup();
String jobName = trig.getJobName();
if (jobGroup != null && jobName != null) {
if (log.isInfoEnabled()) {
log.info("trigger " + trig.getFullName()
+ " already attached to job "
+ jobGroup + "." + jobName);
}
} else {
trig.setJobGroup("daily-attendance-group");
trig.setJobName("consolidate-attendance");
sched.scheduleJob(ctx, trig);
if (log.isInfoEnabled()) {
log.info("job " + job.getFullName()
+ " scheduled with trigger "
+ trig.getFullName());
}
}
}
}

Note that there are two versions of the method scheduleJob. You cannot use the version that accepts both a job and a trigger because internally the scheduler would try to add the job to itself. Since I have already done this, the method would fail. You need to tie the job to the trigger through methods in the Trigger class and use the scheduleJob that only accepts a trigger.

That is it. Running the CronInit class above on the same machine where the webapp is running would create the job and the schedule it with the trigger. Below is a sample of the log from the scheduler:

...
Connected to server
INFO: Server startup in 5315 ms
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:00:00 MYT 2006
current:Sun Jun 18 22:05:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:05:00 MYT 2006
current:Sun Jun 18 22:10:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:10:00 MYT 2006
current:Sun Jun 18 22:15:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:15:00 MYT 2006
current:Sun Jun 18 22:20:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:20:00 MYT 2006
current:Sun Jun 18 22:25:00 MYT 2006
...

Sun putting its weight on Dojo AJAX

According to TheServerSide, Sun is putting its AJAX bet on Dojo. When I started looking, around August last year, to include AJAX technology into the school management system pet project my wife and I were working on, I decided to use DWR because it seemed easier to work with. Even now, it has resisted trying to be the "AJAX everything including the kitchen sink" by keeping its focus on AJAX remoting alone. With a lot of web sites showing improved usability due to AJAX widgets, a limited AJAX library might eventually lose out to one that integrates AJAX remoting seamlessly with AJAX widgets. We, for one, would love to have an AJAX calendar or timetable for the school management system project. I will definitely give Dojo a serious second look.

Tuesday, June 13, 2006

OpenSymphony Quartz on Tomcat and Postgresql

It is always a bad thing to execute long running processes directly from a web page. It is worse when you need to tell your user to click a specific button at a specific time of day before she could see her consolidated reports.

To run processes asynchronously, OpenSymphony Quartz comes to the rescue. When I was a Java "technical lead" a while back, I had asked one of my junior developers to use it. But I did not really try it myself.

It is time now for my wife and I to use it for the school management system we are developing. We need to generate attendance records from students checking in and out of the school. It needs to run every morning some time after students have come and every afternoon after they have left the school. And we need Quartz for some other backend processes too.

As always, OpenSymphony documentation is all over the place and it took some time to gather the bits and pieces to configure Quartz to run on Tomcat and the Postgresql database. The first thing was to create the database tables as given in Quartz distribution (docs/dbTables/tables_postgres.sql). And per the FAQ, I created these indexes for performance sake:

create index idx_qrtz_t_next_fire_time
on qrtz_triggers(NEXT_FIRE_TIME);
create index idx_qrtz_t_state
on qrtz_triggers(TRIGGER_STATE);
create index idx_qrtz_t_nf_st
on qrtz_triggers(TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_ft_trig_name
on qrtz_fired_triggers(TRIGGER_NAME);
create index idx_qrtz_ft_trig_group
on qrtz_fired_triggers(TRIGGER_GROUP);
create index idx_qrtz_ft_trig_n_g
on qrtz_fired_triggers(TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_trig_inst_name
on qrtz_fired_triggers(INSTANCE_NAME);
create index idx_qrtz_ft_job_name
on qrtz_fired_triggers(JOB_NAME);
create index idx_qrtz_ft_job_group
on qrtz_fired_triggers(JOB_GROUP);

Incidentally, tables_postgres.sql has a typo in it. If you simply copy the driverDelegateClass property setting from the comment into your quartz.properties you will be in for a surprise (Couldn't load delegate class).

Next, I copied quartz-all-1.5.2.jar (I am using version 1.5.2) and all jars from lib/core that I didn't already have into the webapp WEB-INF/lib.

After a few attempts, I ended up with these properties in my quartz.properties (in WEB-INF/classes):

org.quartz.dataSource.petahdb.jndiURL = java:comp/env/jdbc/petahdb

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate

org.quartz.jobStore.dataSource = petahdb
org.quartz.jobStore.useProperties = true

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 4

The datasource URL jdbc/petahdb refers to the datasource I have already defined in Tomcat context.xml and referenced in web.xml. Note that the word petahdb in "org.quartz.dataSource.petahdb.jndiURL" is an arbitrary name given to the datasource and it is used as the value for the org.quartz.jobStore.dataSource property.

To get Quartz running when Tomcat runs, as suggested in Quartz documentation, I simply put the fragment below in web.xml:

<servlet>
<servlet-name>QuartzInitializer</servlet-name>
<servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class>
<init-param>
<param-name>shutdown-on-unload</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>

This was what I got when I finally managed to get Tomcat to run without errors:

INFO: Quartz Scheduler v.1.5.2 created.
INFO: Using thread monitor-based data access locking (synchronization).
INFO: Removed 0 Volatile Trigger(s).
INFO: Removed 0 Volatile Job(s).
INFO: JobStoreTX initialized.
INFO: Quartz scheduler 'QuartzScheduler' initialized from default
resource file in Quartz package: 'quartz.properties'
INFO: Quartz scheduler version: 1.5.2
INFO: Freed 0 triggers from 'acquired' / 'blocked' state.
INFO: Recovering 0 jobs that were in-progress at the time of the
last shut-down.
INFO: Recovery complete.
INFO: Removed 0 'complete' triggers.
INFO: Removed 0 stale fired job entries.
INFO: Scheduler QuartzScheduler_$_NON_CLUSTERED started.
INFO: QuartzInitializer: Scheduler has been started...
INFO: QuartzInitializer: Storing the Quartz Scheduler Factory in
the servlet context at key:
org.quartz.impl.StdSchedulerFactory.KEY