Package org.cytoscape.work
Tasks are like action listeners
Tasks are somewhat analogous to Swing action listeners. To recap Swing action listeners, let's say you want to create a menu item and listen to its selection. You would follow these steps:
- Create a
JMenuItem
and assign it a name, icon, and tooltip. - Write an action listener. This could be a named class or an anonymous class.
- Add the
ActionListener
to the menu item by calling theaddActionListener
method.
Why tasks?
Cytoscape is in an OSGi environment. Its components are deliberately made to be loosely coupled, and this loose coupling changes how we think about action listeners. With action listeners, you have to explicitly tell an object you want to listen to it by calling a method on that object. In a loosely decoupled environment, action listeners are not appropriate. Instead, you implicitly tell an object you want to listen to it by exporting an OSGi service. That object knows all the OSGi services interested in it and calls them when necessary.
We establish Task
s as a replacement for action listeners to take advantage
of OSGi's loose coupling. A single task object can be thought of as an
action listener. Tasks implement the run
method, which is analogous
to the action listener's actionPerformed
method. A TaskFactory
creates tasks. You export a task factory as an OSGi service, not tasks.
Why task factories?
Why even have task factories? Why not just use tasks directly?
Many times tasks need input parameters.
For example, if you're performing the zoom in task, how would the task know which network
view on which to perform the zoom? The run
method, after all,
does not allow for input parameters.
To deal with this issue, you create a task factory that can take
some parameters, then it returns a new task initialized with those parameters.
The task can obtain those parameters either through its constructor or as an
inner class that has access to the task factory's members.
Moreover, a well written task factory returns new instances of Task
each time
the task factory is invoked. A new task object per invocation neatly contains state that does not
spill over into separate task invocations. Incorrectly written task factories
return the same task instance each time it is invoked.
(If you know a functional programming language, here is an
analogy: a task is like an inner function
that the task factory function returns; the task gets inputs from free variables
defined in the task factory function.)
Overview of a menu item with a task factory
To illustrate tasks and task factories, here is how you create a menu item in Cytoscape's OSGi environment:
- Write a task factory.
- Create a list of properties about the task factory that specify the name of the menu item, its icon and tooltip.
- Register the task factory, along with its properties, an an OSGi service.
Task iterators
Sometimes when a task is executing, you need to run additional
tasks after it is done. TaskIterator
s let you create a series of tasks
where you choose the task to execute next, even while a task in that iterator
is executing.
When you write your task factory implementation, you return a TaskIterator
that
encapsulates your tasks. In most cases, you want to execute a
single task, not a sequence of tasks. In this case, when you write a task factory
implementation, return a new task iterator that only contains your single task.
Task managers
Most of the time, you export a task factory service, and Cytoscape
invokes the task for you. Sometimes, you will want to invoke a task yourself.
In this case, you import the TaskManager
service
and call the TaskManager.execute(org.cytoscape.work.TaskIterator)
method to
run the task.
Action listeners are dead—long live tasks
The analogy of tasks as an OSGi equivalent of action listeners sweeps many of its advantages under the rug. Here are some additional benefits of tasks:
-
Tasks are run asynchronously. This means you can encapsulate
complicated, long-running algorithms entirely in a single task.
You do not have to think about threading issues, as this will not freeze Cytoscape.
However, if a portion of your task's code invokes Swing, you may have to wrap the
code in a
SwingUtilities.invokeLater
. -
Because tasks can take a long time to complete,
they can inform the user of its progress through
TaskMonitor
.TaskMonitor
s are passed in to the task'srun
method. In the task'srun
method, you call its methods to update the user interface. -
Tasks can throw exceptions in the
run
method. Cytoscape will catch the exception and inform the user. When a task encounters an exception, it is considered good practice to catch the exception and throw another exception with a high-level explanation that the user can understand. For example, let's say your task is reading from a file, which can cause anIOException
to be thrown. Because the explanation of theIOException
will probably be unhelpful for the user, you should catchIOException
and throw your own exception with an easily understandable explanation and some solutions for the user to remedy the problem. -
Tasks can be cancelled by the user. Tasks are required to implement the
cancel
method. Long-running tasks must respond to user cancellations and return from therun
method gracefully. -
Tasks are independent of Swing. Provided that
the
run
method does not explicitly use Swing, the task can be used in other environments besides Swing, like the command-line. - Tasks can have user-defined inputs called tunables. When Cytoscape detects that the task has tunables, it creates an interface for user input. Once the user clicks OK, Cytoscape fills in the tunables with the user's input, then executes the task. Tunables are suited for algorithms with settings that the user can change before running the task. With tunables, you do not have to manually create a Swing dialog to get user input. If the task is run in the command-line, the user can still provide input.
Task Factories in org.cytoscape.task
Package
As stated above, the primary purpose of task factories is that they accept input parameters
to be passed on to task instances. While developing Cytoscape, we found that many of our task
factories accept the same input parameters. At the top level of the org.cytoscape.task
package, we created a dozen specialized task factories that accept specific inputs.
Each interface in this package is a specialized task factory that groups together
task factory implementations based on common inputs.
For instance, the NodeViewTaskFactory
takes a node view as an input
parameter. Instead of a TaskFactory
, when you write an
implementation to NodeViewTaskFactory
, Cytoscape understands that your task factory
expects a node view as input. In terms of the user interface, Cytoscape will create a menu
item for your task factory when the user right-clicks on a node. Your task factory
will be invoked with the node view the user right-clicked on when the user selects your menu item.
In this respect, a NodeViewTaskFactory
defines an action that operates on a node view.
Subpackages in the org.cytoscape.task
package contain interfaces denoting
common user actions in Cytoscape. In most cases, you do not write implementations to these
interfaces. Instead, if you want to use the functionality specified by one of these
interfaces, you import them as an OSGi service.
Here's an example. The CloneNetworkTaskFactory
interface in the org.cytoscape.task.create
subpackage represents the action of
duplicating a network. If you want to duplicate a CyNetwork
, you would import the
CloneNetworkTaskFactory
as an OSGi service, then invoke its task by passing in
the CyNetwork
you want to duplicate to the task factory.
Sometimes you do write your own implementation to an interface in a
org.cytoscape.task
subpackage. For instance, let's say you
are developing your own method that analyzses the topology of a network.
You encapsulate the analysis in a task. You then export your task factory service
as an AnalyzeNetworkCollectionTaskFactory
.
Exporting your interface using AnalyzeNetworkCollectionTaskFactory
instead of just an NetworkCollectionTaskFactory
makes the purpose of your task more explicit.
Before exporting your task through a basic TaskFactory
,
it is important to think about the inputs the task needs and its objectives.
Is it the case that your task operates on a CyNetwork
?
Your task factory should implement NetworkTaskFactory
.
Specialized task factory interfaces in org.cytoscape.task
make
the inputs and the objectives explicit.
Examples
Hello World Menu Item
class MyTask implements Task {
public void run(TaskMonitor monitor) {
logger.info("Hey chef");
}
public void cancel() {
}
}
...
// Example that displays a user message at the bottom of the Cytoscape desktop
Logger logger = LoggerFactory.getLogger("CyUserMessages");
TaskFactory myTaskFactory = new TaskFactory() {
public TaskIterator createTaskIterator() {
return new TaskIterator(new MyTask());
}
public boolean isReady() {
return true;
}
};
Properties props = new Properties();
props.setProperty(PREFERRED_MENU,"Apps");
props.setProperty(TITLE,"Why, hello there children");
props.setProperty(MENU_GRAVITY,"1.0");
props.setProperty(TOOLTIP,"Demonstrates how cool the work framework is");
registerService(bc, myTaskFactory, TaskFactory.class, props);
Approximating π
Here is another example of a task that approximates π using the Wallis product:
// Example that approximates pi Logger logger = LoggerFactory.getLogger("CyUserMessages"); class MyPiTask implements Task { final int iterations = 1000; public void run(TaskMonitor monitor) { double pi = 2.0; for (int n = 1; n <= iterations; n++) { pi *= ((double) (2 * n) * (2 * n)) / ((2 * n - 1) * (2 * n + 1)); } logger.info("Our approximation of pi is: " + Double.toString(pi)); } public void cancel() { } } ... TaskFactory myTaskFactory = new TaskFactory() { public TaskIterator createTaskIterator() { return new TaskIterator(new MyPiTask()); } public boolean isReady() { return true; } }; Properties props = new Properties(); props.setProperty(PREFERRED_MENU,"Apps"); props.setProperty(TITLE,"Approximate Pi"); props.setProperty(MENU_GRAVITY,"1.0"); props.setProperty(TOOLTIP,"Approximates pi using the Wallis product"); registerService(bc, myTaskFactory, TaskFactory.class, props);
Using a task monitor
The problem with the above example is that if the π calculation takes a long time, the user is not informed of its progress. Here, we modifyMyPiTask
so that it informs the user during this long calculation
by using the task monitor.
class MyPiTask implements Task { final int iterations = 1000; public void run(TaskMonitor monitor) { monitor.setTitle("Calculating Pi"); double pi = 2.0; for (int n = 1; n <= iterations; n++) { monitor.setProgress(((double) n) / iterations); pi *= ((double) (2 * n) * (2 * n)) / ((2 * n - 1) * (2 * n + 1)); } logger.info("Our approximation of pi is: " + Double.toString(pi)); } public void cancel() { } }
Cancelling a task
The problem with our task is that it does not adequately respond to user cancellation. When the user cancels the task, nothing happens. We have to fill in thecancel
method. The main challenge of dealing with cancellation
is that the thread executing run
is different
from the one executing cancel
. We have to come up with a way
to communicate cancellation from cancel
to run
.
Here is how we deal with this issue:
class MyPiTask implements Task { final int iterations = 1000; volatile boolean cancelled = false; public void run(TaskMonitor monitor) { monitor.setTitle("Calculating Pi"); double pi = 2.0; for (int n = 1; n <= iterations; n++) { if (cancelled) break; monitor.setProgress(((double) n) / iterations); pi *= ((double) (2 * n) * (2 * n)) / ((2 * n - 1) * (2 * n + 1)); } if (!cancelled) logger.info("Our approximation of pi is: " + Double.toString(pi)); } public void cancel() { cancelled = true; } }When the user hits the Cancel button, Cytoscape invokes the
cancel
method.
This method switches the cancelled
variable to true. When run
starts another
iteration of the for
loop, it checks the cancelled
variable.
Since cancelled
was flipped, it stops the loop.
In other words, the cancel
method tells the run
method to stop through
the cancelled
variable.
Communicating cancellation through a boolean variable works well for tasks with loops, since each loop iteration can just check if the variable has changed and stop. Tasks that do I/O operations, however, require more consideration, like a task that downloads a file from some URL. There are several ways you can deal with cancellation during an I/O operation:
-
Create a variable in your task that holds the thread executing
run
. Whenrun
first starts, fill in this variable with the methodThread.currentThread()
. Now thecancel
method can know the thread that is executingrun
. When cancellation occurs, callThread.interrupt()
on the thread executingrun
. Interrupting a thread in an I/O operation will sometimes stop it. Here's how this would look:class MyTask implements Task { volatile Thread runThread = null; public void run(TaskMonitor monitor) { runThread = Thread.currentThread(); ... // complicated I/O operations } public void cancel() { runThread.interrupt(); } }
-
If your
run
method is reading from anInputStream
, aReader
, or aSocket
, callclose
from thecancel
method. This will stop the I/O operation immediately in therun
method.It is discouraged to use
URLConnection
, because it is not possible to cancel a pending connection. Consider using Apache HttpClient library instead. -
Use Java's non-blocking I/O package:
java.nio
. Since non-blocking I/O operations are usually done in a loop, it works well with the approach of checking for cancellation from a variable. Using non-blocking I/O is more elegant, but it takes a lot more effort to write.
A task with tunables
We have hard-coded a value for the number of iterations to approximate π. Thanks to tunables, we can easily give the user the option to specify the number of iterations. That way the user can decide how long to run our task—or how accurate our approximation should be. We makeiterations
a tunable whose value the user can specify.
Task myPiTask = new Task() { @Tunable(description="How many iterations of the Wallis product to compute?") public int iterations = 1000; boolean cancelled = false; public void run(TaskMonitor monitor) { monitor.setTitle("Calculating Pi"); double pi = 2.0; for (int n = 1; n <= iterations; n++) { if (cancelled) break; monitor.setProgress(((double) n) / iterations); pi *= (2 * n) * (2 * n) / ((2 * n - 1) * (2 * n + 1)); } if (!cancelled) logger.info("Our approximation of pi is: " + Double.toString(pi)); } public void cancel() { cancelled = true; } };
-
Interface Summary Interface Description ObservableTask ATask
that notifies its observers when it is finished executing.ServiceProperties Reserved keywords for OSGi service properties (meta data).SynchronousTaskManager<T> A marker interface that indicates that the TaskManager in question will execute the tasks found in the TaskFactory synchronously with the current thread, blocking code execution until all tasks finish.Task This interface specifies a unit of work to be executed asynchronously in its ownThread
along with a user interface to display its progress, provide a means for the user to cancel theTask
, and show information about anyException
s thrown during its execution.TaskFactory Returns an instance of aTaskIterator
.TaskManager<T,C> TaskMonitor Used by aTask
's implementationrun
method to inform users of the status of its execution.TaskObserver An observer that gets notified when anObservableTask
finishes executing.TunableHandler Interface for classes that deal with reading out and writing backTunable
s and their properties.TunableHandlerFactory<T extends TunableHandler> A factory service to create aTunableHandler
for a single type of object, determined by the type of the field or the return value of the getter method in the appropriate methods.TunableMutator<T extends TunableHandler,S> This is a type of tunable interceptor that reads and modifies the values annotated with theTunable
annotation.TunableRecorder<T extends TunableHandler> TunableRecorder is a special type of tunable interceptor that reads the state of the tunables but does not modify the value of the tunables.TunableSetter An API for setting tunable fields and methods with predetermined values in the Tasks found in the specified TaskIterator.TunableValidator If implemented, this interface is used to apply a test to the modified values of a Tunable. -
Class Summary Class Description AbstractTask A base class for tasks that need to be able to access the TaskIterator that contains them.AbstractTaskFactory A TaskFactory that is always ready to produce a TaskIterator.AbstractTaskManager<T,C> Provides access to a TunableInterceptor to all derived classes and a utility method to determine if an object has been annotated with Tunables.AbstractTunableHandler Provides the standard implementation for most of the methods declared by the TunableHandler interface.AbstractTunableInterceptor<T extends TunableHandler> An abstract base class for TunableRecorder and TunableMutator implementations.BasicTunableHandlerFactory<T extends TunableHandler> A convenience implementation of TunableHandlerFactory that will construct a TunableHandler of the specified type given the TunableHandler in question has at least two constructors, one with Field, Object, Tunable parameters and the other with Method, Method, Object, Tunable parameters.FinishStatus Indicates the status of a task iterator when it has finished forTaskObserver
s.TaskIterator A TaskIterator provides the functionality of sequencingTask
s.TunableGravityOrderer This class provides a comparator to order theTunable
s based on their gravity value. -
Enum Summary Enum Description FinishStatus.Type TaskMonitor.Level Used by theshowMessage
method to indicate the severity of the message.TunableValidator.ValidationState The states the the validator can return. -
Annotation Types Summary Annotation Type Description ContainsTunables An annotation designed to signal that the annotated field contains fields and methods that are annotated with theTunable
annotation.ProvidesTitle An annotation type that can be applied to a method which returns a String that will be used for the title of a Tunable user interface dialog window.Tunable