The sequence diagram below details the participants on both the client and server needed to provide an AJAX progress bar. The details are explained in further detail in the remainder of this document.
Tracking the progress of a server-side operation by a client requires two operations which include:
Now let us review each of these operations in more detail.
Negotiate a Task Id
An HTML form button onClick
event calls a JavaScript
function which in turn will create an
XMLHttpRequest
object and the URL and callback function as
may be seen in the JavaScript code snippet below.
function submitTask() {
var url = "task?action=startTask";
var bttn = window.document.getElementById("taskbutton");
bttn.disabled = true;
initRequest(url);
// set callback function
req.onreadystatechange = processInitialRequest;
req.send(null);
}
function initRequest(url) {
if (window.XMLHttpRequest) {
req = new XMLHttpRequest();
} else if (window.ActiveXObject) {
isIE = true;
req = new ActiveXObject("Microsoft.XMLHTTP");
}
req.open("GET", url, true);
}
The url
in the code above includes a URL parameter action
which is set to "startTask." When the req.send(null);
statement is executed the request is sent to the URL "task" The client
continues processing page events because the interaction is configured
to be an asynchronous HTTP GET with the req.open("GET", url,
true);
statement.
On the server, a servlet TaskServlet
is mapped to the
URL "task." When the TaskServlet receive a request containing a
parameter "action" with the value of "startTask," a new
Task
is created as can be seen in the code snippet below.
if ((action != null) && action.equals("startTask")) {
// this task will take 15 cycles of the counter to complete
Task t = new Task(counter, 15);
taskCounter++;
tasks.put(taskCounter + "", t);
// return intial content
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
PrintWriter pw = response.getWriter();
pw.write("<message&qt;" + taskCounter + "</message>");
pw.flush();
}
The snippet of code from the TaskServlet
above shows the TaskServlet
creating a new Task
object and placing it in a java.util.HashMap
with a unique identifier. For this example, an int
taskCounter
is used to
identify each Task
.
The taskCounter
increments each time a new Task
is created. In cases where the progress will be tracked in an
environment outside the firewall, more unique identifiers may be
necessary to prevent nefarious user from tracking operations they did
not initiate.
The taskCounter
is the unique id that is returned to
the XMLHttpRequest
in an XML document. This id is later
used to track the progress of the Task
when the client
polls the TaskServlet
. In this example, the TaskServlet
has an internal timer running on a thread which sleeps for 2 seconds
and increments the variable counter
. To simulate a
server-side operation this example sets the Task
duration
be 15 increments of the counter
(30 seconds total).
On the client, the response from the TaskServlet
is
received by the XMLHttpRequest
object which calls the processIntialRequest()
callback function shown below.
// callback function for intial request to schedule a task
function processInitialRequest() {
if (req.readyState == 4) {
if (req.status == 200) {
var item = req.responseXML.getElementsByTagName("message")[0];
var message = item.firstChild.nodeValue;
// the initial requests gets the targetId
targetId = message;
messageHash = 0;
window.status = "";
createProgressBar();
showProgress(0);
}
var idiv = window.document.getElementById("task_id");
idiv.innerHTML = "Task ID=" + targetId;
// do the initial poll in 2 seconds
setTimeout("pollTaskmaster()", 2000);
}
}
This processInitialRequest()
function extracts the task
id from the XML document and assigns it to the variable targetId
.
The function will then initialize the progress bar in the HTML DOM and
and show the initial progress of 0. The statement setTimeout("pollTaskmaster()",
2000);
is then executed to start the polling loop.
Polling Loop
The polling loop polls the TaskServet
using the task id
store in the targetId
variable until the server-side operation is completed (a response of
100 is returned).
function pollTaskmaster() {
var url = "task?messageHash=" + escape(messageHash) + "&targetId=" + targetId;
initRequest(url);
req.onreadystatechange = processPollRequest;
req.send(null);
}
The pollTaskmaster()
function configures the
XMLHttRequest object with a URL and callback function.
The request is sent to the URL "task" which is mapped on the server to
the TaskServlet
. The
following code in the TaskServlet
is responsible for
processing the request.
int percentage = 0;
if (tasks.get(targetId) != null) {
percentage = ((Task)tasks.get(targetId)).getPrecentComplete(counter);
}
if ((messageHash !=null) &&
(Integer.valueOf(messageHash).intValue()) == percentage) {
// nothing has changed
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
PrintWriter pw = response.getWriter();
if (percentage &qt; 100) percentage = 100;
pw.write("" + percentage + " ");
pw.flush();
}
In the code above, the Task
object is looked up using the
unique id (targetId
) provided
in the request. The Task getPrecentComplete
method is
called with the current counter
to calculate the progress
of the operation and the results are returned in an XML document.
Note that if the messageHash
representing the current
percentage of completion being displayed by the client matches the
percentage complete of the operation in the Task
(indicating no change) the TaskServlet
will return an
HTTP response code 304
(No Content
). This
saves time for post processing of the message by the client, time for
message generation by the TaskServlet
, and bandwidth for
transmitting the message.
Upon receiving the response from the TaskServlet
the XMLHttpRequest
object
will call the processPollRequest()
to do post processing
of the request.
function processPollRequest() {
if (req.readyState == 4) {
if (req.status == 200) {
var item = req.responseXML.getElementsByTagName("message")[0];
var message = item.firstChild.nodeValue;
showProgress(message);
messageHash = message;
} else {
window.status = "No Update for " + targetId;
}
if (messageHash < 100) {
setTimeout("pollTaskmaster()", 5000);
} else {
setTimeout("complete()", 2500);
}
}
}
The HTML DOM is updated by showProgress()
function if the
req.status
HTTP
status code is 200
. This function will detect if the
percentage from the server is
complete (100) and stop the polling loop by calling the complete()
function. Otherwise,
the polling will continue to loop by executing the setTimeout("pollTaskmaster()",
5000);
statement following a 5 second delay.