Cernunnos Manual


Anatomy of a Task

The capabilities of Cernunnos come in tidy packages: tasks and phrases. The Cernunnos Runtime Environment does the magic of assembling these components into useful bundles of software based on your specifications. On a basic level, you don't have to know how these capabilities work to use them; you simply tell Cernunnos what you need.

Nevertheless, to get the most from Cernunnos it's better to be able to look below the surface.

Most java classes in Cernunnos implement one or the other of the these two core interfaces: Task or Phrase.

FooTask.java

In order to get to know how tasks work, we will look at a made-up task called FooTask ( view complete Java code). This task executes a foo operation, which we loosely define with the following requirements:

Getting Started

The FooTask resides in the org.danann.cernunnos.foo package.

package org.danann.cernunnos.foo; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.danann.cernunnos.AbstractContainerTask; import org.danann.cernunnos.EntityConfig; import org.danann.cernunnos.Formula; import org.danann.cernunnos.LiteralPhrase; import org.danann.cernunnos.Phrase; import org.danann.cernunnos.Reagent; import org.danann.cernunnos.ReagentType; import org.danann.cernunnos.SimpleFormula; import org.danann.cernunnos.SimpleReagent; import org.danann.cernunnos.TaskRequest; import org.danann.cernunnos.TaskResponse;

For our FooTask, note that all other Cernunnos types we import come from the same package: org.danann.cernunnos. This is the root package of Cernunnos and the parent of the foopackage. This relationship illustrates an important principal of dependency organization in Cernunnos: import statements may only go up the package tree . Java classes in Cernunnos may never import down, such as from a.b.c to a.b.c.d, or laterally, such as from a.b.c.d to a.b.c.e.

Class Declaration

Every task must implement the Task interface either directly or indirectly, such as by subclassing AbstractContainerTask.

public final class FooTask extends AbstractContainerTask {

The FooTask extends AbstractContainerTask, which means it supports subtasks. It's not mandatory for tasks that support subtasks to subclass AbstractContainerTask, but it makes the process easier.

Our FooTask is final because it's not designed for inheritance. Many Java engineers feel that this is a good practice generally: make classes either abstract or final, depending if they're designed to be subclassed or not, respectively.

Phrases & Reagents

The FooTask has three member variables, which are private, and two class variables, which are public.

// Instance Members. private Phrase attribute_name; private Phrase foo; private final Log log = LogFactory.getLog(getClass()); /* * Public API. */ public static final Reagent ATTRIBUTE_NAME = new SimpleReagent("ATTRIBUTE_NAME", "@attribute-name", ReagentType.PHRASE, String.class, "Optional name under which the value of FOO will be " + "registered as a request attribute. If omitted, the name 'FooAttributes.FOO' will be " + "used.", new LiteralPhrase(FooAttributes.FOO)); public static final Reagent FOO = new SimpleReagent("FOO", "@foo", ReagentType.PHRASE, String.class, "The foo object that will be available to subtasks as the request attribute specified by " + "ATTRIBUTE_NAME.");

Observe how the ' attribute_name' and ' foo' variables correspond to class variables with similar names: each pairing works in concert to support a single concept. The member variables are instances of Phrase; the class variables are instances of Reagent. The FooTask uses reagents to tell the Cernunnos Runtime about the information it will need to do it's work. It uses phrases to get that information when the time is right.

Both reagents include a name, an XPath expression, a reagent type, an expected return type, and a description -- in that order. The ATTRIBUTE_NAME reagent also specifies a default value. Defaults should be provided where possible and sensible, since they allow authors to accept them tacitly in the majority of cases.

The default value of the ATTRIBUTE_NAME reagent uses a special class: FooAttributes. Cernunnos uses Java classes that end in ' -Attributes' to define standard names for common request attributes. Whenever two (or more) tasks and/or phrases use the same, standard name for something by default, they establish a pattern of cooperation that is implied or tacit: as long as neither default is overridden, they cooperate automatically. See the Java code for this class.

The Cernunnos Manual documents the information specified by reagents in the task and phrase reference; the name and description are provided purely for this purpose. The Cernunnos Runtime uses the XPath expression to analyze the value of the reagent, and the reagent type determines how that value will be calculated. Currently, the return type just acts as a hint, signaling when the author may have made a mistake (allowing Cernunnos to issue a warning). We hope that return types can help Cernunnos provide support for type coercion in the future.

The getFormula() Method

Tasks and phrases both implement the Bootstrappable interface, which declares two methods. The first of these is getFormula().

public Formula getFormula() { Reagent[] reagents = new Reagent[] {ATTRIBUTE_NAME, FOO, AbstractContainerTask.SUBTASKS}; final Formula rslt = new SimpleFormula(getClass(), reagents); return rslt; }

As you might expect, the getFormula() method provides a Formula object to the Cernunnos Runtime. The formula of a task (or phrase) describes its essential metadata: e.g. what Java class implements it, what reagents it uses, and whether it's been deprecated. The information in the formula tells Cernunnos how to bootstrap the FooTask properly and how to document it accurately in the Cernunnos Manual.

The init() Method

The second method that Bootstrappable defines is init().

public void init(EntityConfig config) { super.init(config); // Instance Members. this.attribute_name = (Phrase) config.getValue(ATTRIBUTE_NAME); this.foo = (Phrase) config.getValue(FOO); }

The Cernunnos Runtime calls this method on every task (or phrase) before putting it into service. The init() method will be called exactly once.

Task implementations use the init() method to prepare themselves for service. The Cernunnos Runtime passes an EntityConfig object, which provides access to some essential resources. Perhaps the most important of these resources are the values of reagents declared by the formula. In general, task implementations will "link up" their reagents with member variables in the init() method.

The FooTask connects two member variables in this way: ' attribute_name' and ' foo.' Both of these variables are references of type Phrase. Reagents usually work with phrases, though there are some exceptions. FooTask also calls super.init(), allowing AbstractContainerTask to bootstrap its reagents as well.

The perform() Method

All Task implementations work their magic in the perform() method. Unlike getFormula() and init() above, perform() applies only to tasks; phrases implement a different (but similar) method called evaluate().

public void perform(TaskRequest req, TaskResponse res) { Object f = null; try { final String name = (String) attribute_name.evaluate(req, res); f = foo.evaluate(req, res); res.setAttribute(name, f); if (log.isInfoEnabled()) { log.info("The FooTask performed the foo operation " + "successfully. The 'foo' object is: " + f); } } catch (Throwable t) { String msg = "The FooTask failed to foo properly. The 'foo' " + "object is: " + f; throw new RuntimeException(msg, t); } super.performSubtasks(req, res); }

The Cernunnos Runtime provides a TaskRequest and a TaskResponse; these are similar to request/response objects in Java Servlets, Java Portlets, and numerous other container-managed APIs both within the Java Platform and elsewhere. The request provides inputs to the task, most notably request attributes. The response gives tasks somewhere to direct their outputs, which, incidentally, are often request attributes for subtasks.

The FooTask first invokes evaluate() on its phrases: ' attribute_name' and ' foo.' Notice that evaluate() also uses the request and response; phrases get their inputs in the same manner as tasks (though their outputs are handled differently). FooTask does not know the actual value of the items its phrases represent until they are evaluated in the perform() method. These values depend on contextual circumstances ( e.g. the work of other tasks and phrases), and can be different each time the task is invoked.

All runtime-managed objects in Cernunnos ( viz. both tasks and phrases) are obligated not to maintain stateful information between calls to perform()/ evaluate(); this kind of information must be maintained in the request and response objects. Cernunnos objects are therefore both reusable and thread-safe. Instances of TaskRequest and TaskResponse are neither reusable nor thread-safe.

With the real value of the ' foo' phrase in hand, the FooTask uses the response to create a new request attribute under the name specified by ' attribute_name.' This attribute will be visible to all subtasks. This is the sole purpose of FooTask; from here, it only has to invoke its subtasks.

Now that its own work is done, the FooTask takes a moment to log the outcome of its efforts. Cernunnos uses the Apache Commons Logging package, which is a de facto industry standard. Information about components operating normally ( i.e. without exceptions or surprises) is logged at the TRACE level.

If, on the other hand, an exception does arise while FooTask does its work, it will be caught and execution will pass into the catch block. In this case, our task will wrap whatever it catches in a RuntimeException with a message describing everything known about the foo operation at the time of failure. Under the hood, the Cernunnos Runtime wraps this exception with a ManagedException, which provides helpful information about the source of the error within the Cernunnos XML document that contains our FooTask.

Lastly the FooTask invokes its subtasks with super.performSubtasks(). Notice that our task calls this method outside the try/catch block in which it does its own work. This practice prevents the stack trace from swelling with information that's not especially useful and may even be misleading, and is emerging as the recommended pattern for tasks that support subtasks.