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