5. Adding Commands to Jess
The Java interface jess.Userfunction represents a single
function in the Jess language. You can add new functions to the Jess
language simply by writing a class that implements the
jess.Userfunction interface
(see below for details on how this is done), creating a single instance of
this class and installing it into a jess.Rete object using
Rete.addUserfunction(). The Userfunction classes you write can maintain
state; therefore a Userfunction can cache
results across invocations, maintain complex data structures, or keep
references to external Java objects for callbacks. A single
Userfunction can be a gateway into a complex Java subsystem.
5.1. Writing Extensions
I've made it as easy as possible to add user-defined functions to Jess.
There is no system type-checking on arguments, so you don't need to tell
the system about your arguments, and values are self-describing, so you
don't need to tell the system what type you return. You do, however, need
to understand several Jess classes, including jess.Value,
jess.Context, and jess.Funcall, as previously
discussed in the chapter
Introduction to Programming with Jess in Java.
To implement the jess.Userfunction interface, you need to implement
only two methods: getName() and call(). Here's an example
of a class called 'MyUpcase' that implements the Jess function my-upcase,
which expects a String as an argument, and returns the string in uppercase.
import jess.*;
public class MyUpcase implements Userfunction
{
// The name method returns the name by which the function will appear in Jess code.
public String getName() { return "my-upcase"; }
public Value call(ValueVector vv, Context context) throws JessException
{
return new Value(vv.get(1).stringValue(context).toUpperCase(), RU.STRING);
}
}
The call() method does the business of your Userfunction. When
call() is invoked, the first argument will be a ValueVector
representation of the Jess code that evoked your function. For example,
if the following Jess function call was made,
(my-upcase "foo")
the first argument to call() would be a ValueVector of
length two. The first element would be a Value containing the
symbol (type RU.ATOM) my-upcase, and the second argument
would be a Value containing the string (RU.STRING)
"foo".
Note that we use vv.get(1).stringValue(context) to get the first argument
to my-upcase as a Java String. If the argument doesn't contain
a string, or something convertible to a string, stringValue()
will throw a JessException describing the problem; hence you don't need to worry
about incorrect argument types if you don't want to. vv.get(0)
will always return the symbol my-upcase, the name of the function
being called (the clever programmer will note that this would let you construct
multiple objects of the same class, implementing different functions based
on the name of the function passed in as a constructor argument). If
you want, you can check how many
arguments your function was called with and throw a JessException if it
was the wrong number by using the vv.size() method. In any case,
our simple implementation extracts a single argument and uses the Java
toUpperCase() method to do its work. call() must wrap
its return value in a jess.Value object, specifying the type (here
it is RU.STRING).
Having written this class, you can then, in your Java main program, simply
call Rete.addUserfunction() with an instance of your new class
as an argument, and the function will be available from Jess code. So,
we could have
// Add the 'my-upcase' command to Jess
Rete r = new Rete();
r.addUserfunction(new MyUpcase());
// This will print "FOO".
r.executeCommand("(printout t (my-upcase foo) crlf)");
Alternatively, the Jess language command load-function could
be used to load my-upcase from Jess:
(load-function MyUpcase)
; prints "FOO"
(printout t (my-upcase foo) crlf)
5.2. Writing Extension Packages
The jess.Userpackage interface is a handy way to group a collection
of Userfunctions together, so that you don't need to install them
one by one (all of the extensions shipped with Jess are included in Userpackage
classes). A Userpackage class should supply the one method add(),
which should simply add a collection of Userfunctions to a
Rete object using
addUserfunction(). Nothing mysterious going on, but it's very
convenient. As an example, suppose MyUpcase was only one of a
number of similar functions you wrote. You could put them in a
Userpackageclass like this:
public class MyStringFunctions implements Userpackage
{
public void add(Rete engine)
{
engine.addUserfunction(new MyUpcase());
engine.addUserfunction(new MyDowncase());
engine.addUserfunction(new MyTitlecase());
}
}
Now in your Java code, you can call
r.addUserpackage(new MyStringFunctions());
or from your Jess code, you can call
(load-package MyStringFunctions)
to load these functions in. After either of these snippets, Jess
language code could call my-upcase, my-downcase, etc.
Userpackages are a great place to assemble a collection of
interrelated functions which potentially can share data or maintain references
to other function objects. You can also use Userpackages to make
sure that your Userfunctions are constructed with the correct
constructor arguments.
All of Jess's "built-in" functions are simply Userfunctions,
albeit ones which have special access to Jess' innards, as well as
being automatically loaded by code in the jess.Funcall
class. You can use these as examples for writing your own Jess extensions.
5.3. Obtaining References to Userfunction Objects
Occasionally it is useful to be able to obtain a reference to an installed
Userfunction object. The method Userfunction Rete.findUserfunction(String
name) lets you do this easily. It returns the Userfunction
object registered under the given name, or null if there is none. This
is useful when you write Userfunctions which themselves maintain state
of some kind, and you need access to that state.