FreshMarker – compile it! (2)

“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)”

Leave a Comment