Templates are compiled into a tree structure in which child nodes are stored in linked
lists (TemplateLinkedList objects, which contain TemplateLink objects).
The Template object is the root node.
Each TemplateLink contains a
"statement" object to which it delegates its processing tasks. A TemplateLink
object's child lists are encapsulated within its statement object. For example, a
ListInstruction has one child list representing its loop body; an
IfInstruction has two, one for the clause following the "if", and one for the
"else" clause.
All the abovementioned classes implement the interface TemplateProcessor,
meaning that they have a process() method that sends output to a
PrintWriter. When the Template's process() method is called,
it calls process() on its TemplateLinkedList, which calls
process() on each of its links. Each link calls process() on its
statement object, which in turn calls process() on any child lists it owns.
The only classes that know about the linked-list structure are
TemplateLinkedList, TemplateLink, 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 subclass 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
abstract class that can be subclassed 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 subclass 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 IfInstruction and at most two EndInstructions (one of type
IF_END, and one of type ELSE); only the IfInstruction ends up
in the compiled structure. The IfInstruction 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 IfInstruction, before inserting the
IfInstruction into a TemplateLink.
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 a
RuntimeStructuralElement. (LinkedListTemplateBuilder uses
TemplateLinks as its RuntimeStructuralElements.) 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 build 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
boolean, string, or TemplateModel. It may do this by owning other
Expressions; for example, And does a logical AND on two
Expressions.
The parser tokenizes an expression into a List of ExpressionElements.
Some of these may simply be tokens, such as OpenParen and CloseParen
objects, to be be used and eliminated by ExpressionBuilder. Others may be
Expression objects representing operators; ExpressionBuilder will
initalize these by passing them their operands. ExpressionElements also include
Identifiers and StringLiterals, which are simply given as operands to
the operators.
ExpressionBuilder first groups any parenthetical sub-expressions into
sub-Lists. It then makes repeated passes through the list to associate operators
with their operands at each level of operator precedence, recursively building
sub-expressions as it goes along. The result is a single Expression.
If you want to add another operator, first make an operator class that implements
Binary or Unary (and perhaps extends BooleanExpression or
BinaryBoolean). If the operator is only one character long, you can just add a
line to the parser's makeElement 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 abstract class for expression objects whose string value is
determined by taking the return value of their getAsTemplateModel() method, and
trying to read it as a scalar. Subclasses are Identifier (which can look in the
root of the data model for its value), Dot (a binary operator that uses its 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 stored either in the Template or
in the data model. 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 builder, which returns
the exception's message in a TemplateTextBlock. The various
TemplateProcessors also output exception messages as HTML comments at run time.
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.
Please write to Benjamin Geer at bgeer@freemarker.org if you have questions or suggestions.