Most software applications feature processes strung together in sequences, each fulfilling specific functions. In such systems, an end user, directly or indirectly, initiates or starts most processes. Further, most processes are instantaneous—they execute the moment such execution is requested.

In contrast, conditional processes do not immediately execute. Conditional processes execute on the provision that a particular event or occurrence takes place. A conditional process is usually managed by the application system that needs it; for example it could be managed by a cron (a command that executes processes at a specific date/time) job in Unix or by database triggers embedded in a database management system.

In this article, I describe a mechanism that allows Java-based applications to manage conditional processes within the applications themselves. The mechanism unifies all the conditional processes and provides portability to them.

Before we delve into the mechanism for managing conditional processes, let's go through some conditional process groundwork.

Note: You can download this article's source code from Resources.

Conditional processing

Processes come in a hierarchy—all processes stem from some initial root process, in most cases the operating system itself. The root process creates all other subsequent processes. In a controlled environment, spawning subprocesses does not involve the programmer. In other words a programmer can rely on the operating system to handle the processes it spawns. Similarly, in an application, the application handles the processes it generates.

Normal processes execute instantaneously upon creation. In contrast, conditional processes wait for an event to occur before execution. If the controlling environment produces the event, the process becomes simple because the environment can track the event and notify the waiting process. For example, in a graphical user interface (GUI) application, you can track mouse-generated events and notify any processes waiting for a particular mouse click. GUI frameworks like Swing and AWT trap the operating system events and pass them to the processes that need them.

This all works well in controlled environments. Unfortunately, problems arise when the monitored event resides outside the environment that spawns the process's control. A common example is a time-triggered event. The passage of time does not trigger any events in most operating systems. As a result, a passive event-listener mechanism like Swing or AWT cannot monitor this occurrence, and hence such mechanisms usually rely on an external program, like cron in Unix or the AT command in Windows NT/2000. Similarly, other application-specific events, such as user logins or transactions, do not trigger events implicitly if the stimulus resides outside the program.

You could solve the problem with explicit triggers. For example, to listen to user logins, you'd log such events into the database and add database triggers to monitor the data change. However, explicit triggers are native implementations; they depend on the underlying event's triggering mechanism. Further, you cannot directly control them from the waiting process's environment. For example, to use database triggers you need to either depend on the database vendor's trigger implementation or to have a separate process to execute if different events occur. No unifying factor ties either event.

A better solution actively monitors such events. Active monitoring requires a separate and isolated process entirely dedicated to monitoring the events and triggering the process if the event occurs. Active monitoring unifies the event capturing process—that is, it monitors the database record and the passage of time in a similar manner. The main drawback: it's expensive because you need a separate, continuously running process. Despite its drawbacks, the approach presented in this article employs active monitoring.

Design

Conditional processing works as a simple cause-and-effect mechanism. An event triggers a process to perform a task. The system includes the following objects:

  • Task: Represents a conditional process. A task contains one or more conditions and one or more actions (Figure 1).
  • Action: Represents an action to be performed.
  • Condition: Represents the condition upon which the action will be performed.
Figure 1. A task comprises a list of actions and conditions

Each action can execute only if all conditions are met. Moreover, you can add additional actions or conditions to the same task.

With this design, one developer can create action objects and another can create condition objects, all while a third assembles tasks using these actions and conditions to create conditional processes. Conditional processes can also pass messages between multiple disconnected systems. For example, imagine a corporate portal that interfaces with a human resource management system, a financial management system, and a workgroup collaboration system. The triggering event: an employee applies for leave through the corporate portal. Under normal circumstances, the employee would need to access each system separately to record this event, a tedious and error-prone process. Our goal is to enable the portal to effectively define the trigger as a singular atomic event and allow the portal to execute the event on the user's behalf.

One way to accomplish our goal: direct data manipulation in which the portal changes the raw data from each system's data sources. That solution, however, could open a dangerous security loophole; it would also frequently cause data corruption in individual systems. A common alternate solution would make each system open up APIs for the portal to call. However, you would have to manage the APIs in such a scenario. Each system would have its own API set that the portal would need to understand and call, which would make the corporate portal developer's job difficult. In addition, because there would be no standard way to make the API calls each time, the code management would become messy.

Separating the condition and the action makes passing messages to each system more manageable. In the corporate portal example, each system would write actions performable on itself. For example, the human resource system's action would add a leave record, the financial management system's action would add a claim submission, and the group collaboration system's action would add an entry into the employee's public calendar. By creating a task with these three actions and a condition that monitors the actual event, each system would receive notification. Additionally, because the task can have multiple conditions, the task can satisfy all the conditions before passing the message to all three systems. With a series of such tasks, each system can also pass messages and event notifications to each other.

Implementation

Now let's analyze a possible solution. The mechanism's implementation should be simple and clean. The key is the task object, which contains both action-object and condition-object lists. The task list must be kept in a task registry. This registry can be simple. You'll have to create a registry manager that allows users to add, remove, or modify tasks. You also need a monitor that periodically checks if each task's conditions are met. If they are met, the tasks' actions will execute sequentially. Figure 2 shows the mechanism's features.

Figure 2. The conditional process management mechanism in a nutshell. Click on thumbnail to view full-size image.

Task

A task object contains a list of actions and conditions. Each condition must be fulfilled before the list's actions execute sequentially. In other words, the condition list forms an "AND" sequence, so if a single condition in the list fails, the actions in the task will not be performed. That is one of the simplest implementations of a task. You can also implement more complex and enhanced sequences; for example, an "OR" sequence would indicate that if any of the conditions in the list are met, the actions in the task would be executed.

The "AND" sequence, though simple, provides interesting flexibility in task processing. For example, if a bank teller performs more than 10 transactions between 1 and 2 p.m. (lunchtime) in a day, he will be barred from the system until his supervisor unlocks him. Such a security constraint could prevent bank tellers from performing too many unchecked transactions during lunchtime. In this case, you'd create a task with the following conditions:

  • The transaction user must be a bank teller
  • The time must be between 1 and 2 p.m.
  • The number of transactions must be more than 10

If those conditions are met, the action bars the bank teller from further transactions and sends the supervisor a message.

Here's a small snippet from the Task class:

import java.util.*;
public class Task implements java.io.Serializable {
  private String taskID = new String();
  private String taskName = new String();
  private ArrayList actions = new ArrayList();
  private ArrayList conditions = new ArrayList();
  public String getID() {
    return taskID;  
  }
  public void setID (String id)  {
    taskID = id;
  }
  
  public void execute() {
    for (Iterator i = actions.iterator(); i.hasNext();) {
      Action act = (Action)i.next();
      act.execute();
    }
  }
  public boolean checkAllConditions() {
    for (Iterator i = conditions.iterator(); i.hasNext();) {
      Condition cond = (Condition)i.next();
        if (!cond.check()) {
          return false;             
      }
    }
    return true;
    }
  }
.
.
.

The Task class also features a self-duplication mechanism, necessary because the time conditions are not implemented as a fixed series of time conditions in a task. A duplicate task automatically generates upon the execution's success. The self-duplication mechanism works this way:

  1. Once the task executes, the monitor checks whether the task should periodically repeat
  2. If yes, the task duplicates itself but readjusts the time for execution to the next occurrence
  3. The duplicated task becomes added into the task repository and the executed task is removed

The advantage of such a mechanism is obvious: you don't need to create numerous tasks in the repository to provide for periodic tasks. Each self-generating task produces a duplicate to continue during the next period. (I further describe the duplication mechanism in the "Condition" section below.)

Tasks reside in a task repository:

import java.util.*;
public class TaskRegistry implements java.io.Serializable {
  HashMap registry = new HashMap();
  private static TaskRegistry instance;
    
  static synchronized public TaskRegistry getInstance() {
    if (instance == null) {
    instance = new TaskRegistry();
    }
    return instance;
  }
  public void addTask(Task task) throws NullPointerException {
    if (task.getAllConditions() == null) {
    throw new NullPointerException("### Error : Every task must have at least 1 condition.");           
    }
    if (task != null) {
      registry.put(task.getID(), task);
    }
    else {
      throw new NullPointerException("### Error : You tried to add a null task.");          
    }
  }
.
.
.

You will not need to use the TaskRegistry class directly. This article's implementation features an EJB (Enterprise JavaBean) that provides an interface to manage the tasks in the task registry. Here are some code snippets that describe how the bean implements the task management:

public class TaskRegistryBean implements SessionBean {
  public void registerTask(Task task) 
    throws RemoteException, NullPointerException {
    TaskRegistry registry;
    if ((registry = TaskRegistrySerializer.deserialize()) == null) {
      registry = TaskRegistry.getInstance();
    }
    registry.addTask(task);
    TaskRegistrySerializer.serialize(registry);
  }
  
  public void removeTask(String taskID) 
    throws RemoteException, TaskNotFoundException {
    TaskRegistry registry;
    if ((registry = TaskRegistrySerializer.deserialize()) == null) {
      throw new TaskNotFoundException("The task registry is empty.");
    }
    registry.removeTask(taskID);
    TaskRegistrySerializer.serialize(registry);
  }
     
  public Task getTask(String taskID)  
    throws RemoteException, TaskNotFoundException {
    TaskRegistry registry;
    if ((registry = TaskRegistrySerializer.deserialize()) == null) {
      throw new TaskNotFoundException("The task registry is empty.");
    }
    return registry.getTask(taskID);
.
.
.

Alternatively, in a non-EJB situation, you can use a TaskRegistryManager class instead.

Action

Actions, normal processes that perform the functions desired when all conditions return true, execute immediately. The mechanism caters to any action that can be taken, because the Action interface includes a single method:

public interface Action extends java.io.Serializable {
  public void execute(ArrayList parameters);
}

You can implement any action with the execute() method. The Action interface takes in an ArrayList parameter list. The Action class can either use or ignore those parameters.

Condition

Conditions, which check the system's current state, must be customized and must implement the Condition interface:

public interface Condition extends java.io.Serializable {
  public boolean check();
  public Object getResult();
}

The getResult() method returns any object upon checking the condition, which will form part of the parameters passed to the action objects.

Page 2 of 3

TimeCondition, a special Condition instance that Task always checks, represents a conditional check against time. TimeCondition's frequency repeating function allows a task to execute at regular intervals ranging from one minute to one year. Here's a TimeCondition snippet:

public class TimeCondition implements Condition {
  public static final int NONE = 0;
  public static final int HOUR = Calendar.HOUR;
  public static final int DAY = Calendar.DATE;
  public static final int  WEEK = Calendar.WEEK_OF_YEAR;
  public static final int  MONTH = Calendar.MONTH;
    
  int frequency = NONE;
  int multiplier = 1;
    
  Date time = null;
    
  public boolean isRepeated() {
  if (frequency == HOUR || frequency == DAY || frequency == WEEK || 
      frequency == MONTH ) 
    return true;
  else
    return false;
  }
  /**
  Implements the check() method from Condition
  */
  public boolean check() {
    if (time == null) return false;
    Date now = new Date();
    if (time.compareTo(now) < 0) {
      return true;
    }
      return false;         
    }
  }

TimeCondition

checks whether the current time is after the recorded time, a situation that causes the condition to trigger a positive response. Consequently, you cannot specify with the

TimeCondition

such tasks as, "run this program between 2 p.m. to 3 p.m.," "approve this check while the current time is before 5 p.m.," or even "check for mail at 2 p.m. and 5 p.m. every day." Nevertheless, you can describe such conditions with a few

TimeConditions

combined with other conditions.

For example, to describe "run this program between 2 p.m. and 3 p.m.," combine a TimeCondition that authorizes upon crossing 2 p.m. and a customized condition that checks if the time is before 3 p.m. The mechanism's flexibility lets you add any number of conditions to a task.

As for the second task, "approve this check while the current time is before 5 p.m.," simply describe the task in a customized condition that checks whether the time is before 5 p.m. before authorizing the check.

The last task, "check for mail at 2 p.m. and 5 p.m. every day" looks difficult because we cannot describe it in a single task without complicated conditions. Nevertheless, rather than putting two conditions in the same task, you can resolve the problem by splitting the check into two tasks, one that checks for mail at 2 p.m. and another that checks for mail at 5 p.m.

As mentioned above, with TimeCondition you can repeat tasks with frequencies ranging from one minute to one year. Therefore, you can specify tasks repeated once every hour, indefinitely. How? As mentioned above, each task with a repeatable TimeCondition will duplicate and add itself into the task registry once it successfully executes. The time is rolled forward to the required frequency. For example, if you need to execute a task once every three days:

  TimeCondition tc = new TimeCondition();
  tc.setTime(new Date());
  tc.setFrequency(TimeCondition.DAY);
  tc.setMultiplier(3);
  task.addCondition(tc);

What happens if the other conditions fail? The task will be monitored and each condition checked until all conditions pass. Rolling forward uses the time specified in the TimeCondition, not the time when all conditions pass. In the example above, if a task executes once every three days and the conditions pass one day after the task is added, the TimeCondition check executes two days later rather than three. If all conditions pass only after the next scheduled TimeCondition check, the date will roll forward until after the next check and after all the conditions pass.

For example, if the conditions pass five days after the task is added, the next TimeCondition check is one day after that:

if (timeCondition.isRepeated()) {
  toRepeat = true;
  TimeCondition newTimeCondition = new TimeCondition();
  int freq = timeCondition.getFrequency();
  int multiplier = timeCondition.getMultiplier();
  Date time = timeCondition.getTime();
        
  GregorianCalendar g = new GregorianCalendar();
  g.setTime(time);
  
  /* If the new time is before the current time, roll it until it is 
     after */
  while(g.before(Calendar.getInstance()) {
    g.roll(freq,multiplier);
  }
  newTimeCondition.setTime(g.getTime());
  newTimeCondition.setFrequency(freq);
  newTimeCondition.setMultiplier(multiplier);
  newTask.addCondition(newTimeCondition);
}

Data storage

You must store the task data. However, because this is a reusable mechanism, you cannot anticipate the action or condition types for the various systems that use them. You could shift the responsibility for defining the data storage to the systems that use the data, as seen in Figure 3. That way, each system that uses data would need to:

  • Define the necessary actions and conditions
  • Define the database tables that store the task (hence the actions and conditions) information

The mechanism must also have:

  • A central table to control each subtable
  • An action and condition registry to manage the various action and condition types
  • Classes to convert the data stored in the database back into a reusable Java object
Figure 3. Conventional storage for actions and conditions. Click on thumbnail to view full-size image.

A much easier alternative, shown in Figure 4, uses Java Object Serialization to serialize the task objects (and, because of the object tree, the action and tasks as well) into data storage. The default implementation serializes the object to file. Although some find serialization to file objectionably slow, it works reasonably well (depending on your need) because only one monitor retrieves the tasks from the repository and the processing often takes longer than the retrieval process under normal circumstances. However, with serialization to file, you cannot have a large number of tasks. For a more robust and scalable implementation, serialize the data and store it as blobs in a relational database (a discussion beyond this article).

Figure 4. Object-oriented storage for actions and conditions

In both cases, because the task objects themselves are serialized and deserialized, you don't need to convert the data in the database into an object and back. Consequently, the mechanism does not need to know or understand the action and condition types; hence, it does not need to register new action and condition types. Because the customized actions and conditions implement the respective interfaces, they are deserialized, cast back to these interfaces, and activated through the interface-defined methods. Therefore, the mechanism never needs to know what kind of actions or conditions the developer implemented.

Monitor and processing tasks

The Monitor standalone application monitors and inspects each task in the task registry to ensure that the task conditions are fulfilled. If so, the task's actions execute. The multithreaded Monitor application spawns a single thread for each registered task. You can configure the maximum number of spawned threads; if the task number exceeds the maximum thread number, the monitor waits until a thread becomes free for processing. Multithreading means that task processing will be parallel rather than sequential—and hence will be faster. Adding threads produces faster processing, but, because each thread occupies memory, it also slows down the system. Therefore, choosing the appropriate maximum thread number proves important for optimum performance. Monitor employs the Xander failover server mechanism (see the example source code) to ensure that a server always monitors the tasks in the task registry.

Let's examine the Monitor class implementation below. Note that Monitor implements the Application class from the Xander API:

public class Monitor implements Application {
  Config config = Config.getInstance();
  int _PERIOD = config.getMonitorWaitInterval(); // Period in seconds
  int _THREADS = config.getMonitorThreads();
  static int _threadCount = 0;
  Date _start = new Date();
  /**
  Activates the monitor
  */    
  public void activate() {
    Date now = new Date();
        
    long start_millis = _start.getTime();
    long now_millis = now.getTime();
        
    if (now_millis > (start_millis + (_PERIOD * 1000))) {
      _start = now;
      monitor();        
    }
  }
private void monitor() {
  System.out.println("Triggering the monitor now!");
  
  try {
    Hashtable env = new java.util.Hashtable(1);
    env.put("java.naming.factory.initial",
            "weblogic.jndi.WLInitialContextFactory");
    InitialContext initContext = new javax.naming.InitialContext(env);
    TaskRegistryRemote taskRegistry = 
           (TaskRegistryRemote)initContext.lookup("taskregistry");
    Task [] tasks  = taskRegistry.getAllTasks();
    ArrayList taskList = new ArrayList();
    
    for (int i=0; i<tasks.length;i++) {
      taskList.add(tasks[i]);
    }        
    while (!taskList.isEmpty()) {
      ArrayList dupTaskList = (ArrayList)taskList.clone();
      for (Iterator itr = dupTaskList.iterator(); itr.hasNext();) {
        Task task = (Task)itr.next();
        if (_threadCount < _THREADS) {
          TaskProcessingThread thread = new TaskProcessingThread(task);
          thread.start();
          taskList.remove(taskList.indexOf(task));
        }
        else {
          System.out.println("Waiting for free thread ...");
        }
      }
    }
  }
  catch (TaskNotFoundException e) {
    System.out.println("No tasks found in the task registry.");  
  }  
  catch (RemoteException e) {
    System.err.println("Cannot get EJBs.");
    e.printStackTrace();
    System.exit(-1);
  }
}

Each thread activates a TaskProcessorBean EJB, a simple stateless session bean that checks a task's every condition. If all conditions pass, the task's actions execute:

public class TaskProcessorBean implements SessionBean {
  
  public void process(Task task) throws RemoteException {
    /**
    If all the conditions pass, execute the task
    */
    try {
      if (task.checkAllConditions()) {
        task.execute();
.
.

Upon the task's successful execution, the TaskProcessorBean checks the task for a TimeCondition. If it finds a repeatable TimeCondition attached to the task, TaskProcessorBean duplicates the task and modifies the TimeCondition to activate at the next occurrence by rolling the TimeCondition's time forward to the next date, as specified by the frequency and multiplier:

/* Remove the task from the registry */
registry.removeTask(task);
Task newTask = task.duplicate();
.
.
.
if (timeCondition.isRepeated()) {
  toRepeat = true;
  TimeCondition newTimeCondition = new TimeCondition();
  int freq = timeCondition.getFrequency();
  int multiplier = timeCondition.getMultiplier();
  Date time = timeCondition.getTime();
  
  GregorianCalendar g = new GregorianCalendar();
  g.setTime(time);
  
  while(g.before(Calendar.getInstance()) {
    g.roll(freq,multiplier);
  }
  newTimeCondition.setTime(g.getTime());
  newTimeCondition.setFrequency(freq);
  newTimeCondition.setMultiplier(multiplier);
  
  newTask.addCondition(newTimeCondition);
    
  }
.
.
.
}
if (toRepeat) {
  registry.registerTask(newTask);   
}

As with the TaskRegistryBean, the TaskProcessor need not be an EJB; you could convert it into a Java class.

How to use

Ease of use often proves crucial when designing reusable infrastructures, and this article's mechanism is no different. To use the mechanism, follow a two-step process:

  1. Create and register the task into the task registry
  2. Run the Monitor as a standalone server to loop through the tasks in the task registry and trigger them

The following snippet shows how to create and register a task:

.
.
.
// Lookup the task registry bean
TaskRegistryRemote beanRemote = ctx.lookup("taskregistry");
// Create a new task
Task task = new Task();
System.out.println("Task created: " + task.getID());
// Add a time condition
TimeCondition tc = new TimeCondition();
tc.setTime(new Date());
tc.setFrequency(TimeCondition.HOUR);
task.addCondition(tc);
// Add another condition; the task executes only if both conditions pass
TestCondition testc = new TestCondition();
Task.addCondition(testc);  
// Add an action to execute
TestAction act = new TestAction();
task.addAction(act);
// Register the task  
beanRemote.registerTask(task);
.
.
.

To activate the Monitor, just run:

%prompt%> java -Dxander.properties=xander.prop xander.server.Server

The Xander server activates the Monitor at regular intervals (configurable in the xander.prop file).

Page 3 of 3

Drawbacks

Because of its design and implementation, this mechanism has several drawbacks:

  • You must run a separate Java standalone server. Because that server is multithreaded and stays in permanent memory, it occupies memory. Therefore, avoid running the server on the same machine as the task-processing server in a production environment.
  • The server cannot schedule beyond a minimum time barrier. That is, you cannot schedule tasks over less than a certain time. The limit depends on the number of tasks in the task registry, as well as the time needed to process each of them. As a general rule, avoid scheduling tasks that run more than once a minute.
  • All conditions must be met before each action activates. This is purely an implementation-based restriction.

Despite these drawbacks, developers should find the unified conditional processing mechanism useful in many situations.

Conditional processes made simple

Developers traditionally relegate conditional processing to the backbenches of server scripts run by cron Unix jobs or batch manager programs that schedule individual disparate programs. As a result, controlling conditional processes programmatically from a unified viewpoint often proves difficult if not impossible. Nevertheless, with smart design, you can control conditional processes easily. In this article, you saw a simple but efficient mechanism to create and control conditional processes directly from any Java-based program.

Chang Sau Sheong has programmed with Java for more than five years. He specializes in enterprise Java systems, especially portal software. Sau Sheong currently serves as chief architect/vice president at elipva, an e-commerce software company that produces the elipva Portal Application Framework (epaf), a J2EE (Java 2 Platform, Enterprise Edition)-based portal construction toolkit, and elipva Zephyr, a Java-based personal virtual assistant (PVA) with portal-like interfaces. Sau Sheong's writing credits include articles in Dr. Dobb's Journal and JavaWorld, as well as a contribution to IT2010: Beyond the Web Lifestyle, Chan Meng Khoong, editor (Prentice Hall, 1999; ISBN: 013017937X).

Learn more about this topic