Package org.cytoscape.work


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;
          }
      };
  
  • Class
    Description
    A base class for tasks that need to be able to access the TaskIterator that contains them.
    A TaskFactory that is always ready to produce a TaskIterator.
    Provides access to a TunableInterceptor to all derived classes and a utility method to determine if an object has been annotated with Tunables.
    Provides the standard implementation for most of the methods declared by the TunableHandler interface.
    An abstract base class for TunableRecorder and TunableMutator implementations.
    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.
    An annotation designed to signal that the annotated field contains fields and methods that are annotated with the Tunable annotation.
    Indicates the status of a task iterator when it has finished for TaskObservers.
     
    A Task that notifies its observers when it is finished executing.
    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.
    Reserved keywords for OSGi service properties (meta data).
    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.
    This interface specifies a unit of work to be executed asynchronously in its own Thread along with a user interface to display its progress, provide a means for the user to cancel the Task, and show information about any Exceptions thrown during its execution.
    Returns an instance of a TaskIterator.
    A TaskIterator provides the functionality of sequencing Tasks.
    Executes the Tasks found in the TaskIterator provided by a TaskFactory.
    Used by a Task's implementation run method to inform users of the status of its execution.
    Used by the showMessage and setStatusMessage methods to indicate the severity of the message.
    An observer that gets notified when an ObservableTask finishes executing.
    This interface is meant to be implemented by task factories that provides tasks which switches something on and off.
    An annotation type that can be applied to public fields or a methods in a Task object that allows the Task to be configured with user supplied information.
    This class provides a comparator to order the Tunables based on their gravity value.
    Interface for classes that deal with reading out and writing back Tunables and their properties.
    A factory service to create a TunableHandler 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.
    This is a type of tunable interceptor that reads and modifies the values annotated with the Tunable annotation.
    TunableRecorder is a special type of tunable interceptor that reads the state of the tunables but does not modify the value of the tunables.
    An API for setting tunable fields and methods with predetermined values in the Tasks found in the specified TaskIterator.
    If implemented, this interface is used to apply a test to the modified values of a Tunable.
    The states the the validator can return.