2.3.33

Release date: 2024-06-01

Please note that with this version the minimum required Java version was increased from Java 7 to Java 8. Also for the few who rely on Servlet and/or JSP support, the minimum is now increased to Servlet 3.0, and JSP 2.2 (which are still very old versions from 2011).

Changes on the FTL side

  • FREEMARKER-183: If FreeMarker is configured like so, values in Java records can now be referred like obj.price, instead of like obj.price(). Furthermore, FreeMarker can now be configured to allow this for all 0-argument non-void methods. See more details in the Changes on the Java side section below.

  • GitHub PR 87 Comparing strings is now way faster, if the incompatible_improvements setting is at least 2.3.33. If your template does lot of string comparisons, this can mean very significant speedup. With this enabled, we use a simpler way of comparing strings, and because templates were only ever allowed equality comparisons between strings (not less-than, or greater-than), it's very unlikely to change the behavior of your templates. (Technically, what changes is that instead of using Java's localized Collator-s, we switch to a simple binary comparison after UNICODE NFKC normalization. So, in theory it's possible that for some locales two different but similarly looking characters were treated as equal by the collator, but will count as different now. But it's very unlikely that anyone wanted to depend on such fragile logic anyway. Note again that we still do UNICODE normalization, so combining characters won't break your comparison.)

  • When concatenating many sequences (like of Java List-s) with the + operator, the resulting sequence is now much less slow to read. (Background: the + operator, when applied on two sequences, produces a result sequence that just view, not a copy of the original sequences. Thus is very fast to add together long sequences. But, if you concatenate many, like hundreds, of sequences, reading the sequence back will become expensive. It was always like that, so it's still not recommended to concatenate more then a few tens of sequences. But if that recommendation is not kept, now the slowdown can be way less punishing.)

    • Iteration (like listing) is now reasonably fast, and need not be concerned about. The speed of fetching the next item is now mostly independent of the number of sequences concatenated, while earlier it has become slower as that number grew. By iteration we mean going through all the items, strictly in order, without using an index explicitly. Examples of iteration are <#list concatedSeq as ...>, and concatedSeq?join(', ').

    • Accessing items by index, like concatedSeq[i], is now much faster than before, but still can be slow if you had concatenated a lot of sequences. Item access speed is now roughly O(N), where N is number of concatenated sequences (not the number of items!), so traversing the whole sequence by index is O(N²).

    • In previous versions concatedSeq?size could cause stack overflow if it concatedSeq was concatenated together from thousands of sequences. Now the stack usage is constant and negligible.

  • FREEMARKER-216: Fixed IllegalAccessException-s on Java 16 and later, when calling a public method that was last overridden by a public class that's however JDK internal (see JEP 396). More generally, we avoid this error if a class comes from the package of a Java module that doesn't export that package to the module where FreeMarker resides, by looking for an accessible public method in a public class that the unaccessible one overrides (maybe indirectly). If there's no such method, then the method won't be visible (despite that both the method and the class is public).

  • FREEMARKER-219: The "truncate" family of built-ins, as in maybeLong?truncate(10, ''), if the terminator string is set to 0 length, now it will not add a space before the terminator string when the cut happened exactly after the end of a word. (Note that if you are using something like maybeLong?truncate_c(10, ''), then certainly what you really want is maybeLong[0 ..* 10], as that doesn't do trimming at the cut.)

  • GitHub PR 89: Added TemplateProcessingTracer mechanism, that can be used to monitor coverage, and performance inside templates as they are being processed. For example, you could construct a heat map for how often the different parts run, or finding the performance hot spots. (There can be other creative uses, like watching for a variable to have a certain value.) Use Environment.setTemplateProcessingTracer(TemplateProcessingTracer) to enable this kind of monitoring. (See the API docs for more.)

Changes on the Java side

  • FREEMARKER-183: Better support for Java records, if you set the incompatible_improvements setting to 2.3.33 or higher (or if you create your own ObjectWrapper, then set its incompatible_improvements, or just its recordZeroArgumentNonVoidMethodPolicy property to BOTH_METHOD_AND_PROPERTY_UNLESS_BEAN_PROPERTY_READ_METHOD). If in a Java record you have something like int price(), earlier you could only read the value in templates as obj.price(). With this improvement obj.price will do the same (and similarly, obj["price"](), and obj["price"] will do the same). This has always worked for JavaBeans properties, like int getPrice() could always be used in templates as obj.price, in additionally to as obj.getPrice(). Now this also works for Java records, as there we simply treat all methods that has 0 arguments, and non-void return type as if it was a JavaBean property read method. Except, here the name of the method is exactly the same as the name of the faked JavaBeans property (price), while with real JavaBeans the read method name typically would be getPrice, and the property name would be price (so we have two separate names). There are some strange technical tricks involved for the same name to be usable in both ways, but as far as most users care, it just works.

    Some more technical changes:

    • Added two new settings to BeansWrapper, and therefore DefaultObjectWrapper: recordZeroArgumentNonVoidMethodPolicy, and defaultZeroArgumentNonVoidMethodPolicy. Each has enum type freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy, that can be METHOD_ONLY, PROPERTY_ONLY_UNLESS_BEAN_PROPERTY_READ_METHOD, or BOTH_METHOD_AND_PROPERTY_UNLESS_BEAN_PROPERTY_READ_METHOD. Therefore:

      • Note that with defaultZeroArgumentNonVoidMethodPolicy you can set similar behavior to non-records. That is, you can call 0 argument non-void methods without (), if you want. It's only meant to be used for methods that are mere value readers, and has no side effect.

      • For records, you can enforce proper style with setting recordZeroArgumentNonVoidMethodPolicy to PROPERTY_ONLY_UNLESS_BEAN_PROPERTY_READ_METHOD. The default with incompatible_improvements 2.3.33 is more lenient, as there using () is allowed (for backward compatibility, and because people often just use the Java syntax).

    • Added new interface, freemarker.template.MethodCallAwareTemplateHashModel, which adds getBeforeMethodCall(String key). If you have something like obj.price() in a template, where obj (after wrapping) implements that interface, then getBeforeMethodCall("price") called instead of TemplateHashModel.get("price"). This is needed for ZeroArgumentNonVoidMethodPolicy.BOTH_METHOD_AND_PROPERTY_UNLESS_BEAN_PROPERTY_READ_METHOD to work.

    • Added GenericObjectModel, which extends StringModel with implementing MethodCallAwareTemplateHashModel, and has a more telling name. BeansWrapper, and therefore DefaultObjectWrapper now creates GenericObjectModel-s instead of StringModel-s. This is like so regardless of any setting, like regardless of incompatible_improvements.

    • You shouldn't override BeanModel.get(String) anymore, but BeanModel.get(String, boolean). If you have overridden get, then see in the Javadoc for more.

  • GitHub PR 88: Added a new possible value for the auto_escaping_policy configuration setting, force (Configuration.FORCE_AUTO_ESCAPING_POLICY). This policy is to always require auto-escaping, to avoid accidents where because of misconfiguration, or a mistake of the template author it's disabled. With this policy, using output formats that don't support escaping will not be allowed. Using built-ins, and directives that disable auto-escaping (like ?no_esc) will also be errors (on parse-time). Note that if markup (like HTML) comers from the data model, then with this policy you will have to ensure that they come as TemplateMarkupOutputModel-s (which won't be auto-escaped even with this policy), not as String-s, because the template authors can't disable escaping for the value anymore.

  • FREEMARKER-214, GitHub PR 90: Update JavaCC (used for generating the template parser) from 6.1.2 to 7.0.12, to avoid creating new LookAheadSuccess with stack trace instance for each FMParser instance.

  • FREEMARKER-218: Servlet, and JSP support classes now has a Jakarta variant in the new freemarker.ext.jakarta.jsp and freemarker.ext.jakarta.servlet packages. These are the slightly modified copies of freemarker.ext.servlet, freemarker.ext.jsp (which are also present in the same FreeMarker jar), that depend on the Jakarata API-s (jakarta.servlet, jakarta.servlet.jsp, jakarta.el, etc.), instead of the tradition javax Serlvet/JSP/EL API-s. There's also freemarker.ext.jakarta.servlet.WebappTemplateLoader, which is the Jakarta equivalent of freemarker.cache.WebappTemplateLoader. (Note that Jakarta packages/classes are not visible in the FreeMarker source code, as they are generated during build from the pre-Jakarta classes.)

  • When FreemarkerServlet was used with the TemplateExceptionHandler.DEBUG_HANDLER or HTML_DEBUG_HANDLER, on Jetty the response web page was possibly empty or partial, as the Jetty has abruptly closed the connection after we flushed the HTTP response with status code 200, and yet thrown an exception. Now in this situation we only log the exception, but don't throw it. (Production system should always use RETHROW_HANDLER, and this change has no effect there.)

  • When concatenating sequences (like Java List-s) with the + operation in templates, the resulting TemplateSequenceModel now also implements TemplateCollectionModelEx (with is similar to Java's Iterable). It's because using that is much more efficient then indexed access, if the sequence was concatenated together from a lot of sequences.

  • We don't generate RMIC stub classes for freemarker.debug.impl.Rmi*Impl classes anymore. Almost nobody uses that API, but if you do, you certainly already rely on dynamic stubs anyway. (The rmic tool used to generate the stub classes was removed from the JDK, starting with JDK 15.)

  • Configuration.getVersion().getBuildDate() will now always return null, as we don't store the build date anymore, to make the build reproducible. For same reason, META-INF/MANIFEST.FM now will not store any timestamps, nor information about the build environment.

  • Build changes (in case you build FreeMarker itself, not just depend on it):

    • FREEMARKER-204, GitHub PR 79: Switched from Ant, to Gradle. IDE setup now involves no lengthy manual adjustments, assuming it's fairly recent IDE with decent Gradle support. There's on build.properties to set up either, but you need to ensure that you have both JDK 8, and JDK 16m and that Gradle finds them. See the README.md for more instructions. (Gradle itself is self-installing, of course.)

    • JavaDoc is now generated with JDK 16 (so now we have search on it)

    • The build is now reproducible (see reproducible-builds.org). However, therefore it doesn't contain any build timestamps anymore.

  • Minimum requirements were increased:

    • Java 8 (or higher)

    • If Servlet-related features are used, Servlet 3.0 (or higher)

    • If JSP-related features are used, JSP 2.2 (or higher)