“Speed, quality, price. Pick any two.”
James M. Wallace
In the first article on static FreshMarker templates, some basic thoughts were put on paper. This article is intended to set a first milestone on the way to supporting much faster templates than before. As a nice side effect of this initial work, a pretty printer for FreshMarker templates will be created.
There are various ways to create a Java source code from a Template
instance. To reach our goal, we have to choose one of them. So that the development of dynamic and static templates is not too far apart, we choose a way that keeps both implementations together for as long as possible. Which means, we create a static template from the dynamic template.
The approach is shown here as a small diagram. The FreshMarker Engine generates a Template
data structure from the template file. The code generator reads the data structure and converts it into Java source code. This process makes it possible to use the entire existing infrastructure, from loading the template file to the plugin mechanism, the template reduction and FreshMarker extensions, without duplicating any code.
The code generator will be made available in a separate library in the future. Java classes can then be generated via the command line or a Maven plugin. The main reason for a separate library is the additional code that is unnecessary for the use of dynamic templates.
In order for the code generator to be able to generate Java source code from the template data structure, it must be able to access the data. If you read the blog frequently, you may already have an idea of how this is solved. As so often, the magic word is the Visitor Pattern.
The Visitor Pattern makes it possible to execute new operations on a group of objects without changing the classes of these objects. It separates the algorithms from the object structures on which they operate by defining a Visitor
interface that provides a visit
method for each class of the object structure type.
The Visitor Pattern is already used in FreshMarker to generate the template from the CongoCC parse tree. Visitors are now required for the Fragment
and TemplateObject
implementations. The TemplateVisitor
, which is used for Fragment
implementations, has visit
methods for all of them.
public interface TemplateVisitor<R> { default R visit(BlockFragment fragment, List<Fragment> fragments) { return null; } default R visit(ConditionalFragment fragment, TemplateObject conditional, Fragment content) { return null; } default R visit(ConstantFragment fragment, String value) { return null; } // ... }
Each method in the interface is a default method with the generic type R
as the return value. Classes that implement the TemplateVisitor
interface therefore do not have to control the entire processing via their internal state. The Fragment
implementations in turn implement the accept
method specified by the Fragment
interface.
public class BlockFragment implements Fragment { // ... @Override public <R> R accept(TemplateVisitor<R> visitor) { return visitor.visit(this, fragments); } }
In the accept
method, the Fragment
implementations then call the visit
methods intended for them. In this example, the BlockFragment
class calls the visit
method, to which it also passes the list of its sub-fragments. This is necessary in this case because the list of sub-fragments cannot be accessed from outside.
The next code shows how this all comes together in the TemplatePrinter
class. It implements the visit
methods from the TemplateVisitor
interface. In addition, it uses an ExpressionPrinter
instance as an implementation of the TemplateObjectVisitor
to evaluate expressions.
public class TemplatePrinter implements TemplateVisitor<String> { private final ExpressionPrinter expressionPrinter = new ExpressionPrinter(); @Override public String visit(BlockFragment fragment, List<Fragment> fragments) { return fragments.stream().map(f -> f.accept(this)).collect(Collectors.joining("\n")); } @Override public String visit(ConstantFragment fragment, String value) { return value; } @Override public String visit(InterpolationFragment fragment, TemplateMarkup expression) { return "${" + expression.accept(expressionPrinter) + "}"; } // ... }
The first visit method on the BlockFragment
runs through the fragments
list and calls the accept
method on each one. These return the respective String
representation. At the end, these String
representations are joined together and used as the return value of the method.
The second visit
method on the ConstantFragment
returns the String
representation of the constant parts of the template. The implementation is trivial, it returns the String
passed by the ConstantFragment
.
The third visit
method is again a little more complicated, because for the String
representation of the InterpolationFragment
, the expression
must first be converted into a String
using the ExpressionPrinter
.
If all visit
methods are implemented, the String
representation of a template can be generated from a Template
instance.
TemplatePrinter printer = new TemplatePrinter(); String signature = "${firstName} ${lastName}, ${company}, ${firstName}.${lastName}@${company}.de"; Template template = builder.getTemplate("signature", signature); Template reduced = template.reduce(Map.of("firstName", "Jens", "lastName", "Kaiser")); assertEquals(signature, template.getRootFragment().accept(printer)); assertEquals("Jens Kaiser, ${company}, Jens.Kaiser@${company}.de", reduced.getRootFragment().accept(printer));
In this example, a Template
instance is created from a signature template. If the TemplatePrinter
is applied to the root element of the Template, its accept
method returns a description that corresponds to the signature template. However, even more is possible with those implemented so far. Let’s apply the Template Reduction to the Template
by replacing the firstName
and lastName
. Then we get a template description in which only the company
is variable.
In the next article on the Freshmarker compiler, we will look at why handling switch statements is not so easy and how to exchange Unicode and ASCII operators.
1 thought on “FreshMarker – compile it! (2)”