Package org.cytoscape.work

This package defines the task framework, where tasks are units of 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:

  1. Create a JMenuItem and assign it a name, icon, and tooltip.
  2. Write an action listener. This could be a named class or an anonymous class.
  3. Add the ActionListener to the menu item by calling the addActionListener method.
When the user selects the menu item, Swing invokes your action listener.

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 Tasks 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:

  1. Write a task factory.
  2. Create a list of properties about the task factory that specify the name of the menu item, its icon and tooltip.
  3. Register the task factory, along with its properties, an an OSGi service.
Cytoscape picks up the task factory, reads its properties, and creates a menu item just for that task factory. When that menu item is selected by the user, Cytoscape tells the task factory to create a task, then invokes it. Tasks are used for more than just creating menu items. For example, they are used for creating toolbar items as well.

Task iterators

Sometimes when a task is executing, you need to run additional tasks after it is done. TaskIterators 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. TaskMonitors are passed in to the task's run method. In the task's run 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 an IOException to be thrown. Because the explanation of the IOException will probably be unhelpful for the user, you should catch IOException 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 the run 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 modify MyPiTask 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 the cancel 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. When run first starts, fill in this variable with the method Thread.currentThread(). Now the cancel method can know the thread that is executing run. When cancellation occurs, call Thread.interrupt() on the thread executing run. 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 an InputStream, a Reader, or a Socket, call close from the cancel method. This will stop the I/O operation immediately in the run 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 make iterations 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;
          }
      };