Notes on the FM-Classic Source Code

The Compiled Form of Templates

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.

Compilation

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.

Expressions

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.

Functions and Includes

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.

Exceptions

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.

GenericEventMulticaster

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.