FreeMarker & Velocity Views
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Struts 7.4.0 (released 2026-10-02) running on Apache Tomcat 11.0.26 (Jakarta Servlet 6.1, Jakarta
Pages 4.0) with Java 21, FreeMarker 2.3.35 (the library struts2-core 7.4.0 depends on) and, for the Velocity half, struts2-velocity-plugin
7.4.0 with Velocity Engine 2.4.1. Everything was verified on 2026-10-08 and re-run on 2026-10-10: the facts against the Struts
documentation and the 7.4.0 sources and jars, and every example by building it into a Maven WAR with the struts2-bom, deploying it on
Tomcat 11.0.26 and calling each URL with curl; the outputs shown are what that run produced (session ids and blank lines are left
out). JSP is not the only way to turn an action into a page. Struts ships a second view technology in its core, FreeMarker, which the framework
itself uses to render every UI tag, and a third one, Velocity, as a plugin. A FreeMarker or Velocity view is a plain text file that is
rendered by a result type, reads the same action and value stack as a JSP and needs no servlet container support for JSP, so it can be
packaged in a jar, tested without a container and edited without a translation step. The running scenario is the Bookshelf catalogue:
the same book data is rendered through JSP-free views, the Struts tags are used from both engines, a layout is built from macros, and each
engine is configured for production.
Why Use a Template View Instead of JSP
All three technologies end in the same place, a Result that writes text to the response. The differences are in how the text is produced
and where it can be loaded from.
| JSP | FreeMarker | Velocity | |
|---|---|---|---|
Result type |
|
|
|
Library on the classpath |
The container’s Jasper engine |
|
|
Needs a JSP container |
Yes (translation to a servlet at first request) |
No; templates load from the web application, the classpath or a directory |
The result mocks a JSP environment through the container’s |
Struts tags |
|
|
|
Expressions |
JSP EL |
FreeMarker |
Velocity references |
HTML escaping of plain output |
No, |
Yes, automatic in 7.4.0 |
No, an event handler or a tool must be added |
Loading templates from a jar |
No |
Yes, the default class loader after the web application |
Yes, the |
Error reporting |
Translation errors with line numbers |
Very detailed, with the FTL stack trace |
Parse errors with line and column |
Typical use |
Pages written by web designers with taglibs |
Pages, mail bodies, plug-in views, every Struts UI tag template |
Existing Velocity templates, legacy Struts 1 style views |
Source: Apache Struts — FreeMarker
Source: Apache Struts — Velocity Plugin
The FreeMarker documentation of Struts gives the reason for choosing it: the engine reports errors well, has built-in internationalization and offers powerful macro libraries, and it suits cases where the action result may have to be loaded from outside a servlet container, for example a plug-in jar that carries the whole action class and its view. The same page notes that Velocity is also supported. The rest of this page treats FreeMarker first because every Struts form tag is already a FreeMarker template, then Velocity.
The FreeMarker Result
struts-default.xml registers the freemarker result type (class org.apache.struts2.views.freemarker.FreemarkerResult), so nothing is
added to the build: FreeMarker is a dependency of struts2-core. The result element takes the template path as its default parameter.
<action name="hello" class="org.example.bookshelf.BookViewAction">
<result type="freemarker">/WEB-INF/views/hello.ftl</result>
</action>
<html>
<head><title>Hello</title></head>
<body>
<h1>Hello, ${title}</h1>
<p>${book.title} by ${book.author}</p>
<p>id=${book.id} price=${book.price} id-c=${book.id?c}</p>
</body>
</html>
/hello (the book title is Dune <b>& more</b> and its id is 1234)<html>
<head><title>Hello</title></head>
<body>
<h1>Hello, Dune</h1>
<p>Dune <b>& more</b> by Frank Herbert</p>
<p>id=1,234 price=1,234.5 id-c=1234</p>
</body>
</html>
Three things in the output matter before anything else is learned. ${title} is not a field access: FreeMarker asked the value stack
for title and the stack called getTitle() on the action (see The Data Model). The markup in the book title was
escaped, because auto-escaping is on (Auto-Escaping). And the number 1234 was printed as 1,234, because FreeMarker
formats numbers for the locale unless told otherwise (Numbers, Dates and Locales). The result type itself has
these parameters, the same ones the results page of this section lists:
| Parameter | Effect |
|---|---|
|
The template to process; relative to the web application root ( |
|
|
|
|
|
|
contentType is how a FreeMarker template produces something other than HTML. The same engine renders an XML export of the catalogue
with a one-parameter change; the response header follows the parameter.
<action name="xml" class="org.example.bookshelf.BookViewAction">
<result type="freemarker">
<param name="location">/WEB-INF/views/catalogue.ftl</param>
<param name="contentType">application/xml</param>
</result>
</action>
<?xml version="1.0" encoding="UTF-8"?>
<catalogue>
<#list books as b>
<book id="${b.id?c}">${b.title}</book>
</#list>
</catalogue>
/xml$ curl -i http://localhost:18080/g10ftlvel/xml
HTTP/1.1 200
Content-Type: application/xml;charset=UTF-8
<?xml version="1.0" encoding="UTF-8"?>
<catalogue>
<book id="1">Dune</book>
<book id="2">Neuromancer</book>
</catalogue>
The Data Model
Everything a template can read is the data model, a hash FreeMarker consults by name. In Struts it is a ScopesHashModel, built for each
request by FreemarkerManager. Its own entries are the names below, and a name that is not found there is looked up in the value stack and
in the places behind it. The list was printed from a template with <#list .data_model?keys as k>${k} </#list> on 7.4.0.
<#list .data_model?keys as k>${k} </#list>
success result)request stack Parameters session Request JspTaglibs s response struts action Application Session .freemarker.RequestParameters base
| Name | What it is |
|---|---|
|
The action most recently executed ( |
|
The value stack itself; |
|
The |
|
The |
|
The context path of the request ( |
|
The |
|
Hash models over the request, session and servlet context attributes; |
|
The request parameters of the HTTP request ( |
|
Access to JSP tag libraries (see JSP Tag Libraries from FreeMarker) |
|
The Struts tags as macros, so |
|
The exception, when the view is rendered for an exception mapping or an error page |
Variable Resolution
A plain name such as title is resolved in this order, first match wins (ScopesHashModel.get): the entries of the data model above, the
value stack, the action context, the request attributes, the session attributes, then the application attributes. That ordering is the
reason ${book.title} needs no # prefix as <s:property value="#request.x"/> does in a JSP, and also the reason a
request attribute named like an action property is hidden by the property.
@Override
public String execute() {
ServletActionContext.getServletContext().setAttribute("shelf", "main shelf");
ServletActionContext.getRequest().getSession().setAttribute("reader", "alberto");
ServletActionContext.getRequest().setAttribute("wishlist", "Hyperion");
ActionContext.getContext().put("myContextParam", "ctx-value");
return SUCCESS;
}
app=<#if Application.shelf?exists>${Application.shelf}</#if>
session=<#if Session.reader?exists>${Session.reader}</#if>
request=<#if Request.wishlist?exists>${Request.wishlist}</#if>
Parameters.filter=<#if Parameters.filter?exists>${Parameters.filter}</#if>
stack.findString=${stack.findString('title')}
context=${stack.findValue('#myContextParam')}
OGNL-tag=<@s.property value="%{#request.wishlist}"/>
base=[${base}]
action=${action.title}
request=${request.requestURI} response=${response.status}
application.shelf=${application.shelf} session.id.length=${session.id?length}
filter-on-action=${filter}
/scopes?filter=x on a second request (the first one created the session)app=main shelf
session=alberto
request=Hyperion
Parameters.filter=x
stack.findString=Dune
context=ctx-value
OGNL-tag=Hyperion
base=[/g10ftlvel]
action=Dune
request=/g10ftlvel/scopes response=200
application.shelf=main shelf session.id.length=32
filter-on-action=x
The same values reached through the s tag macros (OGNL-tag) use OGNL and the hash #request, while FreeMarker reads the hash
models Request and Session. Mixing the two works, but choose one style per template. Note that filter-on-action only prints x
because the action’s setFilter carries @StrutsParameter; the request parameter itself is always readable as Parameters.filter
whether or not the action accepts it.
Auto-Escaping
Struts 7.4.0 configures FreeMarker with the HTML output format and the "enable if default" auto-escaping policy (see the
FreemarkerManager.createConfiguration source), so every ${...} interpolation in a template is HTML-escaped. This is the biggest
difference from the JSP model, where plain EL output is not escaped. FreeMarker has three ways to leave the escaping out, and the Struts
property tag has its own switch.
auto=${book.title}
noesc=<#noautoesc>${book.title}</#noautoesc>
no_esc=${book.title?no_esc}
prop=<@s.property value="book.title"/>
prop-noesc=<@s.property value="book.title" escapeHtml="false"/>
fmt=${.output_format} ${.auto_esc?c}
json=${book.title?json_string}
/escape for a title Dune <b>& more</b>auto=Dune <b>& more</b>
noesc=Dune <b>& more</b>
no_esc=Dune <b>& more</b>
prop=Dune <b>& more</b>
prop-noesc=Dune <b>& more</b>
fmt=HTML true
json=Dune <b>& more<\/b>
Source: Apache FreeMarker — noautoesc
The old way of escaping, the ?html built-in, is a template error under auto-escaping. A template written for Struts 2.3 or 2.5 stops at the
first request:
<p>${book.title?html}</p>
/legacyHTTP/1.1 500
Using ?html (legacy escaping) is not allowed when auto-escaping is on with a markup output format (HTML), to avoid double-escaping mistakes.
The fix is to delete the built-in and let the engine escape. ?no_esc and <#noautoesc> are for markup that was already made safe, such
as the output of another tag; they are the FreeMarker equivalent of cross-site scripting holes when applied to user input. The escaping
format is HTML, so ${...} inside a <script> block is escaped as HTML, not as JavaScript; for a JavaScript or JSON context use
?json_string (shown above, the / was escaped too) or build the JSON in Java.
Numbers, Dates and Locales
FreeMarker formats numbers and dates with its own rules, driven by the locale of the request. This is a different mechanism from the
Struts type conversion that the property tag applies, and the Struts documentation warns about it: ${...} evaluates a FreeMarker
expression and converts the result with FreeMarker’s formatting, while the property tag evaluates an OGNL expression and converts the
result to a string with the Struts conversion rules.
ftl-number=${book.price}
ftl-date=${book.published?string("yyyy-MM-dd")}
ftl-id=${book.id}
tag-price=<@s.property value="book.price"/>
tag-id=<@s.property value="book.id"/>
?request_locale=de, and with an Accept-Language: de header$ curl "http://localhost:18080/g10ftlvel/convert"
ftl-number=1,234.5
ftl-date=1970-01-01
ftl-id=1,234
tag-price=1234.5
tag-id=1234
$ curl "http://localhost:18080/g10ftlvel/convert?request_locale=de"
ftl-number=1.234,5
ftl-date=1970-01-01
ftl-id=1.234
tag-price=1234,5
tag-id=1234
The request_locale parameter is the one the i18n interceptor reads, so the action locale and the FreeMarker locale move together (the
header gave the same output as the parameter). The three practical rules follow from the output: use ?c for identifiers, years and
anything that is not a quantity (${book.id?c} printed 1234, ${book.id} printed 1,234); format dates with ?string(pattern) or
?date, ?time and ?datetime; and use <@s.property> when the Struts converters configured for the action must decide. A java.util.Date of
unknown kind cannot be printed without one of those:
<p>${book.published?string("yyyy-MM-dd")}</p>
<p>${book.published}</p>
/datetype<p>1970-01-01</p>
<p>Can't convert the date-like value to string because it isn't known if it's a date (no time part), time or date-time value.
...
Tip: Use ?date, ?time, or ?datetime to tell FreeMarker the exact type.
A default locale for the engine can be fixed in freemarker.properties (locale=en_US, see freemarker.properties); the
request locale still wins when the i18n interceptor sets one. Messages from resource bundles are read through the action:
${action.getText("book.heading", [title])} and <@s.text name="book.heading"><@s.param value="title"/></@s.text> both printed
Catalogue of Dune for a bundle entry book.heading=Catalogue of {0} in BookViewAction.properties.
Struts Tags as FreeMarker Macros
Every generic and UI tag of Struts is available as a FreeMarker macro under the prefix s, with the syntax <@s.tag ...>...</@s.tag>. The
s prefix is in the data model (see the table above), so no assign or taglib declaration is needed. A form that is written in JSP as
<s:form>, <s:textfield> and <s:submit> is written in FTL by changing the prefix delimiters:
<@s.form action="hello" method="get" theme="simple">
<@s.textfield name="title" placeholder="Title" cssClass="wide"/>
<@s.select name="shelf" list="books" listKey="id" listValue="title"/>
<@s.submit value="Search"/>
</@s.form>
/form<form id="hello" name="hello" action="/g10ftlvel/hello.action" method="get"><input type="text" name="title" value="Dune" id="hello_title" class="wide" placeholder="Title"/><select name="shelf" id="hello_shelf">
<option value="1">Dune</option>
<option value="2">Neuromancer</option>
</select>
<input type="submit" value="Search" id="hello_0"/>
</form>
Source: Apache Struts — FreeMarker Tags
The same tag set that the tags and form-tags pages of this section describe is therefore usable from FreeMarker; those pages are written for JSP, and this one only covers what differs.
Attributes and Expressions
A FreeMarker tag call takes attributes in two forms, and they are evaluated differently. A quoted string is handed to the tag as a string,
and the tag evaluates it as OGNL for the attributes it declares (value="title", list="books", test="title == 'Dune'",
value="%{#request.wishlist}"), converting the result to the attribute’s type. An unquoted value is a FreeMarker expression, evaluated by
FreeMarker before the tag sees it; FTL lists, hashes and numbers pass through as objects. The next examples show where the two meet.
<@s.select theme="simple" name="r1" list="{1, 2, 3}"/>
<@s.select theme="simple" name="r2" list=[1, 2, 3]/>
<@s.select theme="simple" name="r3" list={"A":"Alpha","B":"Beta"}/>
<@s.textfield theme="simple" name="a" size="20" cols=5/>
/tags2 (the r1 and r2 options are identical, and r3 has two options)<select name="r1" id="r1">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<select name="r2" id="r2">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<select name="r3" id="r3">
<option value="A">Alpha</option>
<option value="B">Beta</option>
</select>
<input type="text" name="a" size="20" value="" id="a" cols="5"/>
The documentation example list="%{1, 2, 3}" rendered an empty select on 7.4.0; use the OGNL form list="{1, 2, 3}" (no %) or an FTL
sequence. Where the tag needs a number (size, rows, cols), both size="20" and cols=5 worked in the native macros; the "must be
a number" rule that the Struts docs state applies to the JSP tag library bridge.
Inline attributes also replace the param tag for the tags that take parameters. The query-string builder url shows the four forms and
a trap in the first:
A: <@s.url value="hello" filter="%{title}" personId="%{book.id}"/>
B: <@s.url value="hello" filter=title personId=book.id?c/>
C: <@s.url value="hello"><@s.param name="filter" value="title"/><@s.param name="personId" value="book.id"/></@s.url>
D: <@s.url var="u" value="hello"><@s.param name="filter" value="title"/><@s.param name="personId" value="book.id"/></@s.url>${u} / ${u?no_esc}
/tags2 for these lines (the session id that the first response carries is ;jsessionid=... after hello and is left out)A: hello?filter=%25%7Btitle%7D&personId=%25%7Bbook.id%7D
B: hello?filter=Dune&personId=1234
C: hello?filter=Dune&personId=1234
D: hello?filter=Dune&amp;personId=1234 / hello?filter=Dune&personId=1234
Line A is the example from the documentation, and in 7.4.0 the %{...} is not evaluated for an inline parameter: the literal text is
URL-encoded into the query string. Line B passes an FTL expression, which works; line C is the param tag, which evaluates OGNL; line D
stores the URL in a variable, and printing the variable with ${u} escapes the already escaped ampersand a second time
(&), so use ?no_esc there or print the URL by leaving var out (A to C print it directly).
Control Flow: FreeMarker Directives or Struts Tags
The Struts control tags (iterator, if, else, set, property) exist as macros, but FreeMarker has its own directives that are faster
and need no OGNL, and the performance-tuning guide of Struts recommends using the FreeMarker equivalent instead of the JSP tags. The two are
interchangeable for the Bookshelf catalogue:
<ul>
<#list books as b>
<li>${b?counter}. ${b.title} (${b.price})</li>
</#list>
</ul>
<ul>
<@s.iterator value="books" status="st">
<li>${st.count}. <@s.property value="title"/> (<@s.property value="price"/>)</li>
</@s.iterator>
</ul>
<@s.if test="title == 'Dune'">it is Dune</@s.if><@s.else>another book</@s.else>
/iter<ul>
<li>1. Dune (9.99)</li>
<li>2. Neuromancer (7.5)</li>
</ul>
<ul>
<li>1. Dune (9.99)</li>
<li>2. Neuromancer (7.5)</li>
</ul>
it is Dune
Inside <@s.iterator> the current element is on top of the value stack, so <@s.property value="title"/> (OGNL) and ${title} (the
FreeMarker lookup of the stack) both read the book’s title; inside <#list> only the loop variable b is the book and ${title}
would read the action’s title. That difference is the most common source of wrong output when a JSP is converted.
Reading attributes in a Tag Template
Since Struts 7.0 the tag templates read attributes where earlier versions read parameters, as the FreeMarker Tags page notes. A tag
template is the FTL file that draws one tag (the xhtml theme’s controlfooter.ftl, for example); overriding it from the web
application changes every form field. The extra, unknown attribute description of the form field below is reachable as
${attributes.description} in that template:
<@s.form action="hello" method="get">
<@s.textfield label="Title" name="title" description="the book title"/>
</@s.form>
controlfooter.ftl placed at src/main/webapp/template/xhtml/controlfooter.ftl (the copy in the web application wins over the one in the Struts jar)${attributes.after!}<#t/>
<#if attributes.description??>
<br/><small class="hint">${attributes.description}</small>
</#if>
</td><#lt/>
</tr>
<td class="tdInput"><input type="text" name="title" value="Dune" id="hello_title" description="the book title"/> <br/><small class="hint">the book title</small>
</td>
Two details of the output are not in the documentation. The description attribute was also written into the <input> element as an
HTML attribute, because every attribute the tag does not know becomes a dynamic attribute and dynamic attributes are copied to the
element; and a dynamic attribute value is not evaluated for OGNL on a text field (foo="checked: %{title}" was printed as written),
only the templates that set evaluate_dynamic_attributes do that. Hyphenated attribute names need the hash syntax, dynamicAttributes={"data-ajax":"false"},
which printed data-ajax="false".
JSP Tag Libraries from FreeMarker
FreeMarker can call any JSP tag library through the JspTaglibs hash, so a third-party tag does not need a JSP. The Struts
documentation says the application must register org.apache.struts2.views.JspSupportServlet in web.xml with load-on-startup
1 first:
<servlet>
<servlet-name>JspSupportServlet</servlet-name>
<servlet-class>org.apache.struts2.views.JspSupportServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<#assign st=JspTaglibs["/struts-tags"]>
<@st.property value="title"/>
/taglibs on Tomcat 11.0.26 with Pages 4.0HTTP/1.1 500
java.lang.NoClassDefFoundError: jakarta/servlet/jsp/el/VariableResolver
freemarker.ext.jakarta.jsp.PageContextFactory.getCurrentPageContext(PageContextFactory.java:35)
On the stack this page targets the bridge does not work: FreeMarker 2.3.35’s PageContextFactory refers to
jakarta.servlet.jsp.el.VariableResolver, a class that is not in the jakarta.servlet.jsp-api 4.0.0 jar, so the first tag call fails
(the failure is in FreeMarker, not Struts). Loading jakarta.tags.core separately also needs the jakarta.servlet.jsp.jstl-api jar in the
WAR, or the TLD parse fails with ConditionalTagSupport not found. For the Bookshelf views the answer is to avoid the bridge: <#list>,
<#if> and the ? built-ins replace JSTL, and the s macros replace the Struts taglib. The Struts tags in JSP are the right tool when a page is a
JSP anyway (see the tags pages of this section).
Layouts with Macros and Includes
FreeMarker has no layout mechanism of its own, but <#import> and macros with a nested body give one in a few lines, which is the
JSP-free equivalent of a Tiles definition or a JSP tag-file layout (the Tiles and page layouts page of this section compares them).
The layout lives in a template, the page imports it and wraps its body in the macro.
<#macro layout heading>
<html>
<head><title>${heading} - Bookshelf</title></head>
<body>
<header><h1>Bookshelf</h1></header>
<main>
<#nested>
</main>
<footer>(c) Bookshelf</footer>
</body>
</html>
</#macro>
<#import "layout.ftl" as l>
<@l.layout heading="Catalogue">
<ul>
<#list books as b>
<li>${b.title} - ${b.price}</li>
</#list>
<#list stock?keys as k>
<li>${k}: ${stock[k]}</li>
</#list>
</ul>
</@l.layout>
/page (stock is a LinkedHashMap returned by getStock())<html>
<head><title>Catalogue - Bookshelf</title></head>
<body>
<header><h1>Bookshelf</h1></header>
<main>
<ul>
<li>Dune - 9.99</li>
<li>Neuromancer - 7.5</li>
<li>Dune: 3</li>
<li>Neuromancer: 0</li>
</ul>
</main>
<footer>(c) Bookshelf</footer>
</body>
</html>
Source: Apache FreeMarker — import
The import path is relative to the importing template and resolved through the same loaders as the result’s template. The stock?keys
loop shows what the StrutsBeanWrapper does for maps: ?keys returns the real keys (see The Object Wrapper).
The Square-Bracket Syntax
When an editor or IDE makes the angle-bracket FTL syntax awkward, FreeMarker accepts square brackets for directives and macro calls
if the template starts with [#ftl]. The tag macros follow:
[#ftl]
<h1>[#if title?has_content]${title}[/#if]</h1>
[@s.property value="book.title"/]
/square<h1>Dune</h1>
Dune <b>& more</b>
Errors in Templates
A template error is a first-class feature of FreeMarker and Struts changes what it does. The default handler is HTML_DEBUG, in every
mode (not only struts.devMode): the response has status 200, the output so far, and an HTML block that holds the failing expression,
the FTL stack trace and the Java stack trace. That page leaks template paths and class names to the browser, so production applications
set the rethrow handler.
<p>start ${title}</p>
<p>missing: ${nothingHere}</p>
<p>end</p>
/broken with the default handler, and with template_exception_handler=rethrow$ curl -i http://localhost:18080/g10ftlvel/broken
HTTP/1.1 200
<p>start Dune</p>
<p>missing: <!-- FREEMARKER ERROR MESSAGE STARTS HERE --> ... FreeMarker template error (HTML_DEBUG mode; use RETHROW in production!)
The following has evaluated to null or missing:
==> nothingHere [in template "WEB-INF/views/broken.ftl" at line 2, column 15]
$ curl -i http://localhost:18080/g10ftlvel/broken # after adding template_exception_handler=rethrow
HTTP/1.1 500
<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title>
The rethrow response is Tomcat’s error page (or the one the error-page mapping or the exception mapping of the action selects) and the
partial output is not written. A FreeMarker view can be the target of an exception mapping, where the exception variable holds the
exception; the Bookshelf action throws IllegalStateException("catalogue offline"):
<action name="boom" class="org.example.bookshelf.BookViewAction" method="boom">
<exception-mapping exception="java.lang.IllegalStateException" result="oops"/>
<result name="oops" type="freemarker">/WEB-INF/views/oops.ftl</result>
</action>
Oops: <#if exception??>${exception.message}</#if>
exceptionStack: <@s.property value="exception.message"/>
/boomOops: catalogue offline
exceptionStack: catalogue offline
The exception handling page of this section covers exception mappings and global results; the view on this page is only the last step.
Configuring FreeMarker
Struts reads FreeMarker configuration from four places: constants in struts.xml or struts.properties, a freemarker.properties file on
the classpath, init parameters in web.xml (the TemplatePath and freemarker.incompatible_improvements ones), and a replacement
FreemarkerManager. The defaults that matter come from FreemarkerManager.createConfiguration in 7.4.0: HTML output format with
auto-escaping, HTML_DEBUG error handling, localised template lookup off, and TemplateClassResolver.SAFER_RESOLVER for the ?new built-in.
freemarker.properties
A freemarker.properties file on the classpath (typically src/main/resources) holds any setting that the FreeMarker Configuration
understands. Struts reads it once, when the engine is created, and applies each key with Configuration.setSetting.
template_exception_handler=rethrow
default_encoding=UTF-8
locale=en_US
version=${.version} incompatible_improvements=${.incompatible_improvements} locale=${.locale} output_format=${.output_format}
version=2.3.35 incompatible_improvements=2.3.34 locale=en_US output_format=HTML
incompatible_improvements is 2.3.34 by default in Struts 7.4.0, which is behind the library’s own 2.3.35 so that FreeMarker’s newer
behaviour changes are off. It is raised from web.xml, not from freemarker.properties:
<context-param>
<param-name>freemarker.incompatible_improvements</param-name>
<param-value>2.3.28</param-value>
</context-param>
incompatible_improvements=2.3.28version=2.3.35 incompatible_improvements=2.3.28 locale=en_US output_format=HTML
The comment in the Struts docs says a higher number can change the pages and the Struts tags, so a change needs the full set of views re-tested.
Struts Constants for FreeMarker
| Constant | Default | Meaning |
|---|---|---|
|
|
Plug in a subclass of |
|
|
Use the Struts map wrapper (see The Object Wrapper) |
|
|
Cache the bean wrapper |
|
|
Strong size of the template cache; |
|
not set |
The template update delay; if unset FreeMarker’s own default applies |
|
|
Remove the whitespace of directive-only lines at compile time; switched off while |
The constant struts.freemarker.templatesCache is listed in default.properties only as a commented-out line and has no injection point in
7.4.0 (the FreeMarker page says the option was removed). The FreeMarker page also says the update delay defaults to 60 seconds; the
verification showed otherwise. A template edited in the deployed WAR showed up between 4 and 6 seconds later with no constant set:
$ echo 'EDITED ${books?size}' > .../webapps/g10ftlvel/WEB-INF/views/ws.ftl
t=2s: <ul>
t=4s: <ul>
t=6s: EDITED 2
Set struts.freemarker.templatesCache.updateDelay to a long time (3600 s) in production, where templates do not change, and to a short one
in a development server.
Whitespace Stripping
Since 7.2.0 Struts compiles templates with whitespace stripping on. A line that contains only an FTL directive is removed together with its indentation, which shrinks the HTML. A template that shows the effect:
<ul>
<#list books as b>
<li>${b.title}</li>
</#list>
</ul>
struts.freemarker.whitespaceStripping set to false (first) and with the default true<ul>$
$
<li>Dune</li>$
$
<li>Neuromancer</li>$
$
</ul>$
<ul>$
<li>Dune</li>$
<li>Neuromancer</li>$
</ul>$
The $ is the end of the line (cat -e). Stripping removed the directive-only lines together with their blank leftovers; the first
<li> keeps extra indentation, which does not change how the browser renders the list. The constant turns itself off in development
mode so that the generated output stays easy to read.
Where Templates Are Loaded From
FreemarkerManager.createTemplateLoader builds a MultiTemplateLoader. It searches, in this order: a directory or classpath location
given as the TemplatePath context parameter (prefix file:// or class://), then the web application, then the classpath. That order
is why an application can override a template that ships inside the Struts jar: the copy in the web application wins.
<context-param>
<param-name>TemplatePath</param-name>
<param-value>file:///var/lib/bookshelf/templates</param-value>
</context-param>
A file ext/ext.ftl under that directory was rendered by <result type="freemarker">/ext/ext.ftl</result> and printed
external template: Dune / Bookshelf (the second value is explained in A Custom FreemarkerManager). A file at
WEB-INF/views/hello.ftl under the same directory replaced the web application’s hello.ftl:
$ cat /var/lib/bookshelf/templates/WEB-INF/views/hello.ftl
OVERRIDE from TemplatePath: ${title}
$ curl http://localhost:18080/g10ftlvel/hello
OVERRIDE from TemplatePath: Dune
The override appeared only after the application was redeployed: FreeMarker’s MultiTemplateLoader remembers which loader found a
template last and asks it first, so a file added later to a loader that was not consulted is not noticed until the engine restarts. Add
overrides before the first request, or restart. The Struts documentation notes that the template path is not relative to the application
root.
The Object Wrapper
FreeMarker wraps Java objects with an object wrapper. Struts uses StrutsBeanWrapper, which extends the BeansWrapper but changes
maps. FreeMarker normally supports either friendly map built-ins (?keys, ?values) with String keys only, or no built-ins and any key
type; the Struts wrapper gives both. The Bookshelf stock map in the layout example is printed key by key with ${stock[k]}
and stock?keys. If the special behaviour causes trouble, the constant struts.freemarker.wrapper.altMap set to false restores the
standard wrapper.
A Custom FreemarkerManager
The manager can be replaced to add variables to every template, change the template loader or extend the configuration. The
constant struts.freemarker.manager.classname names a subclass; the method to override for shared variables is populateContext.
public class BookshelfFreemarkerManager extends FreemarkerManager {
@Override
protected void populateContext(ScopesHashModel model, ValueStack stack, Object action,
HttpServletRequest request, HttpServletResponse response) {
super.populateContext(model, stack, action, request, response);
model.put("appName", "Bookshelf");
}
}
<constant name="struts.freemarker.manager.classname" value="org.example.bookshelf.BookshelfFreemarkerManager"/>
Every template can now print ${appName!"(no appName)"}; the external template of the previous section printed Bookshelf.
Velocity Views
The Velocity support is a plugin. It adds the velocity result type, the Struts tags as Velocity directives, and a context that mirrors
the FreeMarker data model. Add the plugin with the BOM-managed version; Velocity Engine 2.4.1 comes with it.
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-velocity-plugin</artifactId>
</dependency>
Source: Apache Struts — Velocity Plugin
Packages that use the velocity type extend velocity-default (which extends struts-default) and the result takes the template path
as location:
<package name="vm" extends="velocity-default" namespace="/vm">
<action name="hello" class="org.example.bookshelf.BookViewAction">
<result type="velocity">/WEB-INF/vm/hello.vm</result>
</action>
</package>
<h1>Hello, $title</h1>
<p>$book.title by $book.author</p>
<p>id=$book.id price=$book.price</p>
/vm/hello (the title is Dune <b>& more</b>)<h1>Hello, Dune</h1>
<p>Dune <b>& more</b> by Frank Herbert</p>
<p>id=1234 price=1234.5</p>
Source: Apache Struts — Velocity Plugin
Compare with the FreeMarker output: Velocity printed the markup unescaped and the number without a locale grouping. The Velocity result is
a cross-site scripting hole for every reference that holds user input until escaping is added (Escaping References).
The result takes the same two parameters as every StrutsResultSupport: location and parse (default true; false does not
evaluate the location for OGNL expressions). It mocks a JSP execution environment through the container’s JspFactory to display the
template.
The Velocity Context
The context holds the same built-ins as the FreeMarker model; the names verified on 7.4.0 are listed below, with the ones the older documentation mentions but that do not resolve.
stack=#if($stack)Y#{else}n#end
action=#if($action)Y#{else}n#end
req=#if($req)Y#{else}n#end
stack-value=$stack.findString('title')
base=$base
request-uri=$request.requestURI
struts-class=$struts.getClass().getSimpleName()
stack, action, base, struts, request, response, session and application resolve; req, res, ognl and text do notstack=Y action=Y base=Y struts=Y request=Y response=Y session=Y application=Y req=n res=n ognl=n text=n
stack-value=Dune
base=/g10ftlvel
request-uri=/g10ftlvel/vm/ctx
struts-class=VelocityStrutsUtil
Source: Apache Struts — Velocity
Properties of the action resolve with Velocity’s own reference syntax through the value stack ($title calls getTitle() on the action),
and $struts is the helper VelocityStrutsUtil: $struts.findString('title'), $struts.getText('book.heading'), $struts.urlEncode('a b&c')
(which printed a+b%26c) and $struts.translateVariables(...). Velocity 2.4 sets $foreach.count; the $velocityCount of older Velocity is not set.
Struts Tags as Velocity Directives
Each Struts tag is a Velocity directive named #s plus the tag name, with the attributes as quoted name=value strings:
#sform ("action=hello" "method=get"). The plugin registers 37 of them (read from its jar): #saction, #sactionerror,
#sactionmessage, #sa, #sbean, #scheckbox, #scheckboxlist, #scombobox, #scomponent, #sdate, #sdoubleselect,
#sfielderror, #sfile, #sform, #shead, #shidden, #si18n, #sinclude, #sinputtransferselect, #slabel,
#soptgroup, #soptiontransferselect, #sparam, #spassword, #sproperty, #spush, #sradio, #sreset, #sselect, #sset,
#ssubmit, #stextarea, #stext, #stextfield, #stoken, #surl and #supdownselect. There are no control-flow
directives (iterator, if, else, generator, append, merge, sort, subset); Velocity’s #foreach and #if replace them.
#sform ("action=hello" "method=get" "theme=simple")
#stextfield ("name=title" "placeholder=Title")
#sselect ("name=shelf" "list=books" "listKey=id" "listValue=title")
#ssubmit ("value=Search")
#end
#foreach($b in $books)
<li>$foreach.count: $b.title</li>
#end
/vm/tags<form id="hello" name="hello" action="/g10ftlvel/vm/hello.action" method="get"><input type="text" name="title" value="Dune" id="hello_title" placeholder="Title"/><select name="shelf" id="hello_shelf">
<option value="1">Dune</option>
<option value="2">Neuromancer</option>
</select>
<input type="submit" value="Search" id="hello_0"/>
</form>
<li>1: Dune</li>
<li>2: Neuromancer</li>
Source: Apache Struts — Velocity Tags
A Velocity directive must say up front whether it is a block or an inline directive, so most tags have no #end and only #sform, #sa
and #scomponent are blocks (the jar overrides getType for exactly those three). The #sa block printed its body before the <a>
element in the test (Back<a href="..."></a>), so a link is safer written with #surl and plain HTML. The Struts tags print the
same HTML as the FreeMarker macros, because the template engine that draws a UI tag is chosen by struts.ui.templateSuffix (ftl in
default.properties): the tag templates are FreeMarker even in a Velocity page.
Escaping References
Velocity does not escape. Its engine ships an event handler that does, EscapeHtmlReference, and it is enabled in velocity.properties
on the classpath; the match property is a regular expression that selects the references to escape, so a handler matching
/.*/ escapes everything.
event_handler.reference_insertion.class = org.apache.velocity.app.event.implement.EscapeHtmlReference
event_handler.escape.html.match = /\$book.*/
$book.title is now escaped and $title (which does not match) is nottitle=Dune
book.title=Dune <b>& more</b>
Use /.*/ unless a reference is known to hold safe markup; a Velocity template without a handler is the Velocity equivalent of
<#noautoesc> everywhere.
velocity.properties, Macros and Contexts
If a velocity.properties is on the classpath it is loaded by Velocity; the default location is the constant
struts.velocity.configfile. The Struts resource loaders (strutsfile for the web application, strutsclass for the classpath) are
installed by default and the directives are registered for you. The file can add a macro library:
velocimacro.library = /WEB-INF/vm/bookshelf-macros.vm
#macro(bookCard $b)
<article><h2>$b.title</h2><p>by $b.author</p></article>
#end
#bookCard($book)
title=$title
text=#stext("name=book.heading")
/vm/macros<article><h2>Dune <b>& more</b></h2><p>by Frank Herbert</p></article>
title=Dune
text=Catalogue of {0}
(The title was escaped by the handler of the previous section.) #stext and $struts.getText(...) print the message as written when no
parameter is given.
Two other constants extend the context. struts.velocity.contexts takes a comma-separated list of VelocityContext classes that are
chained to the Struts context, so every template sees what they put:
public class ShelfContext extends VelocityContext {
public ShelfContext() {
put("shelfName", "Main shelf");
put("shelf", new ShelfTool());
}
}
<constant name="struts.velocity.contexts" value="org.example.bookshelf.ShelfContext"/>
shelfName=$shelfName
label=$shelf.label($title)
shelfName=Main shelf
label=[Dune]
The third constant, struts.velocity.toolboxlocation, names a Velocity Tools configuration file, and it is the one to avoid in 7.4.0.
Velocity Tools and the Toolbox Trap
Velocity Tools are the reusable helpers (EscapeTool, NumberTool, DateTool) that the earlier generation of Struts and Velocity
integrations loaded in a toolbox. The plugin supports a toolbox through struts.velocity.toolboxlocation, with the optional jars
struts2-velocity-tools-view-jakarta and velocity-tools-generic 3.1 on the classpath (neither is brought in by the plugin, since both
are optional dependencies). The location is read from the classpath, and the configuration is validated: an application-scoped tool in a
request toolbox is rejected.
<tools>
<toolbox scope="request">
<tool key="shelf" class="org.example.bookshelf.ShelfTool"/>
</toolbox>
</tools>
shelf=$shelf.label('Dune')
title=$title
stack=$stack
/vm/macros with struts.velocity.toolboxlocation set to tools.xmlshelf=[Dune]
title=$title
stack=$stack
The tool works, and the value stack is gone: $title and $stack are printed as written, and a Struts directive such as
#stextfield fails with NullPointerException: Cannot invoke "org.apache.struts2.util.ValueStack.getContext()" because "stack" is null.
The cause is in StrutsVelocityManager.createContext: when a toolbox is configured the context comes only from VelocityTools.createContext,
a ToolContext with the default (request) toolbox, and the Struts context is never built. A toolbox therefore turns the integration into
plain Velocity. Use struts.velocity.contexts (above) for helpers; put a Velocity Tools toolbox into a context class if a tool is needed.
A /WEB-INF/tools.xml location gave Could not find any configuration at /WEB-INF/tools.xml because the file is read from the classpath.
Source: Apache Velocity Tools 3.1
The older way, the Velocity Tools VelocityViewServlet, still exists in the Struts fork jar (struts2-velocity-tools-view-jakarta) and
renders templates outside Struts completely: mapped to *.vm in web.xml, it rendered a standalone.vm with #set($x = 40 + 2)$x and
the request URI.
<servlet>
<servlet-name>velocity</servlet-name>
<servlet-class>org.apache.velocity.tools.view.VelocityViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>velocity</servlet-name>
<url-pattern>*.vm</url-pattern>
</servlet-mapping>
$ curl http://localhost:18080/g10ftlvel/standalone.vm
standalone: 42 /g10ftlvel/standalone.vm
That page has no action, value stack or Struts tags, so it is a plain Velocity template server, not a Struts view.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
A template written for Struts 2.3 or 2.5 escapes with |
Delete the built-in; auto-escaping does it. Use |
User input appears as markup in a Velocity page (cross-site scripting) |
Velocity does not escape references |
Enable |
A FreeMarker error returns 200 and a page full of stack trace |
The default handler is |
Set |
|
A |
|
An id prints as |
FreeMarker formats numbers for the locale |
|
|
An inline parameter is not evaluated as OGNL in 7.4.0, and a stored URL is escaped twice |
Use an FTL expression ( |
|
The |
|
|
The loop variable is not on the value stack, only inside |
Use the loop variable ( |
|
Variables from older documentation: |
|
|
FreeMarker 2.3.35’s JSP taglib bridge needs a class that Pages 4.0 removed |
Use the native |
A template added to |
|
Add overrides before the first request, or restart the application |
After setting |
The toolbox context replaces the Struts context in 7.4.0 |
Remove the constant; chain helper objects with |
|
The toolbox file is read from the classpath |
Put it in |
|
Velocity 2.4 renamed the loop variable |
|
What Changed Recently
| Version | Change |
|---|---|
2.5.13 |
The |
6.0.0 |
The Velocity support is a plugin, |
7.0.0 |
Tag templates and tags read |
7.2.0 |
FreeMarker whitespace stripping ( |
Source: Struts 6.x.x to 7.x.x migration
Only the 7.4.0 state itself was read from the jars and the sources; the versions of the earlier rows are as the documentation and the migration guide give them.
Where the Books and Docs Are Outdated
The two Struts books consulted for concept coverage describe the technology in its first generation and are not reproduced here.
| Material | Struts 7.4 |
|---|---|
Struts in Action (Struts 1), the Velocity chapter: Velocity templates instead of JSP through the |
Struts 2 and later do not use the servlet. The |
Struts 2 in Action: tags are FreeMarker templates; the FreeMarker examples use |
Auto-escaping is on in 7.4.0 and |
Struts docs: |
|
Struts docs: update delay "60 s", |
None of these behaved as documented in the verification run (see the examples above) |
Struts docs: the Velocity Properties page lists |
No such files in the 7.4.0 jars; the core jar has a |
Both books also describe the earlier Struts 2 and Struts 1 generations of the JSP tag libraries and the Struts 1 tags. The Struts 1 page of this section, Struts 1 (Classic) & Migration, covers the correspondence, and the migration material tells which of the old packages and classes are gone. The Velocity examples in the books should be treated as design ideas, not as configuration.
Putting It Together for the Bookshelf
A Bookshelf application that uses FreeMarker for every page needs, in the build, only struts2-core. It sets
template_exception_handler=rethrow in freemarker.properties, keeps auto-escaping, formats identifiers with ?c, builds pages from an
imported layout macro, writes forms with the s macros (using FTL expressions for inline attributes) and overrides a tag template in
template/xhtml/ when a field needs a hint. A Velocity page is added only when an existing set of templates has to be reused, with the
escape handler enabled and the toolbox left alone. JSP remains the choice when a page needs a JSP-only tag library, since the FreeMarker
bridge is broken on Pages 4.0.
References
-
Apache Struts — FreeMarker Tags, Using inline attributes with templates
-
Apache Struts — FreeMarker Support, FreeMarker configuration with recent releases
-
Apache Struts — FreeMarker Support, FreeMarker Whitespace Stripping
-
Apache Struts — FreeMarker Support, Using struts UI tags - or any JSP Tag Library
-
Apache Struts — FreeMarker Support, Dynamic attributes support
-
Apache Struts — Performance Tuning, Enable FreeMarker template caching
-
Apache Struts — Performance Tuning, Enable FreeMarker whitespace stripping
-
Apache Struts — Performance Tuning, Use the FreeMarker equivalent