Skip to content

New Java Features Now Supported in FreeMarker 3

Jonathan Revusky edited this page Nov 23, 2023 · 3 revisions

FreeMarker was mostly developed against quite old (from the current perspective) versions of Java. One goal of the FreeMarker 3 Revival is to address this. FreeMarker 3 now interoperates much more seamlessly with new (or not so new!) functional programming features introduced in JDK 8 nearly a decade ago.

Functional Interfaces

Basically, you can now expose a lambda (or equivalently, a method reference) to a FreeMarker template and then use it in the template with a natural syntax. Like so:

  dataModel.put("func", (Function<String,String>) s->"<b>"+s.toUpperCase()+"</b>");

In the template, you could now write:

  ${func("foo")}

And it will output:

  <b>FOO</b>

You could also have:

  static String func(String s) {return "<b>+s+"</b>";}

and put the method in your data model via:

  dataModel.put("func", Function<String,String> MyClass::func);

with the same effect.

Funky syntax, eh?

It's a funny thing. I say natural syntax above, but the fairly straightforward way this is all implemented actually does lead to some things that are a bit funky. For example, suppose you have a method that effectively returns a method (or function) like so:

public class Foo {
   public Function<String,String> vendAFunc() {return MyClass::func;}
}

and elsewhere, of course:

   dataModel.put("foo", new Foo());

In your template, you could write:

   ${foo::vendAFunc()("foo")}

You see, the vendAFunc() returns the function but then you need an additional pair of (...) to invoke the vended function! I have to admit that when I first looked at this, the result of my handiwork, I was a bit perturbed, since such syntax is quite jarring at first sight. Then it dawned on me that, in Java, you would have to write:

  foo.vendAFunc().apply("foo")

In this case apply is the name of the (single) method in the functional interface being used, java.util.function.Function<String,String>. Actually, in Java, you cannot just write:

  foo.vendAFunc()("foo")

That's not even valid syntax! But in FreeMarker (3), it is! Though you can also write:

  ${foo::vendAFunc()::apply("foo")}

But really, why would you? The point is that vendAFunc() is returning a lambda here and in FreeMarker, we can just call it directly with (...) without using the apply functional interface name, and thus, the funky syntax (from a Java perspective anyway) with the adjacent parentheses pairs.

Now, suppose you expose one of these functional objects to the template like so:

  dataModel.put("baz", (BiFunction<String,Integer,String>) (s,i)->""+i+". " + s);

And then in the template you could have:

  ${baz("Howdy",1024)}

which would output:

  1024. Howdy

Suppose you had a table to look up these routines:

 #var table = {"bar" : baz, "bat" : aMacro}

And elsewhere:

 #macro aMacro s, i
     ${i}. ${s}[#t]#-- We need this [#t] to get rid of the superfluous whitespace
 /#macro

and, as you can anticipate, both:

 ${table.bar("yes", 7)}

and:

 ${table.bat("yes", 7)}

would output the same thing, namely:

 7. yes

The point here is that in the lookup table, the lambda that comes from the Java layer and the FTL macro basically can be treated the same, you can put them in a function lookup table and they are referenced and used indistinctly. Well, maybe all this has a downside, as it empowers people to do excessively clever things. Well, regardless, these lambdas and functional interfaces have existed in Java for some time, so finally, it makes sense for an updated version of FreeMarker to know about them, no?

Records

The current FreeMarker 3 preview now "knows" about records (introduced as a preview in JDK 14 and as a stable feature in JDK 16) so you can expose a record to a FreeMarker template and it works with the natural syntax.

     record MyRecord(String name, int age) {}

     dataModel.put("rec", new MyRecord("Joe Blow", 33));

In the template, you can simply write:

     ${rec.name}

In earlier versions of FreeMarker you would have had to write:

     ${rec.name()}

This is basically because the older versions of FreeMarker assume that the getter method for the name property would be called getName, which is, after all, the entrenched convention. However, with the new records feature, Oracle (finally?) eschewed that tradition and decided to just use the property's name as the accessor method. So if an object is a record, FreeMarker 3 checks for a method with the same name as the property -- as well as getName() and an isName() that returns a boolean.

Well, this is really just a minor change that is warranted, at the very least, by the principle of least astonishment.