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.

Table 1. JSP, FreeMarker and Velocity as Struts views (7.4.0)
JSP FreeMarker Velocity

Result type

dispatcher (the default)

freemarker, in struts-default.xml

velocity, from struts2-velocity-plugin, package velocity-default

Library on the classpath

The container’s Jasper engine

freemarker 2.3.35, a dependency of struts2-core

velocity-engine-core 2.4.1, a dependency of the plugin

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 JspFactory; templates load from the web application or the classpath

Struts tags

<s:property/> through struts-tags.tld

<@s.property/>, native macros (no taglib)

#sproperty(...), native directives

Expressions

JSP EL ${...}, OGNL in Struts tag attributes

FreeMarker ${...}, OGNL in %{...} of tag attributes

Velocity references $title, OGNL in directive arguments

HTML escaping of plain output

No, <c:out> or escapeXml is needed

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 strutsclass loader

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

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.

flowchart LR A["Action method returns a result code"] --> B["Result mapped in struts.xml"] B --> C{"Result type"} C -->|"dispatcher"| D["RequestDispatcher forwards to a JSP"] C -->|"freemarker"| E["FreemarkerResult: FreemarkerManager builds the data model"] C -->|"velocity"| F["VelocityResult: VelocityManager builds the context"] E --> G["Template found in TemplatePath, the web application or the classpath"] F --> H["Template found in the web application or the classpath"] G --> I["Rendered text is written to the response"] H --> I D --> I

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.

A FreeMarker view for the Bookshelf catalogue (action and template)
<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>
The response for /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 &lt;b&gt;&amp; more&lt;/b&gt; 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:

Table 2. Parameters of the freemarker result
Parameter Effect

location (default)

The template to process; relative to the web application root (/WEB-INF/views/home.ftl) or a classpath resource (/com/company/web/views/home.ftl)

parse

true by default; false stops the location being parsed for ${...} expressions

contentType

text/html unless specified

writeIfCompleted

false by default; true writes to the response only if the template ran without an error (template_exception_handler=rethrow in freemarker.properties has the same effect)

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.

An XML view through the same result type
<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>
The response for /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>
The names printed by that template (7.4.0, an action returning the success result)
request stack Parameters session Request JspTaglibs s response struts action Application Session .freemarker.RequestParameters base
Table 3. The variables of a FreeMarker view in 7.4.0, as verified (what the documentation lists is compared in Where the Books and Docs Are Outdated)
Name What it is

action

The action most recently executed (${action.title})

stack

The value stack itself; ${stack.findString('title')} evaluates an OGNL expression and ${stack.findValue('#myContextParam')} reads the action context

request, response

The HttpServletRequest and HttpServletResponse (${request.requestURI})

session

The HttpSession, present only when a session already exists (${session.id})

base

The context path of the request (/g10ftlvel)

struts

The StrutsUtil helper (findString, findValue, getText, urlEncode, buildUrl, translateVariables, include, bean)

Request, Session, Application

Hash models over the request, session and servlet context attributes; ${Session.reader} is the attribute reader

Parameters

The request parameters of the HTTP request (${Parameters.filter})

JspTaglibs

Access to JSP tag libraries (see JSP Tag Libraries from FreeMarker)

s

The Struts tags as macros, so <@s.form> works without an assign

exception

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.

flowchart TD A["Template asks for a name, for example title"] --> B{"In the data model entries? action, stack, request, Session, base ..."} B -->|"yes"| Z["Use the value"] B -->|"no"| C{"On the value stack? action properties first"} C -->|"yes"| Z C -->|"no"| D{"In the action context? application, parameters, custom keys"} D -->|"yes"| Z D -->|"no"| E{"A request attribute?"} E -->|"yes"| Z E -->|"no"| F{"A session attribute?"} F -->|"yes"| Z F -->|"no"| G["Missing: a plain interpolation fails, name?? and name! are safe"]
Reading each scope from a template (the action sets an application, a session and a request attribute and an action-context entry)
@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}
The output of /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}
The output of /escape for a title Dune <b>& more</b>
auto=Dune &lt;b&gt;&amp; more&lt;/b&gt;
noesc=Dune <b>& more</b>
no_esc=Dune <b>& more</b>
prop=Dune &lt;b&gt;&amp; more&lt;/b&gt;
prop-noesc=Dune <b>& more</b>
fmt=HTML true
json=Dune &lt;b&gt;&amp; more&lt;\/b&gt;

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>
The result of /legacy
HTTP/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"/>
The output for the default locale, with ?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>
The result of /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>
The response for /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>

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/>
The response for /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}
The output of /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&amp;personId=%25%7Bbook.id%7D
B: hello?filter=Dune&amp;personId=1234
C: hello?filter=Dune&amp;personId=1234
D: hello?filter=Dune&amp;amp;personId=1234 / hello?filter=Dune&amp;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 (&amp;), 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>
The response for /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>
The cell the form tag now writes
<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"/>
The result of /taglibs on Tomcat 11.0.26 with Pages 4.0
HTTP/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>
The response for /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>

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"/]
The response for /square
<h1>Dune</h1>
Dune &lt;b&gt;&amp; more&lt;/b&gt;

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>
The response for /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"/>
The response for /boom
Oops: 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
With that file the output of the broken template becomes a 500, as shown above, and a template can print the engine settings
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>
With that context parameter the same template printed incompatible_improvements=2.3.28
version=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

Table 4. The FreeMarker constants of default.properties (7.4.0)
Constant Default Meaning

struts.freemarker.manager.classname

FreemarkerManager (commented out)

Plug in a subclass of org.apache.struts2.views.freemarker.FreemarkerManager

struts.freemarker.wrapper.altMap

true

Use the Struts map wrapper (see The Object Wrapper)

struts.freemarker.beanwrapperCache

false

Cache the bean wrapper

struts.freemarker.mru.max.strong.size

0

Strong size of the template cache; 0 selects the soft cache storage, which performs better under load

struts.freemarker.templatesCache.updateDelay

not set

The template update delay; if unset FreeMarker’s own default applies

struts.freemarker.whitespaceStripping

true

Remove the whitespace of directive-only lines at compile time; switched off while struts.devMode is true

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>
The same template with 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.

flowchart LR A["Template name, for example /WEB-INF/views/hello.ftl"] --> B{"TemplatePath context parameter set?"} B -->|"yes"| C["file:// directory or class:// location"] B -->|"no"| D["Web application root"] C -->|"not found"| D D -->|"not found"| E["Classpath: struts jars and WEB-INF/classes"] C -->|"found"| Z["Template used"] D -->|"found"| Z E -->|"found"| Z E -->|"not found"| X["TemplateNotFoundException"]
<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>

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>
The response for /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>

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()
The output: stack, action, base, struts, request, response, session and application resolve; req, res, ognl and text do not
stack=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

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
The response for /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>

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 not
title=Dune
book.title=Dune &lt;b&gt;&amp; more&lt;/b&gt;

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")
The response for /vm/macros
<article><h2>Dune &lt;b&gt;&amp; more&lt;/b&gt;</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)
The output
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
The output of /vm/macros with struts.velocity.toolboxlocation set to tools.xml
shelf=[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.

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

Table 5. The mistakes that FreeMarker and Velocity views most often cause in Struts 7.4
Symptom Cause Fix

ParseException: "Using ?html (legacy escaping) is not allowed when auto-escaping is on"

A template written for Struts 2.3 or 2.5 escapes with ?html

Delete the built-in; auto-escaping does it. Use ?no_esc or <#noautoesc> only for markup that is already safe

User input appears as markup in a Velocity page (cross-site scripting)

Velocity does not escape references

Enable EscapeHtmlReference in velocity.properties with a match that covers the references, or use the Struts tags

A FreeMarker error returns 200 and a page full of stack trace

The default handler is HTML_DEBUG, in every mode

Set template_exception_handler=rethrow in freemarker.properties

Can't convert the date-like value to string

A java.util.Date of unknown kind is interpolated

?string("yyyy-MM-dd"), ?date, ?time or ?datetime

An id prints as 1,234 or a year as 2,026

FreeMarker formats numbers for the locale

?c for identifiers and years; ?string("0.00") for fixed formats

<@s.url ... personId="%{id}"/> puts %25%7Bid%7D in the URL and ${u} shows &amp;

An inline parameter is not evaluated as OGNL in 7.4.0, and a stored URL is escaped twice

Use an FTL expression (personId=id?c) or the param tag; print the URL directly or with ?no_esc

<@s.select list="%{1, 2, 3}"/> is empty

The %{...} form is not evaluated for list here

list="{1, 2, 3}" or an FTL sequence [1, 2, 3]

${title} shows the action’s title inside <#list>

The loop variable is not on the value stack, only inside <@s.iterator>

Use the loop variable (${b.title}) in <#list>

FreeMarker template error: attributes or req, res, ognl missing

Variables from older documentation: attributes exists only in tag templates and the others are not in the 7.4.0 model

Parameters.name, request, response, stack; see the variable table

NoClassDefFoundError: jakarta/servlet/jsp/el/VariableResolver from JspTaglibs

FreeMarker 2.3.35’s JSP taglib bridge needs a class that Pages 4.0 removed

Use the native s macros and FreeMarker built-ins; keep JSP tag libraries in JSP pages

A template added to TemplatePath or the web application is not seen

MultiTemplateLoader remembers the loader that last found a template

Add overrides before the first request, or restart the application

After setting struts.velocity.toolboxlocation, $title is printed literally and #stextfield throws a NullPointerException

The toolbox context replaces the Struts context in 7.4.0

Remove the constant; chain helper objects with struts.velocity.contexts

Could not find any configuration at /WEB-INF/tools.xml

The toolbox file is read from the classpath

Put it in src/main/resources and use tools.xml

$velocityCount is printed as text

Velocity 2.4 renamed the loop variable

$foreach.count and $foreach.index

What Changed Recently

Table 6. Changes in the FreeMarker and Velocity support, with the version where each appeared
Version Change

2.5.13

The freemarker.incompatible_improvements servlet context parameter (the FreeMarker page: "since Struts 2.5.13")

6.0.0

The Velocity support is a plugin, struts2-velocity-plugin, and no longer part of the core (taken from the project’s migration notes; not re-checked against the 6.0.0 sources)

7.0.0

Tag templates and tags read attributes instead of parameters (the FreeMarker Tags page: "Since Struts 7.x the parameters have been replaced with attributes"); the com.opensymphony.xwork2 package is renamed to org.apache.struts2; the VelocityTools class that backs the toolbox is @since 7.0 in the 7.4.0 source

7.2.0

FreeMarker whitespace stripping (struts.freemarker.whitespaceStripping, on by default, off in development mode), as the FreeMarker Support page says ("Available since Struts 7.2.0")

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.

Table 7. Material about FreeMarker and Velocity views that no longer matches Struts 7.4
Material Struts 7.4

Struts in Action (Struts 1), the Velocity chapter: Velocity templates instead of JSP through the VelocityViewServlet and the VelocityStruts tools

Struts 2 and later do not use the servlet. The velocity result and the #s... directives replace it, the plugin is struts2-velocity-plugin, and a toolbox (struts.velocity.toolboxlocation) replaces the context in 7.4.0. The VelocityViewServlet still exists in the Jakarta fork of Velocity Tools and renders plain templates

Struts 2 in Action: tags are FreeMarker templates; the FreeMarker examples use ${title?html} or no escaping at all

Auto-escaping is on in 7.4.0 and ?html is an error. The themes and the form-tag templates are FreeMarker; the theme override is a template with the same path

Struts docs: req, res and ognl in a FreeMarker view; attributes.myParameter for a request parameter; application as the ServletContext

request, response, Parameters.myParameter and Application/application attribute maps; req, res, ognl and attributes are not in the data model

Struts docs: update delay "60 s", foo="checked: %{bar}" evaluated, list="%{1, 2, 3}", personId="%{personId}" in s:url

None of these behaved as documented in the verification run (see the examples above)

Struts docs: the Velocity Properties page lists action-default.vm, tigris-macros.vm, myapp.vm and refers to struts-default.vm

No such files in the 7.4.0 jars; the core jar has a struts.vm with legacy macros that use $req and $res, which the 7.4.0 context does not define

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