Gadjah.Mada
Weekend warrior of the Java Empire
Saturday, December 29, 2007
NetBeans 6 rocks!
I have always harbored some hope that NetBeans would become a viable contender to Eclipse in the free IDE/Rich Client Platform battle. In the past, the various NetBeans releases could not sustain my interest for too long. NetBeans 6, however, manages to do that for more than a month and I am still at it. I guess that is a good thing.
I am not doing full time Java programming these days. In my previous employment, I did web-based application development. I have always managed to avoid doing GUI development. GUI development tends to scare me. NetBeans 6, with its superb GUI builder (Matisse), however, helps tremendously. Of course, there are some rough edges. For example, alignment of my controls could get seriously messed up. But after learning on my own to liberally enclose controls in panels (select controls, right click to open context menu, select Enclose In > Panel), things become under control.
One small caveat. I have not tried building any GUI application on Eclipse IDE or IntelliJ IDEA although I am actively using Eclipse for embedded C development and I have used evaluation version of IDEA on and off for web development. But with such a solid package in NetBeans 6, should I even try them?
Kudos NetBeans. You've got my attention.
Wednesday, June 28, 2006
Trying out Dojo AJAX
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
StatefulJob
:package my.com.petah.common.util.cron;Note that I am using the Jakarta commons logging. I also need to convert the time when the job ran previously into a
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
}
}
String
because, through configuration parameter org.quartz.jobStore.useProperties
, I have chosen to store job property values as String
s 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
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