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 TemplateProcessor
s 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
EndInstruction
s (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
Expression
s; for example, And
does a logical AND
on two Expression
s. Any type casting required is performed by
using the ExpressionUtils
class.
The parser tokenizes an expression into a List
of
Expression
s. 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. Expression
s also include Identifier
s
NumberLiteral
s and StringLiteral
s, 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 CallInstruction
s 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. TemplateProcessor
s 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 EventObject
s or EventListener
s
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. ListenerAdapter
s 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.