Tags Overview & Generic Tags
|
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 and Java 21 with
Jakarta Pages 4.0 and Servlet 6.1; the facts were checked on 2026-10-08 against the 7.4.0 struts2-core jar (its
struts-tags.tld, the tag and component classes and default.properties) and the Tag Developer Guide, and every example on it was built into a WAR, deployed on Tomcat 11 and
requested. The Struts tag library is the part of the framework that a JSP page talks to: a view does not call the action,
it writes <s:property value="filter"/> and the tag looks the name up on the value stack, formats the result and writes
it, escaped. The library has 53 tags in the 7.4.0 descriptor, 27 generic tags (control flow, data access, URLs, assets) and
26 UI tags (form controls and their layout); this page is about the generic ones and about the rules that every tag
attribute obeys, the UI and form tags and the themes that render them are the topic of the pages
UI & Form Tags and Themes & Templates. The running scenario is the Bookshelf, a catalogue of books exposed as Struts actions: the page
reads its list of books, loops over them, builds links to other actions, formats prices and dates and links the CSS and
JavaScript of the page, and every output shown is real. The related pages of this section are linked where they help: FreeMarker & Velocity Views,
Tiles & Page Layouts, Security Hardening,
JSON Plugin & AJAX and Testing.
The Big Picture
A Struts tag is a normal JSP tag handler (a classic tag, see
Custom Tags: Classic Handlers) with one extra step. The handler class lives in
org.apache.struts2.views.jsp (PropertyTag, IteratorTag, URLTag and so on), receives each attribute as a String
through its setters and hands them to a component in org.apache.struts2.components (Property, IteratorComponent,
URL and so on). The component holds the logic: it evaluates the attributes against the value stack (which the framework
put in the request before the JSP ran, see Architecture
and Request Lifecycle), pushes and pops objects on the stack, and writes to the page. The same components serve the
FreeMarker and Velocity tags, which is why the Tag Developer Guide says the library is decoupled from the view technology:
<s:property .../> in a JSP and <@s.property .../> in a FreeMarker template call the same Property component.
The tags divide into three families on the Tag Reference page, plus the UI tags that are the other half of the library.
| Family | Tags | What they do |
|---|---|---|
Control tags |
|
Conditional output; loops; building, joining, ordering and slicing iterators |
Data tags |
|
Read and write values, create objects, run an action from the view, localized text, formatting, includes, URLs and links |
Other tags |
|
Whitespace compression, |
The 26 UI tags are actionerror, actionmessage, checkbox, checkboxlist, combobox, component, datetextfield,
doubleselect, fielderror, file, form, head, hidden, inputtransferselect, label, optgroup,
optiontransferselect, password, radio, reset, select, submit, textarea, textfield, token and
updownselect; they differ from the generic tags in one way that matters: they render through templates and themes.
The Bookshelf Used on This Page
All examples are classes and pages of one WAR, g10tags.war, deployed under the context path /g10tags on Tomcat 11.0.26:
the Struts BOM org.apache.struts:struts2-bom:7.4.0 imported in dependencyManagement, struts2-core, the Servlet and Pages
APIs with scope provided, the Jakarta Tags 3.0 API and implementation (jakarta.servlet.jsp.jstl-api 3.0.2 and
org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1) for the pages that mix JSTL, and two WebJars (org.webjars:bootstrap
5.3.8, org.webjars:jquery 3.7.1) for the webjar tag. StrutsPrepareAndExecuteFilter is mapped to /*. Action URLs
use the default extension .action, which is optional, so /g10tags/control and /g10tags/control.action are the same
request. Imports and package lines are left out of the listings; the packages are org.example.bookshelf and
org.example.bookshelf.model.
| Class or page | Mapping | Concept |
|---|---|---|
|
|
One action behind every page of this document: a |
|
|
The pages that print the outputs quoted below |
|
|
A second action that the |
|
(bundles) |
Texts for |
|
(model classes) |
|
The only non-default setting of the app is the allowlist for the model package; the reason is a rule of 7.x that shows up
in many outputs below, so it is explained once here. OGNL, which evaluates every expression of every tag, refuses to read
a class that is not on the allowlist; Struts allows its own classes and the classes named in struts.xml, and the
application’s model classes need struts.allowlist.packageNames (or struts.allowlist.classes). Without the line, every
<s:property value="title"/> on a Book prints nothing and the log says is not allowlisted.
<struts>
<constant name="struts.allowlist.packageNames" value="org.example.bookshelf.model"/>
<package name="default" extends="struts-default" namespace="/">
<action name="stats" class="org.example.bookshelf.StatsAction">
<result>/WEB-INF/views/stats.jsp</result>
</action>
<action name="stats-featured" class="org.example.bookshelf.StatsAction" method="featured">
<result>/WEB-INF/views/stats.jsp</result>
</action>
<action name="*" class="org.example.bookshelf.ShelfAction">
<result>/WEB-INF/views/{1}.jsp</result>
</action>
</package>
</struts>
public class ShelfAction extends ActionSupport {
private String filter = "all";
private final List<Book> books = List.of(
new Book("Dune", "Frank Herbert", 1965, "scifi", 9.5),
new Book("Neuromancer", "William Gibson", 1984, "cyberpunk", 8.25),
new Book("Hyperion", "Dan Simmons", 1989, "scifi", 11.0),
new Book("Emma", "Jane Austen", 1815, "classic", 6.0));
public String getFilter() { return filter; }
@StrutsParameter
public void setFilter(String filter) { this.filter = filter; }
public List<Book> getBooks() { return books; }
public List<String> getTitles() { return books.stream().map(Book::getTitle).toList(); }
public int getCount() { return books.size(); }
public String getMarkup() { return "<b>Dune</b> & \"more\""; }
public String getNothing() { return null; }
public String getOne() { return "a"; }
public Map<String, String> getGenres() { /* scifi, cyberpunk, classic */ }
public Date getPublished() { /* 1965-09-13 */ }
}
Using the Library in a JSP
The taglib Directive and the TLD
The Struts TLD is inside struts2-core-7.4.0.jar as META-INF/struts-tags.tld, so a page only needs the directive; no
web.xml entry and no WEB-INF/tlds copy:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (Struts)</h1>
<p>Filter: <s:property value="filter"/></p>
<ul>
<s:iterator value="titles"><li><s:property/></li></s:iterator>
</ul>
</body>
</html>
Requested through the shelf action this prints Filter: all and one <li> per title. The facts of the descriptor that
decide how the tags behave, read from the 7.4.0 jar:
| Item | Value |
|---|---|
|
|
|
|
Tags |
53: 27 generic and 26 UI; each tag’s |
|
|
|
|
|
|
|
An attribute of every tag, default |
The attribute rtexprvalue=false explains the most frequent question about Struts tags: <s:property value="${filter}"/>
fails at translation, because ${...} is JSP EL and the attribute accepts no JSP expressions. The expression is OGNL,
and a %{...} marks it when the attribute is a string. The ValueStack, ActionContext & OGNL page shows the failure and the
reason in detail.
Where the Tags Read From
Every expression of every tag is OGNL evaluated against the value stack of the request: a bare name is a property
looked up from the top of the stack downwards (the action first, then whatever push, bean or iterator put above it),
and a name with a # in front is a key of the context map (#session, #request, #attr, #parameters and the
variables created by set, iterator and the other tags with a var attribute). The ValueStack, the ActionContext and
OGNL are the subject of the ValueStack, ActionContext
and OGNL page; the short version that the tags need is the next table.
| Expression in a tag | Looks up | Output of the example action |
|---|---|---|
|
property |
|
|
index into a list, then a property |
|
|
a key of a map |
|
|
the session map of the context |
|
|
a request attribute; |
|
|
the request parameters (an array, printed as its element) |
|
|
the top of the stack explicitly |
|
|
the stack cut one element down from the top |
|
Tag Syntax: How Attributes Are Evaluated
The Tag Syntax page boils the whole subject down to three rules, and every surprise with a Struts tag is one of them being applied:
-
All String attribute types are parsed for the
%{ ... }notation; text outside the notation is literal. -
All non-String attribute types (a
boolean, anint, aListor aCollection, anObject) are not parsed for the notation but evaluated directly as an OGNL expression. -
The exception to rule 2: if a non-String attribute does use the
%{ }notation, the notation is ignored as redundant and its content is evaluated, so%{true}andtruemean the same.
String Attributes
The experiment that shows rule 1 uses s:textfield with theme="simple", which renders just the <input> element and no
table row (themes belong to the page on themes and templates). The same action property filter (value all) is written
five ways, and the HTML of each is quoted:
<s:textfield name="filter" theme="simple"/>
<s:textfield name="filter" value="filter" theme="simple"/>
<s:textfield name="filter" value="%{filter}" theme="simple"/>
<s:textfield name="filter" value="%{getText('book.heading')}" theme="simple"/>
<s:textfield name="filter" value="%{filter} and %{count}" theme="simple"/>
<input type="text" name="filter" value="all" id="filter"/>
<input type="text" name="filter" value="filter" id="filter"/>
<input type="text" name="filter" value="all" id="filter"/>
<input type="text" name="filter" value="Books on the shelf" id="filter"/>
<input type="text" name="filter" value="all and 4" id="filter"/>
The first line gets its value without a value attribute at all, because name tells the tag which property to read (and
to write back on submission). The second prints the literal text filter: value of a form tag is a String attribute,
so filter is a literal, not a property name. The third reads the property with the notation. The fourth calls a method
of the action (getText is inherited from ActionSupport) and the fifth mixes literal text with two expressions, which is
what the notation is for.
The most common use of the same rule is a label that comes from a message bundle or contains data:
<s:textfield name="filter" label="%{#request.requestId}" theme="xhtml"/>
<s:textfield name="filter" label="getText('book.heading')" theme="xhtml"/>
<s:textfield name="filter" label="%{getText('book.heading')}" theme="xhtml"/>
<s:textfield name="filter" label="Cart item No. %{count} note" theme="xhtml"/>
The labels rendered (the xhtml theme wraps each field in a table row; only the <label> element is quoted):
<label for="filter" class="label">42:</label>
<label for="filter" class="label">getText('book.heading'):</label>
<label for="filter" class="label">Books on the shelf:</label>
<label for="filter" class="label">Cart item No. 4 note:</label>
The second label is the trap: label is a String attribute, so without the notation getText('book.heading') is
literal text and prints as it was written. The Text tag page of the Tag Developer Guide shows exactly that form,
label="getText('person.lastName')", as the way to use i18n in an attribute; on Struts 7.4.0 it prints the call, and the
working form needs %{...} around it.
Non-String Attributes
Rule 2 is the reason that test= works without any notation on s:if, and that multiple="true" on a select
is a boolean. The Tag Syntax page gives four spellings of the same boolean; the example WAR renders them with a
s:select (the multi-select attribute multiple is a boolean), and the HTML shows that the first two are multiple
selects and the last two follow the action property allowMulti, which is false:
<s:select name="g" list="genres" multiple="true" theme="simple"/>
<s:select name="g" list="genres" multiple="%{true}" theme="simple"/>
<s:select name="g" list="genres" multiple="allowMulti" theme="simple"/>
<s:select name="g" list="genres" multiple="%{allowMulti}" theme="simple"/>
<select name="g" id="g" multiple="multiple"> <!-- multiple="true" and multiple="%{true}" -->
<select name="g" id="g"> <!-- multiple="allowMulti" and multiple="%{allowMulti}" -->
The list attribute of the same tag is a non-String attribute too: list="genres" is the OGNL expression genres, the
Map of the action. Because it is easy to forget which attributes are Strings, the notation is harmless on the non-String
ones, and most Struts code writes %{...} everywhere an expression is meant; the official examples of the generic tags,
though, write test="#rowstatus.odd == true" and value="users" without it, and so does this page.
The Same Attribute in Both Worlds: property and set
The attributes of the data tags are the ones that show rule 1 and rule 2 side by side. s:property takes value as an
Object (rule 2: an expression), s:set also treats value as an expression, as every set example of the guide shows. The six lines below were requested and printed on the page syntax:
C1 [<s:property value="filter"/>]
C2 [<s:property value="'filter'"/>]
C3 [<s:property value="%{filter}"/>]
C4 [<s:property value="'Hello ' + filter"/>]
C5 [<s:property value="%{'Hello ' + filter}"/>]
C6 [<s:property value="count + 1"/>]
C1 [all]
C2 [filter]
C3 [all]
C4 [Hello all]
C5 [Hello all]
C6 [5]
The pair C1 and C2 is the difference between a property name and an OGNL string literal: single quotes make a literal,
exactly as they did in the pre-2.2 syntax described next. s:set shows the same behaviour with the var:
<s:set var="lit" value="filter"/> stores all, value="'filter'" stores the text filter and value="%{filter}"
stores all again (page data, line S7).
The Alt Syntax, History Only
The page named Alt Syntax in the Tag Developer Guide is now a history page, and it is the reason that older books and
blog posts look different. Before WebWork 2.1.4 every attribute was evaluated as an OGNL expression, so a literal needed
single quotes; the altSyntax option made only %{...} evaluate. It was on by default from WebWork 2.2 and a
struts.xml constant and a per-request flag could switch it off. Struts 2.6 removed both (WW-3877), so the %{...}
notation is the only syntax. The guide’s own before-and-after example is the clearest summary:
<%-- old syntax, expression everywhere --%>
<s:textfield label="'Cart item No.' + #rowstatus.index + ' note'"
name="'cart.items[' + #rowstatus.index + '].note'"
value="note" />
<%-- today: literal text, %{...} marks the expressions --%>
<s:textfield label="Cart item No. %{#rowstatus.index} note"
name="cart.items[%{#rowstatus.index}].note"
value="%{note}" />
A page copied from an old example in which value="note" was supposed to read the property note prints the literal text
note on Struts 7.4.0 (the third line of the experiment above), and a ${...} of the same era is JSP EL, which a Struts
attribute does not accept.
Property Names That Cannot Be Used
The Tag Syntax page lists seven property names as disallowed: parameters, application, session, struts,
request, servletRequest and servletResponse; its example, <s:iterator value="parameters"/> on an action with a
getParameters() getter, "will not work". On Tomcat 11.0.26 with Struts 7.4.0 an action with String[] getParameters()
was read normally:
D1 [<s:iterator value="parameters"><s:property/>,</s:iterator>]
D2 [<s:iterator value="getParameters()"><s:property/>,</s:iterator>]
D1 [x,y,]
D2 [x,y,]
The bare name resolves on the stack, the action is on the stack, so its property wins. The names are still a poor choice:
they are the names of the context map’s own objects, and #parameters, #session and #request with a # mean
something else on the same page; the rule to keep is not to give an action a property that has the name of a context
object.
The Characters of OGNL: the One-Character Trap
A quirk of OGNL deserves its own paragraph because it is the oldest question about the if tag (the guide’s FAQ has it).
In OGNL a single-quoted literal of one character is a char, not a String. Comparing the string a of the action to the
literal 'a' is therefore false:
<s:if test="one == 'a'">one is a</s:if><s:else>one is not a</s:else>
<s:if test='one == "a"'>one is a</s:if><s:else>one is not a</s:else>
<s:if test="one.length == 1">len</s:if>
<s:if test="one in {'a'}">in</s:if>
one is not a
one is a
len
(nothing)
The cure given by the FAQ is to flip the quotes: double quotes inside single quotes make a String. The same applies to
every test or value with a one-character literal; a literal of two or more characters is a String and needs no
care.
Expressions Outside Tag Attributes
JSP EL and OGNL live side by side on a page and do not read the same objects: ${...} is evaluated by the JSP
container and works in template text and in the attributes of JSP or JSTL tags, while OGNL works only in Struts tag
attributes. The Tag Syntax page lists the forms: ${user.username} reads a JavaBean in a standard context (JSTL, FreeMarker
or Velocity), <s:textfield name="username"/> reads a property on the stack, and #{'a':'b'} is a literal map. How
${filter} reaches the value stack (the request wrapper that falls back to the stack), why <s:property value="${x}"/>
fails and what changes with escaping is the subject of the value stack page; the section on JSTL below shows the
combinations that work.
Escaping the Body of a Tag
Since Struts 2.6 and the FreeMarker version that enables auto-escaping by default, the guide says to stop using
?html in custom tags and FreeMarker pages. For the tags whose body is markup the library offers
escapeHtmlBody: it exists on a, submit and component, and the global constant struts.ui.escapeHtmlBody sets it
for all three, with the attribute taking precedence over the constant. On the example page:
<s:a href="%{#attr.nothing}" escapeHtmlBody="true"><b>bold</b> & more</s:a>
<s:a action="stats" escapeHtmlBody="false"><b>bold</b> & more</s:a>
<a href=""><b>bold</b> & more</a>
<a href="/g10tags/stats.action"><b>bold</b> & more</a>
Control Tags
if, elseif and else
if performs basic condition flow. It can stand alone or be followed by any number of elseif tags and one else; an
elseif is only valid after an if and an else only after an if or elseif. The tag has one attribute besides the
pooling one: test (required), a Boolean expression that decides whether the body is written.
| Attribute | Required | Default | Description |
|---|---|---|---|
|
yes |
- |
Expression to determine if the body of the tag is displayed; evaluated as OGNL and converted to a boolean |
The conversion to a boolean is the one of OGNL, and it is where the surprises are. Every line below was requested on the
page control; the expressions and the output are:
IF1 [<s:if test="count > 3">many</s:if><s:elseif test="count > 1">some</s:elseif><s:else>none</s:else>]
IF2 [<s:if test="filter == 'all'">all</s:if>]
IF7 [<s:if test="nothing == null">nothing is null</s:if>]
IF8 [<s:if test="'foo' in {'foo','bar'}">in</s:if><s:if test="'foo' not in {'foo','bar'}">notin</s:if>]
IF10 [<s:if test="filter">str</s:if><s:else>strfalse</s:else>]
IF11 [<s:if test="'a'">lit-a</s:if><s:else>lit-a-false</s:else>]
IF12 [<s:if test="'true'">lit-true</s:if><s:else>lit-true-false</s:else>]
IF13 [<s:if test="'aa'">lit-aa</s:if><s:else>lit-aa-false</s:else>]
IF1 [many]
IF2 [all]
IF7 [nothing is null]
IF8 [in]
IF10 [strfalse]
IF11 [lit-a]
IF12 [lit-true]
IF13 [lit-aa-false]
Read the outputs against the conversion rule of OGNL: a boolean expression is used as it is (IF1, IF2, IF7), in and
not in test membership of a collection literal (IF8), a string that is not true converts to false, so a property that
holds the text all is false (IF10), a single-quoted one-character literal is a char, which converts to true (IF11),
the string 'true' converts to true (IF12) and any other string, 'aa', to false (IF13). The practical rule is to
write a real boolean expression, a comparison or a boolean property, and never to rely on the truthiness of a string. The
Getting Started tutorial on control tags uses the same tag with personBean.over21, a boolean getter, and with
personBean.carModels.length > 1 to choose between "Car model" and "Car models".
A second trap is the method call on a JDK collection. These tests were written the way a Java programmer would write
them, and then the way OGNL needs them (page control, lines IF6 and IF9):
IF6 [<s:if test="books.size() > 2">big</s:if>] [<s:if test="books.size > 2">big-prop</s:if>]
IF9 [<s:if test="noBooks.isEmpty()">empty</s:if>] [<s:if test="noBooks.empty">empty-prop</s:if>] [<s:if test="!books.empty">notempty-prop</s:if>]
IF6 [] [big-prop]
IF9 [] [empty-prop] [notempty-prop]
The method forms print nothing and the log has Declaring class [class java.util.AbstractCollection] of member type
[public abstract int java.util.AbstractCollection.size()] is not allowlisted; the property forms (books.size,
noBooks.empty, books[0]) read the same data through the bean introspection that the allowlist permits. The reason
and the alternative of allowlisting JDK classes are on the value stack page, which also explains why ! and not work
as negation.
The if tag is the cure for logic in the page, not an invitation to put it there: a condition that is more than a
comparison belongs in a boolean getter of the action (isOver21(), isEmpty()), which keeps the page readable and
testable.
iterator
iterator loops over a value. The iterable can be any java.util.Collection, java.util.Iterator,
java.util.Enumeration, java.util.Map or an array; the tag documentation adds that a value that is none of them is put
into a newly created list (the loop then runs once). For each pass the current element is pushed on the value stack,
so inside the loop the properties of the element are the bare names, and it is popped at the end of the pass.
| Attribute | Required | Default | Description |
|---|---|---|---|
|
no |
the object itself, in a new list |
The iterable source to iterate over ( |
|
no |
- |
Name under which the current element is also stored in the context (read it with |
|
no |
|
If specified, an |
|
no |
|
The index to start at |
|
no |
size of the source, or 0 if |
The index to end at, inclusive |
|
no |
|
The increment; it may be negative, in which case |
The basic loops, with the output of the example action (page control, lines IT1 to IT4):
IT1 [<s:iterator value="titles"><s:property/>,</s:iterator>]
IT2 [<s:iterator value="books"><s:property value="label"/>;</s:iterator>]
IT3 [<s:iterator value="books" var="b"><s:property value="#b.title"/>/<s:property value="title"/>;</s:iterator>]
IT1 [Dune,Neuromancer,Hyperion,Emma,]
IT2 [Dune (1965);Neuromancer (1984);Hyperion (1989);Emma (1815);]
IT3 [Dune/Dune;Neuromancer/Neuromancer;Hyperion/Hyperion;Emma/Emma;]
IT1 iterates over a list of strings: <s:property/> with no value prints the top of the stack, which is the current
string. IT2 iterates over beans and reads a property of the current one (label is getLabel() of Book, evaluated on
the element because the element is on the stack). IT3 shows that var gives the element a second name: #b.title
and title are the same book. The Getting Started tutorial on control tags builds a table the same way from the array
personBean.carModels, and says that if the collection holds objects with several fields, value of the nested
property selects the field.
The Status Object
With status the loop gets an IteratorStatus (org.apache.struts2.views.jsp.IteratorStatus), the object behind the
alternate-row-colour idiom of the guide. Its properties and method, from the 7.4.0 class and the output of the example
action (page control, line IT4, which prints index:count:first:last:odd:even:title for each of the four books):
| Property | Meaning | Observed for the four books |
|---|---|---|
|
Zero-based index of the current element |
|
|
One-based count, |
|
|
True for the first element |
|
|
True for the last element |
|
|
True when the count is odd |
|
|
True when the count is even |
|
|
The index and count as Strings (since 6.4.0) |
|
|
The remainder of the count divided by the argument, for an "every third row" rule |
|
IT4 [<s:iterator value="books" status="st"><s:property value="#st.index"/>:<s:property value="#st.count"/>:<s:property value="#st.first"/>:<s:property value="#st.last"/>:<s:property value="#st.odd"/>:<s:property value="#st.even"/>:<s:property value="title"/>;</s:iterator>]
IT4 [0:1:true:false:true:false:Dune;1:2:false:false:false:true:Neuromancer;2:3:false:false:true:false:Hyperion;3:4:false:true:false:true:Emma;]
The idiom the guide uses is <s:if test="#rowstatus.odd == true"> for the zebra stripes and <s:if test="!#userStatus.last">,</s:if>
to put a comma between the items and not after the last; both are pure expressions on status. Note that odd follows the
count, so the first row (index 0, count 1) is odd.
begin, end and step
The three attributes select a part of the source (page control, lines IT5 to IT8; end is inclusive):
IT5 [<s:iterator value="books" begin="1" end="2"><s:property value="title"/>,</s:iterator>]
IT6 [<s:iterator value="books" begin="0" step="2"><s:property value="title"/>,</s:iterator>]
IT7 [<s:iterator value="books" begin="3" end="0" step="-1"><s:property value="title"/>,</s:iterator>]
IT8 [<s:iterator begin="1" end="5" var="i"><s:property value="top"/>,</s:iterator>]
IT5 [Neuromancer,Hyperion,]
IT6 [Dune,Hyperion,]
IT7 [Emma,Hyperion,Neuromancer,Dune,]
IT8 [1,2,3,4,5,]
IT8 has no value: with only begin and end the tag counts, which is the Struts counterpart of c:forEach begin=
end= (the Part I page JSTL Iteration has the JSTL tag), and top is the current number. A second way to count to
n is an OGNL trick that the guide mentions, (n).{ #this }, a projection over the integers from 0 to n-1 (IT9):
IT9 [<s:iterator value="(3).{ #this }" status="stat"><s:property value="#stat.count"/>/<s:property/>,</s:iterator>]
IT9 [1/0,2/1,3/2,]
count is one-based and index zero-based, as the guide notes in its comment.
Maps, Literals, Selection and Projection
The value is an OGNL expression, so the collection operators of OGNL work in it (the page control, lines IT10 to IT17):
IT10 [<s:iterator value="{'a','b','c'}"><s:property/>,</s:iterator>]
IT11 [<s:iterator value="genres"><s:property value="key"/>=<s:property value="value"/>;</s:iterator>]
IT15 [<s:iterator value="books.{? #this.year > 1960}"><s:property value="title"/>,</s:iterator>]
IT16 [<s:iterator value="books.{title}"><s:property/>,</s:iterator>]
IT17 [<s:iterator value="books.{^ #this.genre == 'scifi'}"><s:property value="title"/>,</s:iterator>]
IT10 [a,b,c,]
IT11 [scifi=Science fiction;cyberpunk=Cyberpunk;classic=Classics;]
IT15 [Dune,Neuromancer,Hyperion,]
IT16 [Dune,Neuromancer,Hyperion,Emma,]
IT17 [Dune,]
A Map iterates over its entries, so key and value are the properties of each Map.Entry (IT11); a list literal
{'a','b','c'} is a List (IT10); .{? condition} is a selection of all matching elements (IT15, the books after
1960), .{^ condition} the first match and .{$ condition} the last (IT17); .{title} is a projection, the list of
one property (IT16), the guide’s more compact alternative to a loop over persons that prints person.firstName. A
source that is empty or null writes nothing and is not an error (IT12 and IT13 printed an empty line).
Nested Loops and the [n] Operator
Inside a loop the top of the stack is the element, below it is the action. When two nested loops need the outer element,
the [n] operator cuts the stack: [1] is the stack starting one element below the top. The guide uses it to pass the
current element to another action and to read a property that the inner element hides. The example (page control, IT14)
prints, for each outer book and inner genre, the outer book’s title read as [1].title:
<s:iterator value="books" status="o">
<s:iterator value="genres" status="i">
<s:property value="[1].title"/>-<s:property value="key"/>-<s:property value="#o.index"/><s:property value="#i.index"/>;
</s:iterator>
</s:iterator>
Dune-scifi-00;Dune-cyberpunk-01;Dune-classic-02;Neuromancer-scifi-10;Neuromancer-cyberpunk-11; ...
Source: Tag Developer Guide — iterator tag, Examples (the [1 operator)]
Giving each loop a status or a var (#o, #b) is more readable than counting levels with [n].
Conversion of Generated Numbers
Values that a tag generates (the numbers of a counting loop, count, index) go through the framework’s conversion when a
property tag prints them, and are formatted for the locale (the guide’s example uses fa_IR and prints Persian
digits). Since Struts 6.4.0 the status object has countStr and indexStr, which are Strings and are written as they are;
use them wherever the number is part of an id or a parameter name (id="text_%{#status.countStr}",
name="test[%{#status.indexStr}]"), as in the guide’s example. On the en_US example page countStr and indexStr
printed 1:0,2:1,3:2,4:3 (IT21), the same digits as the numeric properties.
Building and Reshaping Iterators: append, merge, generator, sort, subset
Five control tags create or transform iterators. Each one has a var that stores the result in the context (read with
#name, and for sort, subset and generator also in the page context as the documentation says), and each one
pushes the result on the stack for the duration of its body, so an iterator without a value inside it loops over the
result.
| Tag | Attributes | What it does |
|---|---|---|
|
|
Appends iterators: the entries of the first, then of the second and so on |
|
|
Merges iterators: one entry of each in turn (first of the first, first of the second, second of the first and so on) |
|
|
Splits a string into an iterator |
|
|
Sorts an iterable with a |
|
|
A slice of an iterable: from index |
The example action supplies a Comparator<Book> by year (getByYear()), a Decider for the genre scifi
(getScifiOnly()) and a Converter that upper-cases (getUpper()); the output of each tag (page control):
AP2 [<s:append var="all2"><s:param value="%{titles}"/><s:param value="%{{'x','y'}}"/></s:append><s:iterator value="#all2"><s:property/>,</s:iterator>]
MG1 [<s:merge var="m"><s:param value="%{titles}"/><s:param value="%{{'x','y'}}"/></s:merge><s:iterator value="%{#m}"><s:property/>,</s:iterator>]
GN1 [<s:generator val="%{'aaa,bbb,ccc,ddd'}" separator=","><s:iterator><s:property/>,</s:iterator></s:generator>]
GN2 [<s:generator val="%{'aaa,bbb,ccc,ddd'}" separator="," count="2"><s:iterator><s:property/>,</s:iterator></s:generator>]
GN3 [<s:generator val="%{'aaa,bbb,ccc,ddd'}" separator="," converter="%{upper}"><s:iterator><s:property/>,</s:iterator></s:generator>]
SO1 [<s:sort comparator="byYear" source="books"><s:iterator><s:property value="year"/>,</s:iterator></s:sort>]
SU2 [<s:subset source="books" count="2"><s:iterator><s:property value="title"/>,</s:iterator></s:subset>]
SU3 [<s:subset source="books" start="1" count="13"><s:iterator><s:property value="title"/>,</s:iterator></s:subset>]
SU4 [<s:subset source="books" decider="scifiOnly"><s:iterator><s:property value="title"/>,</s:iterator></s:subset>]
AP2 [Dune,Neuromancer,Hyperion,Emma,x,y,]
MG1 [Dune,x,Neuromancer,y,Hyperion,Emma,]
GN1 [aaa,bbb,ccc,ddd,]
GN2 [aaa,bbb,]
GN3 [AAA,BBB,CCC,DDD,]
SO1 [1815,1965,1984,1989,]
SU2 [Dune,Neuromancer,]
SU3 [Neuromancer,Hyperion,Emma,]
SU4 [Dune,Hyperion,]
Details that the examples settle:
-
appendplaces whole iterables one after the other,mergeinterleaves them: with the four titles andx,ythe results areDune,...,Emma,x,yandDune,x,Neuromancer,y,Hyperion,Emma. A merge ends each source as it runs out. -
generatorsplits withseparator(a missing separator is a translation error, because the TLD marks itrequired; the guide’s own table says so) andcountlimits the number of entries. AConverter(a lambda in the example action,org.apache.struts2.util.IteratorGenerator.Converter) turns each string into an object. The documentation’s example ofseparatorinExample 3combinesvarwith a scriptlet to read the iterator from the page context. -
sortneeds aComparatorthat the action provides; the iterable itself issource.subsethas no condition language of its own, aDecider(SubsetIteratorFilter.Decider) provides the filter, andstartpluscountbeyond the size is not an error (SU3 asked for 13 and got the 3 that remain). -
With
varand no body the result is stored in the page scope:<s:sort ... var="sorted"/>then#attr.sorted(or${sorted}) reads it, while#sorteddoes not (pagecontrol, lines SO2 and SO3:#sortedprinted nothing and#attr.sortedprinted1815,1965,1984,1989). Theappendandmergetags, which document a contextvar, are read as#all2(AP2 and MG1 above). -
An empty iterable in the middle of an
appendends the append:appendof the titles, an empty list andx,yprinted only the four titles (AP1),appendof an empty list first printed nothing (AP3, AP5), whereas the same append without the empty list printed the four titles andx,y(AP2, AP4 with the empty list last). Filter out empty lists beforeappend, or usemerge, which skipped the empty list in the same test (mergeof the titles, an empty list andx,yprintedDune,x,Neuromancer,y,Hyperion,Emma).
Which Tag to Use
The guide does not rank the iterator tags; this table does, from the experiments above and the Bookshelf’s needs.
| Need | Tag | Comment |
|---|---|---|
Show a block only if a condition holds |
|
Put the condition in a boolean getter when it is more than a comparison |
Loop over a collection, map or array |
|
|
Count from |
|
There is no |
Show one page of a list |
|
A real paging needs the slice in the action or the repository, not in the view |
Show a list in a new order |
|
A sort in the view re-sorts on every render |
Join two lists |
|
Prefer building the combined list in the action |
Split a delimited string |
|
The one place where a string is data in the view; localised lists belong in a bundle |
Data Tags
property
property writes the value of an expression, converted to a string and HTML-escaped. It is the tag of every page of this
section and the one with the most useful attributes.
| Attribute | Required | Default | Description |
|---|---|---|---|
|
no |
the top of the stack |
The expression whose value is written |
|
no |
- |
Text written if the value is |
|
no |
|
Escape HTML |
|
no |
|
Escape XML |
|
no |
|
Escape JavaScript |
|
no |
|
Escape CSV (a value for a column) |
Several lines of the page data show the attributes at work. The action’s markup property is <b>Dune</b> & "more":
P1 [<s:property value="filter"/>] [<s:property value="markup"/>] [<s:property value="markup" escapeHtml="false"/>]
P3 [<s:property value="nothing"/>] [<s:property value="nothing" default="n/a"/>] [<s:property value="nonexistent" default="n/a"/>]
P8 [<s:property/>]
P11 [<s:property value="books[0]"/>]
P1 [all] [<b>Dune</b> & "more"] [<b>Dune</b> & "more"]
P3 [] [n/a] [n/a]
P8 [org.example.bookshelf.ShelfAction@baa2886]
P11 [org.example.bookshelf.model.Book@4b0f8ea9]
The default escaping is the first line of defence against cross-site scripting: the markup of the property is shown as
text. escapeHtml="false" writes it raw and must be reserved for values that the application itself produced as markup.
default replaces a null (a property that returns null, or one that does not exist, as P3 shows); an empty string
is a value and is not replaced. A property tag with no value prints the top of the stack, which outside any loop is the
action, hence P8, and an object that has no toString() of its own prints the class name and a hash code (P11). The
Getting Started tutorial recommends overriding toString() of the model classes for this reason; property converts
numbers, booleans and dates to strings by itself (the count, balance and featured properties printed 4, 1234.5 and
true; a Date printed 9/13/65, the short date format of the locale, not a format of your choice, which is what s:date
is for).
The three extra escapes stack on top of the default escapeHtml="true", which makes the combined output surprising;
switch the HTML escape off when one of the others is the one needed (page data, lines P2 and PE1):
PE1 [<s:property value="markup" escapeHtml="false" escapeXml="true"/>]
[<s:property value="markup" escapeHtml="false" escapeJavaScript="true"/>]
[<s:property value="markup" escapeHtml="false" escapeCsv="true"/>]
[<s:property value="markup" escapeXml="true"/>]
[<b>Dune</b> & "more"]
[<b>Dune<\/b> & \"more\"]
["<b>Dune</b> & ""more"""]
[&lt;b&gt;Dune&lt;/b&gt; &amp; &quot;more&quot;]
The last line is the double escape: HTML escaping and then XML escaping turns < into <. Each escape is for a
different context: escapeHtml for HTML text, escapeJavaScript for a value inside a JavaScript string (and the quotes
and the slash are then backslash-escaped), escapeCsv for a CSV column (the value is quoted and a quote is doubled).
Pick one, set the others off, and never write a property inside a <script> block with escapeHtml alone.
set
set assigns a value to a variable in a scope. It is the tag for a complex expression that a page uses several times: the
expression is evaluated once and the rest of the page reads the variable, which is both faster and more readable.
| Attribute | Required | Default | Description |
|---|---|---|---|
|
yes |
- |
The name of the variable |
|
no |
the body |
The expression to assign; if the tag has a body, the value attribute is not evaluated and the body’s text is the value |
|
no |
|
|
|
no |
|
Set to |
S1 [<s:set var="first" value="books[0]"/><s:property value="#first.title"/>] [<s:property value="#attr.first.title"/>]
S2 [<s:set var="h">Hello <s:property value="filter"/> </s:set>"<s:property value="#h"/>"]
S3 [<s:set var="h2" trimBody="false"> Hello </s:set>"<s:property value="#h2"/>"]
S6 [<s:set var="cnt" value="count"/><s:property value="#cnt + 1"/>]
S1 [Dune] [Dune]
S2 ["Hello all"]
S3 [" Hello "]
S6 [5]
With a body, the body’s text (with the tags it contains evaluated) is the value (S2), and its leading and trailing
whitespace is trimmed unless trimBody="false" (S3). Where the variable can be read depends on the scope, and the
visibility differs for each way of reading it. The table summarises the observation of the page scope, which reads one
variable of each kind with #name, #attr.name, #request.name and the JSP EL implicit objects pageScope,
requestScope and ${name}:
Scope of the set tag |
#x |
#attr.x |
${x} |
Comment |
|---|---|---|---|---|
Default scope |
yes |
yes |
yes |
Stored in the context map and in the page scope; |
|
no |
yes |
yes |
Only a request attribute: |
|
no |
yes |
yes |
A page-scope variable: |
|
no |
yes |
yes |
Read with |
The var of the other tags that have one (iterator, bean, url, text, date, number, action) is stored in the context
map only and is read with #name (${name} also works through the request wrapper, as the loops with ${b2.title} in the
JSTL section show). The Struts docs of the url tag add that, with var, the generated URL is placed in the request
scope in addition to the action scope, and that a text tag with var writes nothing to the page.
push
push pushes a value onto the value stack for the duration of its body, so the properties of that object become the bare
names inside it; it is the tag form of "work with this object". It has one required attribute, value.
PU1 [<s:push value="books[0]"><s:property value="title"/> by <s:property value="author"/> / top=<s:property value="top.label"/> / [1]=<s:property value="[1].filter"/></s:push>]
PU2 [<s:push value="books[0]"><s:push value="books[1]"><s:property value="title"/>|<s:property value="[1].title"/></s:push></s:push>]
PU1 [Dune by Frank Herbert / top=Dune (1965) / [1]=all]
PU2 [Neuromancer|Dune]
Inside the body title and author are properties of the pushed Book, top is that book and [1] is the stack
without it, so [1].filter reads the action’s property. The nested push of PU2 shows the order of the stack: the inner
element is on top, and [1].title reads the outer book. The guide’s examples use [1].top and top.property the same way
to explain what each param of a bean sees. push is a convenience: a loop over one element is rarely needed in a page,
and a model object is better exposed by ModelDriven (see ModelDriven, Preparable & CRUD) than pushed in the view.
bean and param
bean instantiates a class that conforms to the JavaBeans specification, sets properties through nested param tags and
pushes the bean on the stack for the body; with var it is also stored in the context, for later use. The class needs a public
no-argument constructor and setters for the parameters, and, as for every object that OGNL touches, its package must
be allowlisted: with a helper class in a package that was not (org.example.other.Helper), the tag created the object
but its param setter and its getter were refused (is not allowlisted in the log) and the properties printed nothing.
The attributes are name (required, the class name) and var.
BE1 [<s:bean name="org.example.bookshelf.model.Reader" var="reader"><s:param name="name" value="'Ana'"/><s:param name="city">Lima</s:param><s:property value="name"/>/<s:property value="city"/></s:bean>] [<s:property value="#reader.name"/>] [<s:property value="name"/>]
BE3 [<s:bean name="org.example.bookshelf.model.Reader" var="r2"><s:param name="name" value="%{filter}"/></s:bean><s:property value="#r2.name"/>]
BE1 [Ana/Lima] [Ana] []
BE3 [all]
Inside the body the bean is on top of the stack (name and city read its properties); outside, the stack is popped
(the third value of BE1 is empty) and the var is the way back (#reader.name). The bean tag is a view-side
object factory and it creates objects in the view that no action tests; use it for a helper such as a formatter or a
counter, not for domain objects. A class that cannot be loaded is a silent failure: with name="org.example.bookshelf.NoSuch"
the log had a ClassNotFoundException, the tag wrote nothing, and every expression after it on the page, even
<s:property value="filter"/>, printed nothing (page bd), because the failed bean was left on the stack. A page that suddenly
goes blank after a bean tag has a mistyped class name.
The param tag parameterises the tag that contains it: bean, include, url, a, text, action, append, merge
and component are the tags that read it. It has name, value and suppressEmptyParameters. The guide stresses a
difference between the two ways to give a value, which is rule 1 versus rule 2 in a new place:
<s:param name="color">blue</s:param> <%-- (A) body: the text "blue", a java.lang.String --%>
<s:param name="color" value="blue"/> <%-- (B) attribute: the OGNL expression "blue", an Object --%>
The second form is the one that catches people out. value is an expression, blue is not a property, so the value is
null; to pass the text it must be a literal, value="'blue'", or the body form. On the example page:
PA3 [<s:bean name="org.example.bookshelf.model.Reader" var="r5"><s:param name="city" value="Lima"/></s:bean><s:property value="#r5.city"/>]
[<s:bean name="org.example.bookshelf.model.Reader" var="r6"><s:param name="city" value="'Lima'"/></s:bean><s:property value="#r6.city"/>]
[<s:bean name="org.example.bookshelf.model.Reader" var="r7"><s:param name="city" value="%{'Lima'}"/></s:bean><s:property value="#r7.city"/>]
PA3 [] [Lima] [Lima]
The empty first value is null from the unquoted Lima. The same mistake appears in the FAQ of the guide about locale links
(<s:param name="request_locale" value="en" />), where the quotes are missing. suppressEmptyParameters leaves an empty
parameter out of the URL, see the url tag below.
action
action executes an action from within a view, and optionally renders its result in place: a way to compose a page from
the output of several actions (a "portlet" of the shelf’s statistics on every page). Its attributes are name (required,
the action name without extension), namespace (default: the namespace of the page), executeResult (default false),
var, flush (default true), ignoreContextParams (default false, so the request parameters of the page are passed
to the inner action) and rethrowException (default false). Nested param tags give parameters to the action.
AC1 [<s:action name="stats" executeResult="false" var="st1"/><s:property value="#st1.total"/>] [<s:property value="#st1.genre"/>]
AC2 [<s:action name="stats" executeResult="true"/>]
AC4 [<s:action name="stats" var="st4" executeResult="false"><s:param name="genre" value="'scifi'"/>inside=<s:property value="#st4 == null"/></s:action> outside=<s:property value="#st4 == null"/> genre=<s:property value="#st4.genre"/>]
AC8 [<s:action name="stats-featured" var="st8" executeResult="false"/><s:property value="#attr.note"/>]
AC1 [4] [none]
AC2 [STATS total=4 genre=none heading=Shelf statistics note=]
AC4 [inside=true outside=false genre=scifi]
AC8 [set by featured()]
AC1 runs the action and reads its properties through the var; AC2 runs it and writes the result (the JSP stats.jsp) into
the page; AC4 confirms the guide’s "Placement in context" rule, the action is published to the context only when the whole
tag has finished, so inside the body #st4 is still null and outside it is not; AC8 shows a method call that works.
Three behaviours of this tag differ from what the guide’s text implies:
-
The guide and its FAQ say that a method other than
executeis selected with the "bang" syntax,<s:action name="actionTagAction!specialMethod" executeResult="true"/>. In Struts 7.4.0 dynamic method invocation is off (struts.enable.DynamicMethodInvocation=falseindefault.properties), andstats!featureddid not callfeatured()(thenoteattribute stayed empty, AC3); a mapping withmethod="featured"(thestats-featuredaction of the example) is the form that works (AC8). Wildcard mappings with{1}give the same effect for many methods; the page Wildcard Mappings & Method Invocation has the details. -
An exception thrown by the inner action does not stop the page by default: with
rethrowExceptionleft atfalsethe tag logs the error and goes on (AC5, theboomaction: the variable was set and the page continued). WithrethrowException="true"the exception propagates out of the tag, the page stops where it is (the browser received the text before the tag with status 200 and the container then reported thatsendError()could not be called on a committed response). -
An inner action runs the full interceptor stack, including validation and the workflow interceptor, which is why the FAQ says that an action tag whose action has validation errors does not execute its method; the default stack excludes
inputfrom validation, so!inputis the documented way around it.
The action tag is a convenience with a cost: it executes a whole request cycle inside the page, so a page with ten of them
runs ten action stacks. For a reusable fragment of a page a JSP include or a tag file is lighter (see the include tag
below, and the Part I page Tag Files & Layouts).
text and i18n
text renders an internationalised message from a resource bundle, i18n puts another bundle on the stack for its body. The
lookup order of the bundles and the locale selection are the subject of the Localization and i18n page, which also describes the
key attribute of the form tags; this section is the tag side. text has name (required, the key), var and the four
escape flags (all false by default, which differs from property: messages are not HTML-escaped).
TX1 [<s:text name="book.heading"/>] [<s:text name="shelf.count"><s:param value="count"/></s:text>] [<s:text name="shelf.price"><s:param value="balance"/></s:text>] [<s:text name="no.such.key"/>] [<s:text name="no.such.key">fallback body</s:text>]
TX2 [<s:text name="book.heading" var="hd"/>after-var:<s:property value="#hd"/>]
TX3 [<s:i18n name="org.example.bookshelf.Messages"><s:text name="welcome"/> | <s:text name="shop.hours"><s:param>9</s:param><s:param>17</s:param></s:text></s:i18n>]
# ShelfAction.properties
book.heading=Books on the shelf
shelf.count=The shelf holds {0} books
shelf.price=Price: {0,number,currency}
# Messages.properties
welcome=Welcome to the Bookshelf
shop.hours=Open from {0} to {1}
TX1 [Books on the shelf] [The shelf holds 4 books] [Price: $1,234.50] [no.such.key] [fallback body]
TX2 [after-var:Books on the shelf]
TX3 [Welcome to the Bookshelf | Open from 9 to 17]
Nested param tags fill the {0}, {1} placeholders of the MessageFormat pattern, a missing key prints the key, or the
body when the tag has one, and a text with a var writes nothing and stores the text. The text is written without
HTML escaping (add escapeHtml="true" if a bundle can contain markup you did not write). Three variations are different
from the guide’s text:
-
In the Text tag page,
<s:property value="text('aaa.bbb.ccc')"/>insides:i18nis shown as the way to read the pushed bundle; on 7.4.0text('...')printed nothing,getText('welcome')printed the keywelcome(the action does not know the pushed bundle) ands:textis the tag that works. -
A tag attribute cannot contain a tag:
<s:textfield label="<s:text name="person.lastName"/>"/>is invalid JSP. Uselabel="%{getText('person.lastName')}", with the notation, as the previous section showed. -
In the example the
shelf.pricepattern uses the default locale ($on this JVM); a pattern like{0,number,currency}follows the locale of the request.
date
date formats a Date (or java.time type) that is on the stack, in one of three ways: a format pattern, the nice
relative form ("in 2 hours, 14 minutes"), or, with neither, the struts.date.format key of the bundles and finally the
DateFormat.MEDIUM format of the locale. name (required) is the date expression, format, nice (default false),
timezone and var are optional. If the value is missing from the stack the tag writes nothing.
DT1 [<s:date name="published" format="dd/MM/yyyy" timezone="UTC"/>] [<s:date name="published" format="yyyy-MM-dd'T'HH:mm" timezone="UTC"/>] [<s:date name="nothing"/>]
DT2 [<s:date name="published" nice="true"/>]
DT3 [<s:date name="published" format="yyyy" var="py"/>var:<s:property value="#py"/>]
DT1 [13/09/1965] [1965-09-13T00:00] []
DT2 [61 years, 42 days ago]
DT3 [var:1965]
nice="true" produces the relative text with the struts.date.format.past, future, seconds, minutes, hours, days
and years keys ({0} ago, in {0} and the choice patterns of the table in the guide), which are the keys to
override in a bundle to translate it; the DT2 output changes with the day it is requested. The formatter is selected by the
constant struts.date.formatter (default dateTimeFormatter, the java.time formatter; simpleDateFormatter selects
the legacy one), and the patterns of the two differ slightly in letters, so a pattern that worked with SimpleDateFormat
needs a check when the formatter is changed. The Localization and i18n page has the traps of a date pattern placed in a bundle
(a MessageFormat passes over it first and swallows a single quote).
number
The number tag formats a number. It is in the 7.4.0 tag library descriptor and the org.apache.struts2.components.Number
class, and it is not in the Tag Reference’s list of generic tags or on a page of its own (the address
tag-developers/number-tag.html answers 404), so the TLD is the documentation. Its attributes are name (required, the
number expression), type (currency, integer, number or percent, default number), currency (a currency code),
groupingUsed, minimumFractionDigits, maximumFractionDigits, minimumIntegerDigits, maximumIntegerDigits,
parseIntegerOnly, roundingMode (ceiling, down, floor, half-down, half-even, half-up, unnecessary, up)
and var.
NU1 [<s:number name="balance"/>] [<s:number name="balance" type="currency" currency="EUR"/>] [<s:number name="0.256" type="percent"/>]
[<s:number name="balance" maximumFractionDigits="0" roundingMode="half-up"/>] [<s:number name="balance" type="integer"/>]
[<s:number name="balance" groupingUsed="false"/>]
NU1 [1,234.5] [€1,234.50] [26%] [1,235] [1,234] [1234.5]
The formatting is locale-aware (en_US here); the integer type truncates by the RoundingMode of the format (1,234 from
1234.5 with the default half-even rule), while roundingMode="half-up" with maximumFractionDigits="0" printed 1,235.
include
include writes the output of a servlet or a JSP page into the page; value (required) is the path. Parameters are
nested param tags and become request parameters of the included resource.
IN1 [<s:include value="/WEB-INF/views/included.jsp"/>]
IN2 [<s:include value="/WEB-INF/views/included.jsp"><s:param name="title" value="books[0].title"/><s:param name="who">Ana</s:param></s:include>]
INC title=${param.title} who=${param.who} / stackfilter=[<s:property value="filter"/>] / nostack=[<s:property value="#attr.title"/>]
IN1 [INC title= who= / stackfilter=[all] / nostack=[]]
IN2 [INC title=Dune who=Ana / stackfilter=[all] / nostack=[]]
Parameters are read in the included page with ${param.title}, as the guide explains in "How to access parameters" (and
its s:set var=... scope="page" examples copy them into the OGNL world): they are not properties on the stack and
<s:property value="#attr.title"/> is empty. The guide says no value stack is created for the included page, so that
s:property cannot read the parameters; on 7.4.0 the page still sees the request’s value stack (filter of the action
printed all inside the included JSP, which is the same request), so the sentence is about the parameters, not about the stack.
Compare with the standard jsp:include and c:import of the Part I page Includes, Forwards & Page Composition, which have
the same request but not the OGNL view; a tag file is the better unit of reuse when the fragment has parameters of its own.
url and a: Building Links
url creates a URL, a creates an HTML <a> with the same attributes. They know the action mapping: action
(a name, with optional namespace and method) is turned into the right path with the context path and the extension, and
the parameters are URL-encoded.
| Attribute | Required | Default | Description |
|---|---|---|---|
|
no |
- |
The action to generate the URL for, if not using |
|
no |
- |
The target, if not using |
|
no |
the namespace of the page, none |
The namespace and the method of the action |
|
no |
- |
Variable that receives the URL; also in the request scope |
|
no |
- |
The |
|
no |
|
|
|
no |
|
Whether the context path is included |
|
no |
|
Whether to encode (session id rewriting) |
|
no |
|
Whether |
|
no |
|
Make the URL absolute |
|
no |
- |
Portlet support, ignored in a servlet container |
The page data (lines UR1 to UR8, requested with a cookie jar, so no ;jsessionid= is appended) prints:
UR1 [<s:url action="stats"/>] [<s:url value="/img/logo.png"/>] [<s:url value="logo.png"/>]
UR2 [<s:url action="stats"><s:param name="genre" value="'sci fi'"/></s:url>]
UR3 [<s:url action="stats" var="u"><s:param name="id" value="1"/><s:param name="x" value="2"/></s:url><s:property value="#u"/>] [<s:property value="#u" escapeHtml="false"/>] [${u}] [<s:url action="stats" escapeAmp="false"><s:param name="id" value="1"/><s:param name="x" value="2"/></s:url>]
UR4 [<s:url action="stats" anchor="top"/>] [<s:url action="stats" includeContext="false"/>] [<s:url action="stats" forceAddSchemeHostAndPort="true"/>] [<s:url action="stats" scheme="https" forceAddSchemeHostAndPort="true"/>]
UR5 [<s:url includeParams="get"><s:param name="id" value="'22'"/></s:url>] [<s:url includeParams="all" action="stats"/>] [<s:url includeParams="none"/>]
UR6 [<s:url action="stats"><s:param name="g" value="titles"/></s:url>]
UR8 [<s:url action="stats"><s:param name="a" value="nothing"/><s:param name="b" value="nothing" suppressEmptyParameters="true"/><s:param name="d" value="''"/></s:url>]
UR1 [/g10tags/stats.action] [/g10tags/img/logo.png] [logo.png]
UR2 [/g10tags/stats.action?genre=sci+fi]
UR3 [/g10tags/stats.action?id=1&amp;x=2] [/g10tags/stats.action?id=1&x=2] [/g10tags/stats.action?id=1&x=2] [/g10tags/stats.action?id=1&x=2]
UR4 [/g10tags/stats.action#top] [/stats.action] [http://localhost:18080/g10tags/stats.action] [https://localhost/g10tags/stats.action]
UR5 [/g10tags/data.action?q=zz&id=22] [/g10tags/stats.action?q=zz] [/g10tags/data.action]
UR6 [/g10tags/stats.action?g=Dune&g=Neuromancer&g=Hyperion&g=Emma]
UR8 [/g10tags/stats.action?a=&d=]
What the outputs teach:
-
actionproduces the context path, the action name and the extension (.action);valuewith a leading slash is context-relative and without it is left as written (UR1,logo.png). Avaluethat starts with a scheme (http://example.org/p) is used as is. -
The parameters are encoded (a space is
+) and, by default, the&between parameters is written as&, correct inside an HTML attribute (UR3). When the URL goes through as:propertythat escapes again, the result is double-escaped (&); the guide says to setescapeAmp="false"for that case, and the equivalent is to print the variable withescapeHtml="false"or to read it as${u}, which is not escaped by the tag at all. For a URL in a JavaScript string or a<meta refresh>choose the flag that matches the context. -
includeParamsis the cause of the question of the FAQ, "why are request parameters appended to our hyperlinks": it copies the parameters of the current request into the generated URL. The default isnonesince Struts 2.1.3 (constantstruts.url.includeParams; the FAQ still says the default isGET),getadds the query-string parameters (UR5:q=zz) andalladds all of them; a parameter given with a nestedparamtakes precedence over an included one (the guide’s Example 3,id=22). Never build a URL from user input withincludeParamsunless the values are meant to be passed on. -
A parameter whose value is an array or an
Iterableis repeated for each element (UR6), and anullvalue is written as an empty parameter (UR8,a=) unlesssuppressEmptyParameters="true"on theparam(the parameterbis gone). -
forceAddSchemeHostAndPortmakes the URL absolute (UR4); the port ofscheme="https"is the one of the constantstruts.url.https.port(443, so it is omitted), andstruts.url.http.portis 80. -
The
encodeattribute removes the session-id rewriting ofresponse.encodeURL(): a first request without a cookie got;jsessionid=...on every action URL and on/img/logo.png, and a<s:url value="/x" encode="false">got none, while the parameters were still encoded.
A1 [<s:a action="stats">Stats</s:a>] [<s:a href="/x/y.html">Plain</s:a>] [<s:a action="stats" cssClass="btn" id="st" title="Go" anchor="top"><s:param name="genre" value="'scifi'"/>Go</s:a>]
A2 [<s:url var="du" action="stats"><s:param name="genre" value="'x'"/></s:url><s:a href="%{#du}">By var</s:a>]
A4 [<s:a action="stats" onclick="alert(1)" data-role="x" cssStyle="color:red">dyn</s:a>]
A1 [<a href="/g10tags/stats.action">Stats</a>] [<a href="/x/y.html">Plain</a>] [<a id="st" href="/g10tags/stats.action?genre=scifi#top" class="btn" title="Go">Go</a>]
A2 [<a href="/g10tags/stats.action?genre=x">By var</a>]
A4 [<a href="/g10tags/stats.action" style="color:red" onclick="alert(1)" data-role="x">dyn</a>]
The a tag takes the attributes of url (A1 and A2), an explicit href (taken literally, with no context path, A1) and
the HTML ones: id, cssClass, cssStyle, title, the on... events and any dynamic attribute (data-role, A4). The
guide’s recommendation is to build a URL with several parameters in a s:url with a var and give the variable as
href="%{#du}" (A2), which keeps the parameters readable. The Getting Started tutorial introduces the tag the
simple way (a plain HTML a with the URL of an s:url inside the href):
<p><a href="<s:url action='hello'/>">Hello World</a></p>
<s:url action="hello" var="helloLink">
<s:param name="userName">Bruce Phillips</s:param>
</s:url>
<p><a href="${helloLink}">Hello Bruce Phillips</a></p>
The second link reads the variable with the JSP EL ${helloLink} because url with var stores the URL in the request
scope too. The tutorial notes that the query-string parameter is URL-encoded (Bruce+Phillips).
debug
debug prints the contents of the value stack and the context, but only if struts.devMode is on; otherwise it writes
nothing. On the example WAR in production mode the output of <s:debug/> was empty (page data, line DB1); a second
build of the same WAR with struts.devMode=true printed a [Debug] link, a hidden div and a "Struts ValueStack Debug"
table with the stack contents and the property names, values and classes of the action. The script that toggles the
div carries a CSP nonce.
Source: Tag Developer Guide — debug tag
The tag shows the stack exactly as OGNL sees it, which is the quickest way to find out why an expression prints nothing; it
must never be left on a deployed page, because the table lists every property of the action. The
struts.devMode constant is documented on the Configuration: struts.xml & Constants page of this section.
Asset Tags: compress, script, link and webjar
The newest generic tags look after the static parts of a page: its whitespace, its scripts and styles and the third-party libraries. They are what a Struts page needs to stay compatible with a Content Security Policy.
script and link, and the CSP Nonce
script writes a <script> element and link writes a <link> element; both automatically add a nonce attribute when
one is available, and are meant to be used with Struts' CSP interceptor. In Struts 7.4.0 the interceptor csp is part of
the defaultStack (the stack trace of an error page lists CspInterceptor between i18n and prepare), so the page
below, with no configuration at all, got a nonce, and the response had the header:
Content-Security-Policy-Report-Only: object-src 'none'; script-src 'nonce-HtixKojr6dS0VH8PCp552muX' 'strict-dynamic' http: https:; base-uri 'none';
The default mode of the interceptor is report only, so the browser reports violations and does not block; the constants
struts.csp.nonce.source (session, the default, or request) and struts.csp.report.maxSize (8192) are in
default.properties (since 7.4.0 the older name struts.csp.nonceSource is deprecated), and the interceptor’s
enforcingMode parameter switches to blocking. The page
Security Headers: CSP, COOP, COEP & Fetch Metadata has the complete treatment of the CSP, Fetch Metadata and COOP/COEP interceptors.
<s:url var="myCss" value="/styles/my-style.css" encode="false"/>
<s:link href="%{myCss}" rel="stylesheet" type="text/css" media="all"/>
<s:webjar var="jq" path="jquery/jquery.min.js"/>
<s:script src="%{jq}"/>
<s:script>
var shelf = '<s:property value="filter"/>';
</s:script>
<s:script async="true" defer="true" type="module" src="/app.js" id="appjs"/>
<link rel="stylesheet" type="text/css" href="/g10tags/styles/my-style.css" media="all" nonce="3E9WuxhDqlqNwhvpWf2Jg7ov"/>
<script src="/g10tags/static/webjars/jquery/3.7.1/jquery.min.js" nonce="3E9WuxhDqlqNwhvpWf2Jg7ov">
</script>
<script nonce="3E9WuxhDqlqNwhvpWf2Jg7ov">
var shelf = 'all';</script>
<script async defer type="module" src="/app.js" nonce="3E9WuxhDqlqNwhvpWf2Jg7ov">
</script>
The attributes of script are the HTML ones (src, async, defer, type, nomodule, integrity, crossorigin,
referrerpolicy, charset) plus id and dynamic attributes; those of link are href, rel, type, media, as,
crossorigin, hreflang, referrerpolicy, sizes and the dynamic ones. The page notes that both inherit all the
fields of the Component class, so some attributes the TLD lists (theme, template) have no effect on them. Two details
in the output: the s:property inside the script body is expanded and escaped for HTML (a title with a quote would show
as ", so a value for JavaScript needs escapeJavaScript="true" and escapeHtml="false"), and the element is
written with its body on separate lines. The nonce is the same for every element of a page and is new for every request: three requests with the same
session cookie printed three different nonces. With the default source session it is generated on each request and stored
in the session attribute nonce (only for a session that already exists), with request in a request attribute.
webjar
webjar (since Struts 7.3.0) resolves a version-less WebJar path to a servable URL. A WebJar packages client-side
libraries as a JAR that has its assets under META-INF/resources/webjars/<name>/<version>/; with the library on the class
path as a Maven dependency, the tag writes the URL with the version that is present, so the page never repeats it and a
dependency upgrade needs no change in the templates. Struts serves the assets under /static/webjars/** (the constant
struts.ui.staticContentPath and the master switch struts.webjars.enabled, default true, an optional
struts.webjars.allowlist of names), and resolves the version with webjars-locator-lite, which struts2-core bundles.
<link rel="stylesheet" href="<s:webjar path="bootstrap/css/bootstrap.min.css"/>"/>
W1 [<s:webjar path="bootstrap/css/bootstrap.min.css"/>] [<s:webjar path="jquery/jquery.min.js"/>] [<s:webjar path="nosuch/x.js"/>] [<s:webjar path="../etc/passwd"/>]
<link rel="stylesheet" href="/g10tags/static/webjars/bootstrap/5.3.8/css/bootstrap.min.css"/>
W1 [/g10tags/static/webjars/bootstrap/5.3.8/css/bootstrap.min.css] [/g10tags/static/webjars/jquery/3.7.1/jquery.min.js] [] []
An unknown WebJar and a path with .. both produced an empty string (the page of the Core Developer Guide says unresolved,
traversal and allowlist-blocked requests all fail closed: a 404 when serving and empty output when building URLs). The tag
has the attributes path (required) and var, and composes with script, link and a raw <link> or <script>, as in
the listing above.
compress
compress (since Struts 7.2.0) removes the whitespace between the tags of its body while keeping the content, to reduce
the size of generated HTML. It is enabled globally by struts.tag.compress.enabled (default true) and is switched off
automatically in development mode so that the output stays readable; force="true" overrides that. The body size is
limited by struts.tag.compress.maxSize (default 10485760, 10 MB, content above it is skipped without compression) and the
log lines show at most struts.tag.compress.log.maxLength (200) characters of the body, so sensitive text is not
written to the log.
<s:compress>
<div>
<p>Compressed text</p>
<span>between tags</span>
</div>
<pre>
keep this
</pre>
</s:compress>
<s:compress force="true"><ul>
<li>one</li>
<li>two</li>
</ul></s:compress>
<div><p>Compressed text</p><span>between tags</span></div><pre>
keep this
</pre>
<ul><li>one</li><li>two</li></ul>
Whitespace between tags is removed and whitespace inside text and inside <pre> is kept, so the tag does not change
what a browser renders. In the devMode build the first block was left alone and the second, with force="true", was
compressed. The documentation lists the safeguards: bodies above the size limit are left as they are, the regular
expressions have a ReDoS guard with a hard 50 MB limit and the log text is truncated. The saving is small for pages that
are served compressed by the container or a proxy; the tag is for pages generated with much indentation.
Mixing Struts Tags with JSTL and EL
Struts tags and JSTL (Jakarta Tags 3.0, jakarta.tags.core) coexist on a page, and the Getting Started tutorial uses plain
${...} in the same page as an s:url. What makes it work is that Struts wraps the request in StrutsRequestWrapper
whose getAttribute() falls back to the value stack, so JSP EL names resolve to action properties. The experiments of the
page jstl (the Tags 3.0 jars of the example WAR, prefix c):
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
J1 [<c:forEach items="${books}" var="b">${b.title};</c:forEach>]
J2 [${filter}] [${count}] [${books[0].label}] [<c:out value="${markup}"/>] [${markup}]
J3 [<s:set var="sv" value="'from-s-set'"/>${sv}] [<c:set var="cv" value="from-c-set" scope="request"/><s:property value="#request.cv"/>] [<s:property value="#attr.cv"/>] [<s:property value="cv"/>] [<s:property value="#cv"/>]
J5 [<s:iterator value="books" var="b2">${b2.title}/<s:property value="title"/>;</s:iterator>]
J6 [<s:iterator value="books"><c:out value="${title}"/>,</s:iterator>]
J8 [<c:if test="${count > 3}">c-if</c:if>] [<s:if test="#attr.cv == 'from-c-set'">s-if</s:if>]
J1 [Dune;Neuromancer;Hyperion;Emma;]
J2 [all] [4] [Dune (1965)] [<b>Dune</b> & "more"] [<b>Dune</b> & "more"]
J3 [from-s-set] [from-c-set] [from-c-set] [] []
J5 [Dune/Dune;Neuromancer/Neuromancer;Hyperion/Hyperion;Emma/Emma;]
J6 [Dune,Neuromancer,Hyperion,Emma,]
J8 [c-if] [s-if]
The combinations that were verified and those that were not:
| Goal | Struts | JSTL and EL |
|---|---|---|
Read an action property |
|
|
Loop over the action’s list |
|
|
Variable set by a Struts tag, read in EL |
|
works |
Variable set by |
|
bare |
Variable or element of a Struts loop in EL |
|
works; inside the loop |
Escaping |
|
|
Expression in a tag attribute |
|
|
Two rules keep the mixture sane. First, escape on output: property escapes by default and ${x} does not, so a user-supplied
value read with EL needs c:out or fn:escapeXml, whereas a property is already safe. Second, pick one language per
expression: do not try to pass EL into a Struts attribute, and when a JSTL variable is needed by a Struts tag read it with
#attr.name or %{#attr.name}. The OGNL page of the Tag Developer Guide has the same example in the other direction:
<c:set var="foo" value="bar" scope="request"/> followed by <s:textfield name="username" label="%{#request.foo}"/>
(which also shows how to give a Struts tag a value from EL). The Part I pages JSTL Core and
Expression Language Basics are the reference for the JSTL side.
The Same Tags in FreeMarker and Velocity
The JSP tag library is one of three bindings of the same components. In a FreeMarker template the tags are called as
user-defined directives with the s namespace, <@s.tag attributes>...</@s.tag>, and in a Velocity template as macros
with an s prefix (#sproperty(...), see the Velocity Tags page); the attributes and the evaluation are those of this
page. The page FreeMarker & Velocity Views has the FreeMarker and Velocity results, the
attributes map that replaced parameters in 7.x and how to call third-party JSP tag libraries from FreeMarker. A view
rendered through the freemarker result on the example WAR (the ftl action) printed the same values as the JSP tags:
<#-- FreeMarker view using the Struts tags -->
F1 [<@s.property value="filter"/>]
F2 [<@s.iterator value="titles"><@s.property/>,</@s.iterator>]
F3 [<@s.if test="count > 3">many<@s.else>few</@s.else></@s.if>]
F4 [<@s.url action="stats" var="u"><@s.param name="genre" value="'scifi'"/></@s.url>${u}]
F5 [<@s.text name="book.heading"/>] [${filter}] [${books[0].title}]
F7 [<#list books as b>${b.title};</#list>]
F9 [<@s.property value="markup"/>] [${markup}] [${markup?no_esc}]
F1 [all]
F2 [Dune,Neuromancer,Hyperion,Emma,]
F3 [many]
F4 [/g10tags/stats.action?genre=scifi]
F5 [Books on the shelf] [all] [Dune]
F7 [Dune;Neuromancer;Hyperion;Emma;]
F9 [<b>Dune</b> & "more"] [<b>Dune</b> & "more"] [<b>Dune</b> & "more"]
(FreeMarker auto-escapes ${markup}, so the three escape forms line up with the JSP ones: property and the FreeMarker
interpolation are escaped, ?no_esc is not. The value of ${filter} in F5 printed the action’s filter as all.) The
Velocity binding is the topic of the Velocity Tags page, Tag
Developer Guide — Velocity Tags.
Worked Example: The Bookshelf Catalogue Page
The pieces fit together in one page, catalogue.jsp, which shows the shelf: a heading and a count from the bundle, the
books in a striped table with their prices, a link per row to the statistics of its genre, the date of the last
update and a reload link that keeps the request parameters. Only generic tags are used, and every concept of the page was
introduced above.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head>
<title><s:text name="book.heading"/></title>
</head>
<body>
<h1><s:text name="book.heading"/></h1>
<p><s:text name="shelf.count"><s:param value="books.size"/></s:text>
(filter: <s:property value="filter" default="none"/>)</p>
<s:if test="books.empty">
<p>The shelf is empty.</p>
</s:if>
<s:else>
<table>
<s:iterator value="books" status="st">
<tr class="<s:if test="#st.odd">odd</s:if><s:else>even</s:else>">
<td><s:property value="#st.count"/><s:if test="#st.last"> (last)</s:if></td>
<td><s:property value="title"/></td>
<td><s:property value="author"/></td>
<td><s:number name="price" type="currency" currency="EUR"/></td>
<td><s:a action="stats" cssClass="more"><s:param name="genre" value="genre"/>more</s:a></td>
</tr>
</s:iterator>
</table>
</s:else>
<p>Updated <s:date name="published" format="yyyy-MM-dd" timezone="UTC"/>
- <s:url var="self" includeParams="get"/><s:a href="%{#self}">reload</s:a></p>
</body>
</html>
Requested with curl on /g10tags/catalogue?filter=scifi (with a cookie jar) the page printed (the first two
rows and the end are quoted):
<h1>Books on the shelf</h1>
<p>The shelf holds 4 books
(filter: scifi)</p>
<table>
<tr class="odd">
<td>1</td>
<td>Dune</td>
<td>Frank Herbert</td>
<td>€9.50</td>
<td><a href="/g10tags/stats.action?genre=scifi" class="more">more</a></td>
</tr>
<tr class="even">
<td>2</td>
<td>Neuromancer</td>
<td>William Gibson</td>
<td>€8.25</td>
<td><a href="/g10tags/stats.action?genre=cyberpunk" class="more">more</a></td>
</tr>
...
<tr class="even">
<td>4 (last)</td>
<td>Emma</td>
<td>Jane Austen</td>
<td>€6.00</td>
<td><a href="/g10tags/stats.action?genre=classic" class="more">more</a></td>
</tr>
</table>
<p>Updated 1965-09-13
- <a href="/g10tags/catalogue.action?filter=scifi">reload</a></p>
Reading the page against the page’s rules: the heading and the count use text (no escaping, from a bundle); the count is
books.size, the property form that the allowlist permits; books.empty guards the table; the iterator’s status gives
the stripes (odd, which follows the count) and the "(last)" mark; the param inside a is evaluated with the book
on the stack, so value="genre" is the genre of the current row; the number tag formats the price with a currency
code; the date has an explicit format and a time zone (the same text on every server); and url with includeParams="get"
carries the filter parameter on to the reload link. Nothing in the page is a scriptlet and no value is written without
escaping.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
A JSP EL |
Use |
A form field or label shows the literal text that was written, for example |
A String attribute ( |
Write |
|
A single-quoted literal of one character is a |
|
|
A string that is not |
Test a boolean property or a comparison |
|
The declaring JDK class is not allowlisted |
Use |
A property of the model prints nothing and the log says |
The model package is not in |
Allowlist the model package or class |
|
|
|
Every expression after an |
The class name of the bean cannot be loaded ( |
Fix the class name; look at the log |
|
An empty iterable in the sequence ends the append |
Drop empty lists, or use |
A |
With |
Read it with |
|
|
|
The URL printed through |
|
|
Extra parameters in every link, or |
|
Leave the default |
|
Dynamic method invocation is off in 7.x |
Map the method ( |
The page stops half way after an |
The inner action threw and the exception propagated |
Handle the error in the action, or leave |
|
Include parameters are request parameters |
|
|
The value is |
Check the property; test the pattern with |
|
The key is in no bundle of the lookup order |
Add the key, or give a body as the default |
|
Not in |
Turn |
|
A non-iterable value is wrapped into a one-element list |
Test the type in the action |
|
The |
Keep |
An action property with the name |
The names are the context map’s |
Rename it |
Source: Tag Developer Guide — Tag Syntax
What Changed Recently
| Version | Change |
|---|---|
2.1.x |
|
2.1.3 |
|
WebWork 2.2 |
The |
2.6 |
The old syntax and its switches removed (WW-3877); FreeMarker auto-escaping on, |
6.4.0 |
|
7.x |
FreeMarker templates read |
7.2.0 |
The |
7.3.0 |
The |
7.4.0 |
|
Where the Books Are Outdated
The books of this section teach the right model for the tags: the stack is the root, # reaches the context, %{} forces
evaluation, the control tags drive a loop, the data tags read and write values. What changed since they were written:
| In the books | Struts 7.4.0 |
|---|---|
The "OGNL everywhere" syntax with |
The notation |
|
|
Struts |
Not in the 7.4.0 TLD; plain |
|
Dynamic method invocation is off by default; map the method or use a wildcard |
|
Refused by the OGNL allowlist for JDK collection classes; use the property forms ( |
|
Use |
|
Still present; a JSP tag file or a FreeMarker macro is the lighter unit of reuse, and layouts belong to a layout engine (see the page on Tiles and page layouts) |
No mention of a nonce, a CSP or a WebJar |
|
Inconsistencies in the Official Documentation
Collected while verifying this page against the 7.4.0 jar, default.properties and the running examples; none of them
changes behaviour, but each can mislead:
-
The Tag Reference page lists 26 generic tags; the 7.4.0 TLD has 27:
number(classNumberTag, componentorg.apache.struts2.components.Number) has no reference page, andtag-developers/number-tag.htmlanswers 404. -
The Tag Syntax page says the property names
parameters,session,request,application,struts,servletRequestandservletResponseare disallowed and that<s:iterator value="parameters"/>"will not work"; an action withgetParameters()was read normally on Struts 7.4.0 (pagesyntax, D1). -
The Text tag page shows
<s:textfield name="lastName" label="getText('person.lastName')"/>as the way to localise a label; without the%{...}notation the attribute is the literal textgetText('person.lastName'). -
The Text tag and i18n tag pages read a pushed bundle with
<s:property value="text('aaa.bbb.ccc')"/>; on 7.4.0text('...')printed nothing and onlys:textreads the bundle ofs:i18n. -
The URL tag page says that the
includeParamsconstant defaults tononesince 2.1.3 (asdefault.propertiesdoes), and the FAQ "Why are request parameters appended to our hyperlinks" says the default isGET; the FAQ’s repair,<s:param name="request_locale" value="en" />, passesnullbecauseenis an expression, not a string. -
The Action tag page and its FAQ select a method with
name!method, which requires dynamic method invocation; the constant defaults tofalseandstats!featureddid nothing on 7.4.0. -
The Include tag page says no value stack is created for the included page, so
s:propertycannot read the parameters; the request’s value stack is visible in the included JSP (the parameters are still only request parameters). -
default.propertiescomments the global constant asescapeHtmlTag(“<s:a ... escapeHtmlTag="true">”); the attribute of the tag isescapeHtmlBody. -
The iterator page documents
statusas aBooleanattribute (it takes a name, and the docs' own examples writestatus="rowstatus"), itsvardescription as the name "used to reference the value pushed into the Value Stack (scope: action)", and its example of the<s:iterator var="counter" begin="1" end="5">comments as ifcounterheld the number (it prints the loop number only throughtop). -
The Push tag examples spell the tag
s:propery(missing letter) and close it with</push>; the Iterator tag example has the same unclosed</push>and the action tag example ends aspanthat was never opened. Theatag page’s usage example callserrorTextandpreInvokeJS, attributes that are not in the 7.4.0 TLD (they belonged to the removed Ajax tags) and uses${id}inside a Struts attribute. -
The
appendtag page’s example of<s:param value="%{myList1}"/>does not say that an empty list in the sequence ends the append. -
The Generator tag page’s "Example 4" is titled "generate an iterator with comparator attribute" and uses
converter. -
The Text tag page links to
java.sun.compages for Java 1.4.2 forMessageFormatandDecimalFormat.
Summary
-
The Struts tags are classic JSP tag handlers over shared components; the TLD is in
struts2-core(uri="/struts-tags", 53 tags: 27 generic, 26 UI) and no attribute accepts a JSP expression. -
Attributes obey three rules: a String attribute is literal text with
%{...}expressions embedded, a non-String attribute is itself an expression, and%{}around a non-String attribute is redundant. The pre-2.2 syntax with'literal'everywhere is gone since 2.6. -
A bare name is read from the value stack,
#namefrom the context map; OGNL’s one-character literal is achar, and the JDK collection methods are blocked, so usebooks.size,books.empty,books[0]. -
The control tags:
if(a real boolean test),iterator(var,statuswithindex,count,first,last,odd,even,begin/end/step),append,merge,generator,sortandsubsetfor iterators. -
The data tags:
property(escapes HTML by default),setand its scopes,push,beanandparam,action,textandi18n,date,number,include,urlanda(includingincludeParams,escapeAmpand the encoding of parameters) anddebug. -
script,link(CSP nonce),webjar(since 7.3.0) andcompress(since 7.2.0) are the asset tags. -
JSTL and EL work beside the tags:
${filter}reads the stack through the request wrapper; escape EL output withc:out; read a JSTL variable from OGNL with#attr.name.
References
-
Tag Developer Guide — Tag Syntax, The value attribute of form tags
-
Tag Developer Guide — Tag Syntax, Using an expression to set the label
-
Tag Developer Guide — Tag Syntax, Expression Language Notations
-
Tag Developer Guide — JSP Tags, Tag Library Definition (TLD)
-
Struts 7.4.0 — struts2-core-7.4.0.jar (META-INF/struts-tags.tld)
-
Struts Wiki — Why won’t the 'if' tag evaluate a one char string?
-
Struts Wiki — Why are request parameters appended to our hyperlinks?
-
Struts Wiki — Why didn’t my action tag get executed when I have validation errors?