First of all, here is an instant gratification download link: freemarker3.jar. I guess that can be considered a preview version of a long-awaited FreeMarker 3.x release. For most typical usage, it is (at least for the moment) a drop-in replacement of older versions of FreeMarker. Actually, this is the same FreeMarker version that is contained in the kitchen-sink congocc.jar.
(Note that since 26 June 2024, all of the packages in FreeMarker 3 are freemarker3.* instead of freemarker.*. This is so that FreeMarker 3 can co-exist in the same app or container with older versions of FreeMarker.)
One important caveat is that your existing templates will not work unless you put the line:
[#ftl legacy_syntax]
at the top of them. Though (and this is foreshadowing...) you can now write that as:
#ftl legacy_syntax
without the square (or pointy) brackets. The strict variable definition can also be turned off globally via:
Configuration.getDefaultConfiguration().setLegacySyntax(true);
So, here is what this is about:
Here are some noteworthy new developments in this version of FreeMarker...
New Terse Syntax
In this latest version of FreeMarker, there is a terse syntax that can be used optionally. As long as a built-in directive (such as #if
, or #list
etc.) starts on its own line, there is no need for the pointy (or square bracket delimiters). Thus,
[#if foo = bar]
Foo is bar.
[#else]
Foo is not bar.
[/#if]
can now be written (if you want):
#if foo = bar
Foo is bar.
#else
Foo is not bar.
#endif
Note that this syntax only works when the directive is the first thing on a line (excluding whitespace). In that case, you enter a mode in which the next end-of-line is taken to be a closing delimiter. So, while you can still write:
[#assign x = 1, y = 2]
it seems like a cinch bet that most people will prefer to write:
#assign x = 1, y =2
You can also write the above on multiple lines, i.e.
#assign x = 1,
y = 2
This is because certain delimiter tokens (the comma among them) suppress the interpretation of the following newline as closing the directive. So, one aspect of this is that the above does not work without the comma. Specifically, if you write:
#assign x = 1
y = 2
that would be as if you wrote, in the older, more verbose syntax:
[#assign x = 1]
y = 2
You see, without the comma, the y = 2
line is parsed as being just regular text to output, which is probably not one's intention in this spot, but the parser can hardly be expected to read your mind! Without the trailing comma, in the terse syntax, the newline character that follows is taken to terminate the assignment instruction. I would admit that does mean that certain terse constructs are harder to read than their fully delimited traditional equivalents. However, on consideration, the older, more verbose syntax is still available, so you can write these things however you think they are more clear. In an earlier implementation of all this (just from a few days ago) it was more strict and the terse directive had to be written on a single line, so you would have to write:
#assign x = 1
#assign x = 2
And you can still write that. Of course, in Java, you can write:
int x = 1, y = 2;
or:
int x = 1;
int y = 2;
I learned a few months ago that the first, shorter form is considered bad style by some style checkers (that presumably enforce whatever style guidelines.)
The #assign and #local and #global directives are actually deprecated in favor of #set and #var
Though the last example I gave used #assign
, that is actually not now the preferred way of declaring/setting a variable. This version of FreeMarker introduces the alternatives (superior, at least in my opinion) of #var
and #set
. This has actually been implemented since 2008 or even 2007. However, it was never in any widely used version of FreeMarker. (However, the templates within CongoCC exclusively use #var/#set
)
What #set
and #var
do is create a clear conceptual distinction between declaring a variable and setting its value. Admittedly, this is more typical of real programming languages, but I think it has value in this kind of templating language. Has it ever occurred to you how fragile shell (or .bat) scripts tend to be? For example, I suppose most people reading this are Java hackers. Suppose you write some script to launch a Java app, with something like:
CLASPATH="~/myjavalibs":$CLASSPATH"
What happens in the above is that CLASPATH
is misspelled, so instead of redefining the existing CLASSPATH
variable, prepending ~/myjavalibs
to it, this line declares a new variable called CLASPATH
. And then the script doesn't work, of course, but it can be not terribly obvious why.
The same problem exists in FreeMarker templates. Suppose you have an existing variable called name
but you write:
[#assign Name = client.name]
and, rather than redefining the existing name
variable, it creates a new variable called Name
and sets that. Using #var/#set
you need to declare the variable with the #var
directive before using it. So, suppose you define a macro like so:
#macro myMacro
#local name = client.name
...
/#macro
In the above, there is effectively a local variable called name
declared and set. The preferred way to express this now is:
#var name
#set name = client.name
So if you misspell the variable, you will get an error. So, if you later write:
#set Name = "Mr. " + client.name
you will hit a clear error since the variable name that was declared was name
, not Name
. With the older disposition, the above error just passes through. But now it gets caught.
Note also that the lines:
#var name
#set name = client.name
can be written more tersely (and this is surely how most people would write it!) as:
#var name = client.name
So, in fact, using the newer #var/#set
is not more verbose than the older #global/#local
. (Quite the contrary, since #var
and #set
are shorter than #assign
or #local
! And, of course, in conjunction with the bracket-free syntax this tends to make templates in the newer style significantly shorter.)
When you declare a variable, it is taken to exist in the block where it is defined. So, if you have:
#if someCondition
#var something = foo.bar
...
${something}
#endif
${something}
In the above, the final ${something}
will be an error since that variable only exists in the block in which it is defined using #var
. So the above will blow up except on the off chance that this scope (the one that contains the if block) contains a something
variable. So the bottom line is that you typically define a variable only as locally as it is needed, so there is much less unintentional clobbering of existing variables via name clashes. Well... more like in a real programming language!
Using a macro as if it was a method.
Consider a simple little macro:
[#macro hello name]Hello, ${name}![/#macro]
If you wanted to capture the output of the macro and store it in a variable, you could do something like:
[#assign hellovar][@hello client.name/][/#assign]
Now, you can write equivalently:
#assign hellovar = hello(client.name)
(Though, again, it is preferable to use the newer #var/#set
)
By the way, this bit of syntactic sugar has been available since July 2020 in my local version, i.e. the one use in Congo development. I implemented this for use in CongoCC (JavaCC 21 at the time) internal development. In any case, I think it corresponds to the principle of least surprise. The attempt to use a macro invocation as if it was a method/function just leads to the macro being invoked and the output being captured. And, generally speaking, it looks preferable to write:
${someMethod(hello(someName))}
rather than:
[#assign intermediateVar][@hello someName/][/#assign]
${someMethod(intermediateVar)}
Some Caveats
One thing to be aware of is that this version of FreeMarker is somewhat stripped down. I was very concerned with reducing the code line count, so I stripped out things that I think have fairly little usage. For example, older versions of FreeMarker had support for using FreeMarker in conjunction with some alternative JVM based languages, specifically Jython (Python on Java), Rhino (Javascript on Java), and JRuby (Ruby on Java). There is little sign that any of that was being used. So I decided that it was not a good tradeoff to carry that stuff forward when doing all this massive refactoring work.
Well, I don't think that stuff is very widely used at all. Actually, the component that was stripped out that might affect the most people is the built-in XML processing support. This may well be put back in at a later point. (Or not.) But do note that the only thing I have to go on is whatever feedback I receive, so if the XML support is important to you, by all means, bring it up!
There is a host of built-ins that were added to Apache FreeMarker over the last decade. I have no idea how widely used the newer built-ins are. The ones that seem to be generally useful will get added in, most likely. But, for the moment, it is quite likely that existing templates will use built-ins that are not present. If you think a built-in should be added, just bring it up on the Congo discussion forum.
Or, generally speaking, if there is some feature that you would consider desirable, by all means bring it up. There is actually a reasonable chance that something you suggest could be implemented. That is certainly not the case for Apache FreeMarker.