SwingCommand has support for cancellable commands. To support cancellation for a Task it is easiest if the Task extends InterruptibleTask. InterruptibleTask helps to implement the logic necessary to interrupt the background thread if cancel() is called.
When you invoke command.execute() on a SwingCommand instance, a reference to the Task which is created to handle the execution is returned from the execute() method. Getting the reference to the Task which is running can be useful – for Tasks which support the cancel operation, you may want to cancel the task by calling task.cancel()
To implement cancellation correctly, your Task must be able to respond to the interrupt request. There are two main cases to consider:
- Interrupting a task which is blocked on IO, or on some other blocking call
- Interrupting a task programatically by checking the isInterrupted() status periodically
Lets look at the first of these, interrupting a task which is blocking:
//interruping a background task with cancel
SwingCommand c
= new SwingCommand
() {
protected Task createTask
() {
return new InterruptibleTask
() {
public void doInBackground
() throws InterruptedException {
Thread.
sleep(10000);
}
public void doInEventThreadIfNotCancelled
() {
textArea.
setText("Task was not cancelled");
}
};
}
};
//let's pop up a option pane if the task is cancelled
c.
addTaskListener(new TaskListenerAdapter
() {
public void cancelled
(Task task
) {
JOptionPane.
showMessageDialog(frame,
"Task was Cancelled");
}
});
Task t
= c.
execute();
//and then a while later, from any thread...
t.
cancel();
In the example above the Task extends InterruptibleTask. During the doInBackground() handler, the background thread is blocked for a time in the call to Thread.sleep(). When task.cancel() is called by any thread, the InterruptibleTask logic will trigger thread.interrupt() on the background thread. This will result in an InterruptedException being generated by the sleep() method. Because cancel() was called, InterruptibleTask will catch the InterruptedException and the Task will finish with cancelled set to true (task.isCancelled() == true).
Note, if cancel() had not been called, the exception would have been allowed to propagate through causing TaskListeners to receive an error message, and the command would finish in the ERROR state.
What if cancel is called before or after the background processing?
If the call to task.cancel() occurs before background processing starts, the doInBackground() handler will not be called, and cancellation will always succeed. If the call to cancel() occurs after doInBackground() has returned, the call to cancel will not succeed – in this case and the task will end with isCancelled() == false.
The doInEventThreadIfNotCancelled() method is only called if the task is not cancelled. (You could alternatively override doEvenIfCancelled() if you wanted to do some post-processing in the event thread regardless of cancellation).
Interrupting a background task using custom logic
Sometimes a blocked thread will not respond automatically to thread.interrupt(). This is the case during some JDBC queries, for example. In some cases you have to implement extra logic to interrupt the task, and you can do this by overriding the doInterrupt() method. This example demonstrates how you might handle interrupting a query on a JDBC statement:
SwingCommand c
= new SwingCommand
() {
protected Task createTask
() {
return new InterruptibleTask
() {
private volatile Statement s
;
private StringBuffer text
= new StringBuffer();
public void doInBackground
() throws SQLException {
s
= connection.
createStatement();
ResultSet r
= s.
executeQuery("select text from Message");
while(r.
next()) {
text.
append(r.
getString(1)).
append("\n");
}
}
protected void doInterrupt
() throws SQLException {
Statement statement
= this.
s;
if ( s
!= null) {
s.
cancel();
}
}
public void doInEventThreadIfNotCancelled
() {
textArea.
setText(text.
toString());
}
};
}
};
Task t
= c.
execute();
//do something else for a while, and then call:
t.
cancel();
In the example above, the basic processing is the same, but this time we have also overridden the doInterrupt() method, to close the JDBC statement if the background query is interrupted. This should cause the statement.executeQuery() method to throw an SQLException. Because cancel() has been called, this exception will be treated as the result of a cancellation, and the task will end with isCancelled() == true.
Interrupting a task by checking isInterrupted()
There’s just one more case to demonstrate. How to handle a non-blocking cancel. This is actually quite simple – you just need to periodically check the isInterrupted() method, and return early if necessary:
SwingCommand c
= new SwingCommand
() {
protected Task createTask
() {
return new InterruptibleTask
() {
public void doInBackground
() throws InterruptedException {
for ( int loop
=0; loop
< 1000000; loop
++) {
if ( isInterrupted
() ) {
break;
}
}
}
public void doInEventThreadIfNotCancelled
() {
textArea.
setText("Task was not cancelled");
}
};
}
};
Here the doInBackground() handler enters a processing loop. On each iteration, it checks to see if isInterrupted() is true – this will be the case if cancel() has been called on the task. If so, it breaks the loop to end the processing early.
Note that a task is considered cancelled even if the doInBackground() does not return early, provided that when the doInBackground() method returns the background thread still has its interrupted flag set.
Whew – that’s a fair amout to absorb. But the thing to take away is that extending InterruptedTask makes handling cancel easy. There is very little boilerplate code in the examples above, and you shouldn’t need to work too hard to get this right.