Templates are compiled into a tree structure in which child nodes are
stored in arrays (TemplateArrayList objects, which contain
TemplateProcessor objects). The Template object is
the root node.
Each object in a
TemplateArrayList is a "statement" object to which performs a
processing task. A TemplateProcessor object may contain
child lists are encapsulated within its statement object. For example, a
ListInstruction has one child list representing its loop body; an
IfElseInstruction can have two or more, one for the clause
following the "if", and many for the "elseif" and "else" clauses.
All the abovementioned classes implement the interface
TemplateProcessor, meaning that they have a process()
method that sends output to a Writer. When the
Template's process() method is called, it calls
process() on its TemplateArrayList, which calls
process() on each of its statement object, which in turn calls
process() on any child lists it owns.
The only classes that know about the array-list structure are
TemplateArrayList and LinkedListTemplateBuilder.
All the other classes that compose the compiled structure know only that they
have been given TemplateProcessors to use at run
time. The only class that knows about LinkedListTemplateBuilder
is Template; if you wanted to substitute a different kind of
compiled structure, you could implement TemplateBuilder, then
subclass Template, overriding only its
compileText() method where it instantiates the builder. You'll
probably also want to modify whatever TemplateCache you're using
so that it instantiates your subclass of Template.
The builder's constructor takes a TemplateParser as an
argument. This is an interface that can be implemented to handle
different sorts of template syntax. Currently, Template's
compileText method instantiates a
StandardTemplateParser and passes it to the builder. To change
the template syntax, you could implement TemplateParser, then
subclass Template, overriding its compileText()
method to instantiate your parser instead of the standard one.
The parser returns one Instruction object at a time, under
the control of the builder, which builds the compiled structure in one pass
using recursive descent. Some of the objects returned by the parser are
instances of EndInstruction; these are simply tokens. Others
are functional objects which the builder initializes and inserts into the
compiled structure. For example, when parsing an if-else structure, the
parser returns an IfElseInstruction and one or more
EndInstructions (one of type IF_END, and any number
of type ELSEIF); only the IfElseInstruction ends up
in the compiled structure. The IfElseInstruction encapsulates
the "else" part of the structure as one of its child lists. The builder must
recursively generate these child lists and use them to initialize the
IfElseInstruction, before inserting the
IfElseInstruction into a TemplateArrayList.
Method overloading is used to identify the subclass of each
Instruction so as to determine how it needs to be built.
TemplateBuilder declares various buildStatement()
methods for building subclasses of Instruction and inserting
them into an ExpressionElement.
Instruction declares a callBuilder() method, which
allows the builder to have an Instruction call it back to
identify itself; subclasses of Instruction implement this method
as a call to the builder's overloaded buildStatement()
method.
There is a separate building mechanism for expressions. The executable
form of an expression is a tree. The parser is responsible for tokenizing
an expression; it then uses the static buildExpression method in
ExpressionBuilder to build an Expression object
(the root node of the expression tree) that can be used at run time. An
Expression is essentially an object that can determine its value
as a TemplateModel. It may do this by owning other
Expressions; for example, And does a logical AND
on two Expressions. Any type casting required is performed by
using the ExpressionUtils class.
The parser tokenizes an expression into a List of
Expressions. Some of these may simply be sub-expressions, build
recursively whenever a parenthesized expression is encountered. Others may be
Expression objects representing operators;
ExpressionBuilder will initalize these by passing them their
operands. Expressions also include Identifiers
NumberLiterals and StringLiterals, which are simply
given as operands to the operators.
ExpressionBuilder makes repeated passes through the list
in precedence order, associating operators with their operands at each level of
operator precedence. The result is a single Expression.
If you want to add another operator, first make an operator class that
implements Binary or Unary (and probably extends
AbstractBinary). If the operator is only one character long,
you can just add few lines to the parser's parseBinaryElement
method to parse it; if it's two characters long, have the parser's
init() method store another anonymous inner class in
longOpMap to parse it. Then add the operator's class to the
static array of arrays of operator classes in ExpressionBuilder,
making sure it's in the right place for its precedence level.
Variable is an interface for Expression objects
whose string value is determined by taking the return value of their
getAsTemplateModel() method.
Implementations are Identifier (the most trivial of variables)
Dot (a unary postfix operator that's passed the right child's
name as a key in the hash pointed to by its left child),
DynamicKeyName (a unary postfix operator that's passed an
expression by the parser, and uses that expression's string value as a key
in the hash pointed to by its operand), and MethodCall (a unary
postfix operator that expects its operand to be a
TemplateMethodModel).
The index variable of a list structure is stored in
TemplateModelRoot while the list is being processed, temporarily
hiding any existing variable with the same name. The same goes for arguments
to functions.
The functions defined in a template are stored at compile time in a
HashMap member of the Template object. This is
desirable for various reasons (e.g. because it makes it possible for two
functions to call each other recursively), but it poses a problem: an
include statement can make additional functions available at
run time, but Template objects are immutable. The current
implementation gets around this problem by allowing functions to be copied
from the Template into the data model at run time. When an
include statement is processed, it retrieves all the functions
from the including template and from the included template, wraps each one in
a special TemplateModel wrapper called FunctionModel,
and puts references to these objects into the root node of the data model,
where they can be found by CallInstructions executed in either
template.
TemplateModelException is thrown only by classes implementing
TemplateModel, and indicates a run-time error.
TemplateException is thrown by classes in the template package,
to indicate either a run-time or a compile-time error. If a compile-time
exception occurs, it bubbles up to the Template, where it is
stored and output at run time. TemplateProcessors also generate
exception messages, which by default are output as HTML comments at run
time. A TemplateExceptionListener may be used to listen for
these exceptions, and output them to other components, such as an event
log.
The package includes a GenericEventMulticaster class that may
be generally useful. Unlike AWTEventMulticaster, it doesn't
know about any of the EventObjects or EventListeners
that it might be used with. When you want to fire an event, you call the
GenericEventMulticaster's fireEvent() method,
passing it the EventObject you want to fire, and a
ListenerAdapter that knows which method to call on each of the
listeners. ListenerAdapters can be implemented as anonymous
inner classes, as in FileTemplateCache. The event listener
methods conform to the JavaBeans naming conventions, including the changes
introduced with JDK 1.4.
Please write to Benjamin Geer at beroul@users.sourceforge.net if you have questions or suggestions.