JSTL Core
|
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 Jakarta Standard Tag Library 3.0 (API jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api
3.0.2, implementation org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1, the latest release of both), which is part of
Jakarta EE 10 and 11, together with Jakarta Pages 4.0, Jakarta Expression Language 6.0 and Jakarta Servlet 6.1, running on
Apache Tomcat 11.0.26 and Java 21. Everything was verified on 2026-10-08: the rules against chapters 1 to 5 of the Tags 3.0
specification and the tag documentation (tagdocs) of the core library, and every example by building it into a Maven WAR
(context path /g5jstlcore) and running it on Tomcat 11.0.26 with curl; the outputs and error messages shown are the
ones that run produced. The Standard Tag Library is the set of tags that page authors use instead of scriptlets: it prints
and escapes values, creates and removes scoped variables, makes decisions and catches errors, and (on the pages of this
section that follow this one) loops, builds URLs, imports resources, localises and formats text, calls string functions
and, in two legacy libraries, queries databases and processes XML. This page covers the whole library at the level of
concepts, the five URIs and the conventions every tag obeys, and then the part of the core library that is neither
iteration nor URLs: c:out, c:set, c:remove, c:catch, c:if and c:choose. The running scenario is the Bookshelf,
a small catalogue of books; the page ends with a book-detail page that uses every tag described here, then the pitfalls,
what changed from JSTL 1.0 to 3.0 and where the books in the section bibliography are out of date. Writing ${...}
itself, the implicit objects and the operators are on the
expression language basics page and the
operators and type coercion page of this section; the
iteration, URL, internationalisation, formatting, function, SQL and XML tags have a page each.
What the Standard Tag Library Is
A JSP page that wants to show a list, hide a block for guests or format a price needs logic, and the first generation of JSP put that logic in Java scriptlets. The Standard Tag Library (JSTL, "Jakarta Tags" since Jakarta EE 9) is the answer the platform standardised: a few dozen tags and functions with fixed names, attributes and behaviour, available in every container, so that the template stays markup and the logic is expressed with the expression language. The specification states the goal as helping page authors who are not fluent in a programming language, and lists what it offers: general-purpose actions that complement the expression language (display a value, set and remove scoped attributes, catch exceptions), control-flow actions (conditionals and iterators), tag library validators that can forbid scripting, and standard actions and EL functions for URLs, text formatting, internationalisation, SQL, XML and string handling.
It is a specification with several implementations of the same tag handlers, not a Tomcat feature. The API artifact holds
the few Java types an application may touch (Config, ConditionalTagSupport, LoopTagSupport, LoopTag,
LoopTagStatus, Result, LocalizationContext and the validators); the implementation artifact holds the tag handlers
and the TLD files that map a URI to them. Neither is in the JSP container: Tomcat 11.0.26 has no JSTL jar in its lib
directory, so the web application packages both, which is the setup the getting
started page of this section builds. Eclipse GlassFish and the other full Jakarta EE servers ship their own.
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.1</version>
</dependency>
A page that uses the tags without the jars fails when it is translated, not when it runs. Removing both jars from the WAR of this page gives, for the first request:
org.apache.jasper.JasperException: Unable to get JAR resource [/jakarta.tags.core] containing TLD: [java.io.FileNotFoundException]
The specification text still says that the library requires a "Jakarta Server Pages 3.0 web container" and "Jakarta Expression Language 4.0": those sentences were not updated, and the current implementation runs on Pages 4.0 and EL 6.0.
The Five Libraries and Their URIs
The specification calls it the standard tag library, but it is exposed as five tag libraries, one per functional area,
so that each area has its own namespace. A page opens a library with the taglib directive; the uri identifies the library
and the prefix is the page author’s choice (the prefixes in the table are the specification’s, and every example of
the section uses them).
| Functional area | URI | Prefix | TLD in the jar | Content | Covered in the section |
|---|---|---|---|---|---|
core |
|
|
|
14 tags |
general-purpose and conditional tags on this page; iteration, URL, import and redirect tags on their own pages (iteration, URLs) |
I18N-capable formatting |
|
|
|
12 tags |
the internationalisation and the formatting pages |
functions |
|
|
|
16 EL functions, no tags |
the functions page |
relational database access |
|
|
|
6 tags |
|
XML processing |
|
|
|
10 tags |
Two more URIs live in the same jar, jakarta.tags.scriptfree and jakarta.tags.permittedTaglibs. They are not tag
libraries in the usual sense but carriers of two tag library validators (ScriptFreeTLV and PermittedTaglibsTLV,
specification chapter 14) that make a page fail translation when it contains scripting elements or imports a library that is not on a
list; the Tag Library Descriptors & EL Functions page shows how to configure them.
The diagram shows the two groups of core tags that this page documents (shaded); the status of the five libraries differs.
The Jakarta EE Tutorial says that "a subset of Jakarta Tags has been archived in favor of Jakarta XML Binding, Jakarta Faces
and Jakarta Persistence", and that the core tag library and the functions "are still available in Facelets" (the Facelets
view technology of Jakarta Faces accepts c: and fn: tags in its templates). In practice core, fmt and functions
are the libraries a JSP page uses today, while sql and xml are kept for compatibility and prototyping.
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<%@ taglib prefix="sql" uri="jakarta.tags.sql" %>
<%@ taglib prefix="x" uri="jakarta.tags.xml" %>
<fmt:setLocale value="en_US"/>
core: <c:out value="${dune.title}"/>
fmt: <fmt:formatNumber value="1234.5" pattern="#,##0.00"/>
fn: ${fn:length(books)} books, ${fn:toUpperCase(dune.title)}
curl for /g5jstlcore/libs.jsp:
core: Dune
fmt: 1,234.50
fn: 3 books, DUNE
The fmt:setLocale line matters for the second output: without a formatting locale (no Accept-Language header, no
fmt:setLocale) the same tag printed the bare 1234.5 and ignored the pattern on Tomcat 11.0.26, which the tag documentation does
not say. The formatting page of this section explains how the locale is chosen.
The legacy URIs
Before Jakarta EE 9 the URIs were different and many pages still use them. The 3.0.1 jar ships all of these TLDs, which is why
old pages keep compiling, but the specification defines only the jakarta.tags.* URIs.
| Generation | Core URI | Behaviour on Tomcat 11.0.26 |
|---|---|---|
JSTL 1.0 (JSP 1.2, 2002), "EL" library |
|
translates, but |
JSTL 1.0, |
|
works with a Java expression: |
JSTL 1.1 and 1.2 (JSP 2.0 to 2.3, Java EE 5 to 8) |
|
works exactly like |
Jakarta Tags 2.0 and 3.0 (Jakarta EE 9 to 11) |
|
the current URI; the same pattern gives |
The first two rows are the books' world: in JSTL 1.0 on JSP 1.2 the container had no expression language, so every library came
twice, one version whose attributes were evaluated by the JSTL tag itself (the "EL" library) and an _rt twin whose
attributes took Java expressions. JSP 2.0 moved the EL into the container, JSTL 1.1 dropped the twins and added the fn:
functions, and the attributes became ordinary request-time attributes. On a current container the 1.0 "EL" library is a trap: it is
found, but it rejects ${...}, and nothing in it is worth keeping.
The Bookshelf Used on This Page
The examples need data in scope and a few beans to modify. A ServletContextListener puts the catalogue into application
scope, and a filter mapped to *.jsp gives every request a fresh ReaderProfile, an empty map and a StringReader.
The pages themselves are plain JSPs in the root of the WAR so that curl can reach them; in the real application of the
section they live under WEB-INF/views behind a servlet.
public record Book(long id, String title, String author, int year) { }
public class ReaderProfile {
private String name = "guest";
private String favouriteGenre = "sf";
private int pageSize = 10;
private boolean admin;
private String note;
// getters, setters (none for the read-only property "readOnly")
public String getReadOnly() { return "fixed"; }
}
@WebListener
public class ShelfListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
var ctx = event.getServletContext();
var dune = new Book(1, "Dune", "Frank Herbert", 1965);
var neuro = new Book(2, "Neuromancer", "William Gibson", 1984);
var disp = new Book(3, "The Dispossessed", "Ursula K. Le Guin", 1974);
ctx.setAttribute("books", List.of(dune, neuro, disp));
ctx.setAttribute("dune", dune);
ctx.setAttribute("catalog", Map.of("dune", dune));
ctx.setAttribute("tricky", new Book(4, "<script>alert('x')</script> & \"Co\"", "A. Author", 2001));
ctx.setAttribute("stock", Map.of("1", 3, "2", 0, "3", 1));
ctx.setAttribute("inventory", Map.of(1L, 3, 2L, 0, 3L, 1));
}
}
@WebFilter("*.jsp")
public class ProfileFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
req.setAttribute("profile", new ReaderProfile());
req.setAttribute("prefs", new HashMap<String, Object>());
req.setAttribute("excerpt", new StringReader("Fear is the mind-killer & <i>nothing</i> else."));
chain.doFilter(req, res);
}
}
Every plain-text page starts with <%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>, which
leaves out most blank lines but has a side effect worth knowing: trimDirectiveWhitespaces also removes template text that is
only white space, so a single space between two ${...} expressions or two tags disappears (${a} ${b} prints ab).
The examples therefore separate values with a visible character such as a bar. A name such as nothing that is not defined
anywhere evaluates to null.
How a Tag Is Translated, Validated and Run
A tag is not interpreted at request time by looking up its name. The page is translated once, and the translation is where most of the work and most of the errors are:
What each step means in practice, in the order Tomcat applies it:
-
The
taglibdirective binds a prefix to a URI. A URI that no TLD declares is a translation error (the message above). The container finds the TLD inMETA-INF/*.tldof any jar underWEB-INF/lib; that is the implicit mapping, and noweb.xmlentry is needed since JSP 2.0. -
The TLD (for example
c.tld,tlib-version3.0,short-namec) lists every tag with its handler class (org.apache.taglibs.standard.tag.rt.core.OutTagforc:out), its body content type and its attributes withrequiredandrtexprvalueflags. The specification’s "Dynamic" column and the tag documentation’s "Request-time" column are thertexprvalueflag. -
Attribute checks happen at translation time: an unknown attribute, a missing required attribute, or an expression in an attribute that does not accept one is an error before anything runs.
-
The validator (
org.apache.taglibs.standard.tlv.JstlCoreTLVfor the core library) looks at the page as a whole, which is how rules such as the one that requiresc:otherwiseto be the last child ofc:choosecan be reported at translation time. -
The handler runs for every request. It evaluates the EL in its attributes, reads and writes scoped attributes through the
PageContextand writes to the currentJspWriter.
The tag library specification summarises the error policy in one sentence: syntax errors and constraints on static attributes
are reported at translation time, constraints on dynamic attributes at request time. The tags also try to avoid exceptions
where a page author could not reasonably handle them: a null where a collection is expected means "do nothing", and a
null where a fixed set of values is expected means "use the default". When an exception is raised it is a
JspException or a subclass (usually JspTagException), with the original error as its root cause if the tag caught one
in its body.
| The page contains | Message | Why |
|---|---|---|
|
|
|
|
|
only |
|
|
a constraint of |
|
|
required attribute |
|
|
unknown attribute |
|
|
required attribute |
|
|
required attribute |
|
|
at least one |
|
|
|
|
|
at most one |
|
|
only whitespace between the children |
Two rows of that table are caught only by the validator, not by the attribute check, and one case is not caught at all at
translation time: a c:when or c:otherwise outside a c:choose. That page translates and the first request fails with
JspTagException: Illegal use of <when>-style tag without <choose> as its direct parent.
Expressions in Attributes
Almost every attribute of a JSTL tag accepts an expression, written with the syntax of the expression language page. Three things from the specification’s chapter 3 decide how the value reaches the tag.
-
Mixed text and expressions. An attribute may contain several
${...}expressions mixed with literal text; the result is aString. -
Coercion to the attribute type. The tag declares a type (
booleanfortest,intforbegin,Objectforvalue) and the EL converts to it with the rules of the type-coercion section of the EL specification.nullbecomes0for a number andfalsefor a boolean. -
Defaults instead of errors. The EL turns many failures into type-correct defaults (a missing property is
null, anulloperand of+is 0) so that a page degrades instead of throwing.
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
1 mixed: <c:out value="Price of ${dune.title} is ${12.5}"/>
<c:set var="n" value="42"/>
<c:set var="m" value="${42}"/>
<c:set var="mix" value="${1}${2}"/>
<c:set var="two" value="${1 + 1}"/>
2 types: n=${n.getClass().getSimpleName()} m=${m.getClass().getSimpleName()} mix=${mix.getClass().getSimpleName()}:${mix} two=${two.getClass().getSimpleName()}
3 compare: ${n == 42} | ${n eq '42'} | ${n + 1}
4 default: <c:out value="${profile.note}" default="no note"/> | <c:out value="${dune.title}" default="no title"/>
5 deep: <c:out value="${nothing.address.city}" default="N/A"/> | <c:out value="${catalog.dune.title}" default="N/A"/> | <c:out value="${catalog.nope.title}" default="N/A"/>
6 checkbox: [<c:forEach var="t" items="${paramValues.tag}">${t},</c:forEach>] none=${empty paramValues.tag}
curl for /g5jstlcore/attrs.jsp, and for /g5jstlcore/attrs.jsp?tag=a&tag=b (only line 6 changes):
1 mixed: Price of Dune is 12.5
2 types: n=String m=Long mix=String:12 two=Long
3 compare: true | true | 43
4 default: no note | Dune
5 deep: N/A | Dune | N/A
6 checkbox: [] none=true
6 checkbox: [a,b,] none=false
Line 2 is the one that surprises: <c:set var="n" value="42"/> stores the String "42", not a number, while
value="${42}" stores a Long, and value="${1}${2}" is two expressions in one attribute, so it is text ("12"), not a
sum. Line 3 shows why that rarely hurts: the EL converts the String where a number is needed, so ${n + 1} is 43 and
${n == 42} is true. Line 6 is the specification’s own reason for the "null means do nothing" rule: a group of check
boxes that nobody ticked sends no parameter at all, ${paramValues.tag} is null, and the loop over it simply
prints nothing instead of throwing.
Scoped Variables: var, scope and Visibility
Every JSTL tag that produces a value exposes it the same way: as a scoped variable, a JSP scoped attribute (an attribute
of the PageContext, the request, the session or the application, see the
implicit objects and scopes page). No tag creates a Java scripting
variable, which is what makes the library usable on pages with scripting disabled. The conventions are fixed by chapter 2
of the specification and are the same in every library.
| Convention | Rule |
|---|---|
|
the attribute that names the main exported variable ( |
|
|
|
secondary variables of a tag are |
static |
both attributes take only literal values (no |
nested variables |
visible only inside the tag body, stored in page scope and removed at the end of the tag; a nested variable has no |
at-end variables |
visible after the tag, with the lifetime of their scope; this is the default ( |
configuration variables |
scoped variables named by the specification that tune the behaviour of a library (locale, bundle, time zone, data source); next section |
A name in the EL is resolved by PageContext.findAttribute, which looks in page, request, session and application scope, in
that order, so scoped variables of different scopes behave as one namespace in which the innermost wins. The Pages and
Tags specifications both ask page authors not to rely on the same name in different scopes, and Tomcat follows the lookup
order. The loop below shows nested versus at-end variables on the Bookshelf; the shadow lines of the c:set section show
the namespace rule.
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:forEach var="b" items="${books}">
<c:set var="inside" value="in-${b.id}"/>
loop ${b.id} ${inside}
</c:forEach>
after: b=[${b}] inside=[${inside}]
<c:forEach var="b2" items="${books}" varStatus="st">${st.count}</c:forEach>
after status: [${st}]
loop 1in-1
loop 2in-2
loop 3in-3
after: b=[] inside=[in-3]
123
after status: []
(The missing space in loop 1in-1 is the trimDirectiveWhitespaces effect described above.)
Configuration variables and the Config class
Some tags read their settings from outside the tag: the formatting locale, the resource bundle, the time zone, the default
SQL data source. The specification calls the setting a configuration setting and offers two ways to set it: a context
initialisation parameter in web.xml (the string value is used for the whole application), or a configuration variable, a
scoped variable that overrides it for a page, a request, a session or the application. Because the Pages specification
says all scopes should behave as one namespace, a configuration variable is stored under a name that
the class jakarta.servlet.jsp.jstl.core.Config manipulates, so that setting one scope does not hide the others; page
authors therefore never set configuration variables with c:set but with the tags that own them (fmt:setLocale,
fmt:setBundle, fmt:setTimeZone, sql:setDataSource) or from Java with Config.
String lang = req.getParameter("lang");
if (lang != null) {
Config.set(req, Config.FMT_LOCALE, Locale.forLanguageTag(lang));
}
<%@ page contentType="text/plain;charset=UTF-8" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<fmt:formatNumber value="1234.5" type="number"/> | <fmt:formatNumber value="1234.5" type="currency" currencyCode="EUR"/>
curl for /g5jstlcore/config.jsp?lang=de and /g5jstlcore/config.jsp?lang=en-US:
1.234,5 | 1.234,50 €
1,234.5 | €1,234.50
| Constant | Name, also the context parameter | Used by |
|---|---|---|
|
|
formatting and i18n tags |
|
|
i18n tags |
|
|
|
|
|
date formatting and parsing |
|
|
SQL tags |
|
|
|
A value of the wrong type is converted with the EL rules and a failed conversion is a JspException at request time, exactly
as for an attribute. The same names are valid as context-param entries; the internationalisation page of this section
shows them in web.xml.
Source: Jakarta Tags 3.0 API — Config
c:out: Printing and Escaping
c:out evaluates an expression and writes the result to the current JspWriter. The tag documentation describes it as
"like <%= ... %>, but for expressions", and the specification’s overview explains what it adds to a bare ${...}: it
escapes the five XML special characters by default and supports a default value for null.
| Attribute | Dynamic | Required | Meaning |
|---|---|---|---|
|
yes |
yes |
the expression to print. A |
|
yes |
no |
|
|
yes |
no |
printed when |
| Character | Entity written |
|---|---|
|
|
|
|
|
|
|
|
|
|
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
1 default: <c:out value="${tricky.title}"/>
2 raw: <c:out value="${tricky.title}" escapeXml="false"/>
3 EL only: ${tricky.title}
4 null: [<c:out value="${nothing}"/>]
5 default: [<c:out value="${nothing}" default="unknown"/>]
6 body: [<c:out value="${nothing}">no value for ${dune.id}</c:out>]
7 empty: [<c:out value="" default="unknown"/>]
8 param: [<c:out value="${param.q}"/>] raw EL: [${param.q}]
9 mixed: <c:out value="Title ${dune.title} by ${dune.author}"/>
10 record: <c:out value="${dune}"/>
11 reader: <c:out value="${excerpt}"/>
12 again: [<c:out value="${excerpt}"/>]
13 num: <c:out value="${dune.year + 1}"/>
14 flag: <c:out value="${param.f}" escapeXml="${param.esc}"/>
1 default: <script>alert('x')</script> & "Co"
2 raw: <script>alert('x')</script> & "Co"
3 EL only: <script>alert('x')</script> & "Co"
4 null: []
5 default: [unknown]
6 body: [no value for 1]
7 empty: []
8 param: [<b>x</b>] raw EL: [<b>x</b>]
9 mixed: Title Dune by Frank Herbert
10 record: Book[id=1, title=Dune, author=Frank Herbert, year=1965]
11 reader: Fear is the mind-killer & <i>nothing</i> else.
12 again: []
13 num: 1966
14 flag: <i>
Source: Jakarta Tags 3.0 tagdocs — c:out
Reading the output line by line:
-
Lines 1 to 3 are the reason the tag exists. A plain
${tricky.title}is not escaped: the EL writes the value as it is, so a title containing markup becomes markup.c:outturns the same value into text. -
Line 4: a
nullvalue prints nothing; there is no "null" string. Line 5 and 6:defaultas an attribute or as the body, which is evaluated only whenvalueisnull(the body of line 6 was read, the tag hasJSPbody content). -
Line 7: the default is used only for
null, never for an empty string; use${empty x ? 'unknown' : x}when "empty" should count as "missing". -
Line 8: the same parameter, escaped and not. The bare expression is the cross-site scripting hole.
-
Line 10: any object is converted with
toString; therecordprints its generated representation. -
Lines 11 and 12: a
Readeris consumed. The first tag copies the text (escaped), the second finds the reader at its end and prints nothing, so aReadercan be shown once. -
Line 14: the value is printed unescaped because the request carried
esc=false:escapeXmlis itself an expression.
escapeXml is not a boolean check
The tag documentation says the default of escapeXml is true and only false switches it off. The implementation is
looser: the attribute is converted to a boolean with the rules of the Pages specification, and the conversion of any text
other than "true" is false, so the escaping is lost in cases a reader would not expect.
<%@ page contentType="text/plain;charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
typo "flase": <c:out value="<b>x</b>" escapeXml="flase"/>
upper "FALSE": <c:out value="<b>x</b>" escapeXml="FALSE"/>
empty "": <c:out value="<b>x</b>" escapeXml=""/>
null EL: <c:out value="<b>x</b>" escapeXml="${nothing}"/>
true: <c:out value="<b>x</b>" escapeXml="true"/>
typo "flase": <b>x</b>
upper "FALSE": <b>x</b>
empty "": <b>x</b>
null EL: <b>x</b>
true: <b>x</b>
The fourth line contradicts the specification’s own rule that a null for an attribute with a fixed set of values "uses the
default value" (here true): the EL has already coerced the null to the boolean false before the tag sees it. The
practical rule is to leave escapeXml out unless you mean false, never to compute it from request data, and never to
spell it with a typo.
c:out, fn:escapeXml and the output context
fn:escapeXml(string) of the functions library does the same replacements as c:out; it is the form to use inside a bigger
expression or an attribute value that is built with ${...}. Both produce identical text on the Bookshelf:
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
c:out: <c:out value="${tricky.title}"/>
fn:escapeXml: ${fn:escapeXml(tricky.title)}
attr: <input value="<c:out value="${tricky.title}"/>">
c:out: <script>alert('x')</script> & "Co"
fn:escapeXml: <script>alert('x')</script> & "Co"
attr: <input value="<script>alert('x')</script> & "Co"">
The escaping is HTML-body and quoted-attribute escaping. It is correct for text between tags and for an attribute
value in single or double quotes (the quote characters are encoded). It is not correct inside a <script> element or an
event handler, in a URL, in a CSS value or in an unquoted attribute, where the browser parses the text with different
rules: those need the encoder for that context, such as the OWASP Java Encoder’s forJavaScript and forUriComponent,
or the c:url and c:param tags for URLs (see the Security and URL pages). The OWASP cheat sheet lists the
contexts and the encoding each needs.
c:set: Creating and Changing Values
c:set assigns a value, and has five forms: it can set a scoped variable from an attribute or from the tag body, set a
property of a bean or an entry of a map (target and property), or map a deferred expression.
| Syntax | Form | Effect |
|---|---|---|
1 |
|
sets the variable to the value of the expression |
2 |
|
sets the variable to the body text (trimmed); the body may contain other tags, so a tag’s output becomes a value |
3 |
|
sets property |
4 |
|
as 3 with the body as value |
5 |
|
maps a deferred expression into the EL variable mapper; no |
| Attribute | Dynamic | Type | Meaning |
|---|---|---|---|
|
yes |
Object |
the expression; the tag documentation lists it as a |
|
no |
String |
name of the scoped variable |
|
no |
String |
scope of |
|
yes |
Object |
the bean or map whose property is set; must evaluate to a JavaBean with a setter or to a |
|
yes |
String |
name of the property or of the map key |
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="greeting" value="Welcome, ${profile.name}"/>
1 value: ${greeting}
<c:set var="label"> Book: ${dune.title} </c:set>
2 body: [${label}]
<c:set var="n" value="42" scope="request"/>
3 request: ${requestScope.n} | page:[${pageScope.n}] | ${n + 1}
<c:set var="n" value="page copy"/>
4 shadow: ${n} / ${requestScope.n}
<c:set var="gone" value="x" scope="session"/>
<c:set var="gone" value="${nothing}" scope="session"/>
5 null removes: [${empty sessionScope.gone}]
<c:set target="${profile}" property="name" value="${dune.author}"/>
<c:set target="${profile}" property="pageSize" value="25"/>
<c:set target="${profile}" property="admin" value="true"/>
6 bean: ${profile.name} | ${profile.pageSize + 1} | ${profile.admin}
<c:set target="${prefs}" property="theme" value="dark"/>
<c:set target="${prefs}" property="theme2">light</c:set>
7 map: ${prefs.theme} | ${prefs.theme2} | size=${prefs.size()}
<c:set target="${prefs}" property="theme" value="${nothing}"/>
8 map null: size=${prefs.size()} | ${empty prefs.theme}
<c:set var="title" value="#{dune.title}"/>
9 deferred: ${title}
1 value: Welcome, guest
2 body: [Book: Dune]
3 request: 42 | page:[] | 43
4 shadow: page copy / 42
5 null removes: [true]
6 bean: Frank Herbert | 26 | true
7 map: dark | light | size=2
8 map null: size=1 | true
9 deferred: Dune
Source: Jakarta Tags 3.0 tagdocs — c:set
What the output shows:
-
Line 2: the body is trimmed (the spaces inside the tag are gone) and it is processed first, so EL and other tags in the body run. This is the way to capture the output of another tag into a variable; the Tags specification’s example sets an attribute of a custom tag from nested markup.
-
Lines 3 and 4: the same name in two scopes. The page-scope
ncreated on line 4 hides the request-scopenfrom the plain EL name, whilerequestScope.nstill reaches the request one. Avoid the situation; if a tag must write a value that a later${n}reads, give it a distinct name. -
Line 5: assigning
nullremoves the variable. The same happens fortarget: line 8 removes the map entry and a bean property is set tonull. -
Lines 6 and 7: a bean property is converted to the property’s type (the text "25" becomes the
int25, so 25 + 1 is 26), and a map entry is created if it does not exist. -
Line 9: syntax 5 keeps the expression, not its value; the variable
titlebehaves as an alias for#{dune.title}in the later EL of the page. It is the one case where the variable lives in the EL variable mapper rather than in a scope, and it exists so that tags of a deferred-expression library (Jakarta Facesh:inputText, for example) can share one expression.
When c:set fails
The tag is strict about the target of a property. The first form of a missing target is a JspTagException without a
message, so wrap uncertain sets in c:catch (below) or check the bean first.
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:catch var="e1"><c:set target="${nothing}" property="p" value="1"/></c:catch>
1 null target: ${e1.getClass().getSimpleName()} | [${e1.message}]
<c:catch var="e2"><c:set target="${profile}" property="readOnly" value="1"/></c:catch>
2 no setter: ${e2.getClass().getSimpleName()} | ${e2.message}
<c:catch var="e3"><c:set target="${profile}" property="pageSize" value="many"/></c:catch>
3 bad value: ${e3.getClass().getSimpleName()} | ${e3.message}
<c:catch var="e4"><c:set target="${profile}" property="nonsense" value="1"/></c:catch>
4 unknown: ${e4.getClass().getSimpleName()} | ${e4.message}
1 null target: JspTagException | []
2 no setter: JspException | No setter method in <set> for property "readOnly"
3 bad value: JspTagException | jakarta.el.ELException: Cannot convert [many] of type [class java.lang.String] to [int]
4 unknown: JspTagException | Invalid property in <set>: "nonsense"
Note that the messages carry the entities <set> literally (the text is not decoded) and name the tag as <set>,
not c:set. Two more combinations are worth knowing. A tag with neither var nor target raises a
JspTagException when it runs, as does target without property. And a tag that has both var and target
silently takes the var path:
<c:set var="a" target="${profile}" property="name" value="x"/>
a=[${a}] name=[${profile.name}]
a=[x] name=[guest]
A property name taken from the request is mass assignment
property is a dynamic attribute, so it can be an expression, and that is a vulnerability when the expression reads
request data. This page is a shortcut for "let the form decide what to set":
<c:set target="${profile}" property="${param.prop}" value="${param.v}"/>
name=${profile.name} admin=${profile.admin} pageSize=${profile.pageSize}
curl "http://localhost:18080/g5jstlcore/mass.jsp?prop=admin&v=true":
name=guest admin=true pageSize=10
Any visitor can switch on the admin flag of the bean, or set any other writable property, because the tag will call any
setter whose name it is given. This is the JSP form of the "bind every request parameter to the model" problem that Struts
had to fix with @StrutsParameter in its own section. The rule: property is always a literal written by the developer,
and the value is validated and converted by a controller before it reaches the bean.
c:remove: Removing a Scoped Variable
c:remove is the companion of c:set. Its two attributes (var required, scope optional, both static) name the
variable. With a scope only that scope is touched (PageContext.removeAttribute(name, scope)); without it the variable is
removed from every scope (removeAttribute(name)). Removing a variable that does not exist is not an error.
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="cached" value="a" scope="request"/>
<c:set var="cached" value="b" scope="session"/>
<c:set var="cached" value="c"/>
1 before: page=${pageScope.cached} request=${requestScope.cached} session=${sessionScope.cached}
<c:remove var="cached" scope="session"/>
2 session removed: [${sessionScope.cached}] request=${requestScope.cached}
<c:remove var="cached"/>
3 no scope: page=[${pageScope.cached}] request=[${requestScope.cached}] session=[${sessionScope.cached}]
<c:remove var="neverExisted"/>
4 missing var: no error
1 before: page=c request=a session=b
2 session removed: [] request=a
3 no scope: page=[] request=[] session=[]
4 missing var: no error
Line 3 is the surprise of the tag: the one-attribute form clears the name everywhere, including a session attribute that
another part of the application may own. Always give the scope of the variable you created. The typical use is
a message that must be shown once (see the worked example), and clearing a cached value held in session or application
scope.
c:catch: Recovering from Errors
c:catch evaluates its body and catches any Throwable thrown by it. The exception is stored in the variable named by
var (always in page scope); without var it is simply swallowed. If the body completes normally the variable is
removed if it existed, so a stale error from an earlier catch does not survive. The specification presents it as a
complement of the JSP error page: actions that are central to the page should not be wrapped, so their exceptions
reach the error page, and actions of secondary importance should be, so that they never trigger it.
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:catch var="err">
before
<c:set var="next" value="${param.page + 1}"/>
after: next is ${next}
</c:catch>
1 err: [${empty err ? 'none' : err.getClass().getName()}] | message: [${err.message}]
<c:catch var="err"><c:set var="ok" value="1"/></c:catch>
2 second catch without an exception: err is [${empty err ? 'removed' : 'kept'}]
<c:catch>text before | <c:set target="${nothing}" property="p" value="1"/></c:catch>
3 no var: the exception was swallowed
<c:catch var="el">${param.page + 1}</c:catch>
4 template EL in the body: [${empty el ? 'none' : el.getClass().getSimpleName()}]
<c:catch var="late"><c:set target="${nothing}" property="p" value="1"/></c:catch>
5 scope: pageScope [${empty pageScope.late ? 'empty' : 'set'}] | requestScope [${empty requestScope.late ? 'empty' : 'set'}]
curl for /g5jstlcore/catch.jsp?page=abc:
before
1 err: [java.lang.NumberFormatException] | message: [For input string: "abc"]
2 second catch without an exception: err is [removed]
text before |
3 no var: the exception was swallowed
4 template EL in the body: [NumberFormatException]
5 scope: pageScope [set] | requestScope [empty]
and for /g5jstlcore/catch.jsp?page=4 (blank lines removed):
before
after: next is 5
1 err: [none] | message: []
2 second catch without an exception: err is [removed]
text before |
3 no var: the exception was swallowed
5
4 template EL in the body: [none]
5 scope: pageScope [set] | requestScope [empty]
Observations that are not in the specification’s text but follow from how the tag is built:
-
The body is not rolled back. Whatever the body wrote before the exception (
before,text before |) is already in the response, so a failed tag can leave half a sentence in the page. Put the risky tag first, or capture its output withc:setand print it afterwards. -
A
NumberFormatExceptionraised by an expression in a tag attribute (line 1) and one raised by an expression in plain template text inside the body (line 4) are both caught, because the whole body runs inside the handler’stry. -
The variable is in page scope (line 5), so it is visible in the rest of the page but not in an included page or in a later request; test it with
${not empty err}. -
Only failures that happen while the page runs are caught. A translation-time error (the table above) fails the page before
c:catchcould exist, and ac:catchhides the errors a developer wants to see, so log what you swallow (err.messagein a comment or a log call) and keep the body small. -
c:catchis for expected failures such as "this parameter was not a number". Unexpected failures should go to an error page, see the error handling page of this section.
c:if: One Condition
c:if evaluates its body only when test is true. There is no else: for two branches use c:choose. It also exposes
the result of the test, which is how a condition is computed once and used several times.
| Syntax | Form | Notes |
|---|---|---|
1 without body |
|
only stores the Boolean result; |
2 with body |
|
body processed when the test is true; the result is stored if |
test is a dynamic boolean. The constraint "if scope is given, var must be too" is checked at translation time.
<%@ page contentType="text/plain;charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:if test="${dune.year lt 1970}">1 Dune is older than 1970</c:if>
<c:if test="${param.admin}">2 admin param is true</c:if>
<c:if test="${not empty param.q}" var="hasQuery" scope="request"/>
3 hasQuery=${hasQuery} request=${requestScope.hasQuery}
<c:if test="${profile.pageSize gt 5}" var="big">4 big=${big}</c:if>
<c:if test="${nothing}">5 never</c:if>
<c:if test="true">6 literal</c:if>
<c:if test="${'TRUE'}">7 upper TRUE</c:if>
<c:if test="${'yes'}">8 yes</c:if>
<c:if test="${empty books or books[0].year gt 1990}">9 first book newer</c:if>
<c:if test="${stock[param.id] gt 0}" var="inStock">10 in stock: ${stock[param.id]}</c:if>
11 inStock=${inStock}
curl for /g5jstlcore/if.jsp?admin=true&q=a&id=1 (blank lines removed):
1 Dune is older than 1970
2 admin param is true
3 hasQuery=true request=true
4 big=true
6 literal
7 upper TRUE
10 in stock: 3
11 inStock=true
and for /g5jstlcore/if.jsp?id=2&admin=1:
1 Dune is older than 1970
3 hasQuery=false request=false
4 big=true
6 literal
7 upper TRUE
11 inStock=false
Source: Jakarta Tags 3.0 tagdocs — c:if
How a test becomes a boolean is the EL’s coercion to boolean: a Boolean is itself, null is false, and a String is
Boolean.valueOf of its text, true only for "true" in any case. That explains the output: ${param.admin} is true for
?admin=true but false for ?admin=1 (line 2 vanished in the second request), the String "TRUE" is true and "yes" is
false, and a missing parameter or an undefined name is false. Prefer an explicit comparison (${param.admin eq 'true'}, or
better a boolean the controller computed) to relying on the conversion of a request string. Line 10 shows var on a
tag with a body: the Boolean is stored whether or not the body ran, and line 11 reuses it (for the second request,
stock['2'] is 0, so the test is false and inStock is false).
The var form is the right tool when a test is needed twice: compute it once, then <c:if test="${canLoan}"> wherever
it is needed, as the worked example does.
c:choose, c:when and c:otherwise: Mutually Exclusive Branches
c:choose is the if / else if / else and switch of the library. It has no attributes of its own; its body holds one
or more c:when (each with a test) and at most one c:otherwise, in that order, with only white space between them.
The first c:when whose test is true is processed, all others are skipped, and c:otherwise runs only when no c:when
did.
The semantic difference from several c:if tags is the point of the tag: a c:if always processes its body when its test is
true, so two true c:if tags both print, whereas two true c:when tags print only the first one’s body. c:when and c:otherwise are only valid as direct children of c:choose, which is also why a
custom conditional tag of an application should expose its result as a variable that a c:when can test
(ConditionalTagSupport is the base class the API offers for that).
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:choose>
<c:when test="${stock[param.id] gt 1}">available (${stock[param.id]} copies)</c:when>
<c:when test="${stock[param.id] eq 1}">last copy</c:when>
<c:when test="${stock[param.id] eq 0}">loaned out</c:when>
<c:otherwise>unknown book ${empty param.id ? '(no id)' : param.id}</c:otherwise>
</c:choose>
fallthrough check:
<c:choose>
<c:when test="${true}">first</c:when>
<c:when test="${true}">second</c:when>
</c:choose>
no otherwise, no match:[<c:choose><c:when test="${false}">x</c:when></c:choose>]
curl for /g5jstlcore/choose.jsp?id=1 prints:
available (3 copies)
fallthrough check:
first
no otherwise, no match:[]
and the first line is last copy for ?id=3, loaned out for ?id=2, unknown book 9 for ?id=9 and
unknown book (no id) without a parameter; the other three lines never change.
The tests of later branches are still evaluated
Only the bodies are skipped. The test of a c:when is an attribute, and the container evaluates an attribute
expression before it calls the tag, so every c:when of a c:choose has its test evaluated even after an earlier branch
matched. A test with a side effect runs, and a test that throws aborts the page although its branch could never have won.
<c:choose>
<c:when test="${true}">first</c:when>
<c:when test="${not empty prefs.put('second', 'evaluated')}">second</c:when>
</c:choose>
second test evaluated: [${prefs.second}]
<c:if test="${false}">never<c:set target="${prefs}" property="body" value="ran"/></c:if>
body ran: [${prefs.body}]
first
second test evaluated: [evaluated]
body ran: []
With <c:when test="${param.x + 1 gt 0}"> as the second branch, ?x=1 prints first and ?x=abc fails the request
(An exception occurred processing [/lazy2.jsp] at line [5], caused by the NumberFormatException of the unneeded test).
The body of a c:if whose test is false, in contrast, is not evaluated (body ran: []). Keep tests free of side effects
and of anything that can throw; guard a risky test with empty or compute the value in the controller.
The same rules in a table, with the error each violation gives:
| Rule | On violation |
|---|---|
|
translation error |
|
translation error naming the offending tag (table above) |
no text other than white space directly inside |
translation error |
|
request-time |
|
translation error for a missing attribute |
a later |
the exception is raised anyway (above) |
Worked Example: the Book Detail Page
The page below uses every tag of this page. It reads the book id, accepts it only if it is a number and exists, shows the stock level, offers a "borrow" form only when there is a copy, shows the "edit" link only to administrators, and displays a message left by the previous request exactly once. The data it needs is the listener of the setup section and the servlet that handles the form.
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:catch var="badId"><c:set var="id" value="${param.id + 0}"/></c:catch>
<c:set target="${profile}" property="name" value="${empty param.reader ? 'guest' : param.reader}"/>
<h1>Bookshelf for <c:out value="${profile.name}"/></h1>
<c:if test="${not empty sessionScope.flash}">
<p class="flash"><c:out value="${sessionScope.flash}"/></p>
<c:remove var="flash" scope="session"/>
</c:if>
<c:choose>
<c:when test="${not empty badId}">
<p class="error">Invalid book id: <c:out value="${param.id}"/></p>
</c:when>
<c:when test="${empty books[id - 1]}">
<p class="error">No book with id <c:out value="${param.id}"/></p>
</c:when>
<c:otherwise>
<c:set var="book" value="${books[id - 1]}"/>
<c:set var="copies" value="${inventory[book.id]}"/>
<c:set var="age" value="${2026 - book.year}"/>
<h2><c:out value="${book.title}"/> <small>by <c:out value="${book.author}"/></small></h2>
<p>Published ${book.year}, <c:out value="${age}"/> years ago.
<c:choose>
<c:when test="${copies gt 1}">${copies} copies on the shelf.</c:when>
<c:when test="${copies eq 1}">Last copy!</c:when>
<c:otherwise>All copies are on loan.</c:otherwise>
</c:choose></p>
<c:if test="${copies gt 0}" var="canLoan"/>
<c:if test="${canLoan}">
<form method="post" action="${pageContext.request.contextPath}/loan">
<input type="hidden" name="id" value="<c:out value="${book.id}"/>">
<button>Borrow</button>
</form>
</c:if>
<c:if test="${profile.admin}"><a href="edit?id=${book.id}">Edit</a></c:if>
</c:otherwise>
</c:choose>
@WebServlet("/loan")
public class LoanServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
long id;
try {
id = Long.parseLong(req.getParameter("id"));
} catch (NumberFormatException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid book id");
return;
}
req.getSession().setAttribute("flash", "Loan requested for book " + id);
resp.sendRedirect(req.getContextPath() + "/book.jsp?id=" + id);
}
}
Responses of the deployed page, with blank lines removed. curl "$B/book.jsp?id=3&reader=Ana", a book with one copy:
<h1>Bookshelf for Ana</h1>
<h2>The Dispossessed <small>by Ursula K. Le Guin</small></h2>
<p>Published 1974, 52 years ago.
Last copy!
</p>
<form method="post" action="/g5jstlcore/loan">
<input type="hidden" name="id" value="3">
<button>Borrow</button>
</form>
id=2 (no copy, so no form) and the failure cases:
<p>Published 1984, 42 years ago.
All copies are on loan.
</p>
<p class="error">Invalid book id: abc</p>
<p class="error">No book with id 9</p>
<p class="error">No book with id </p>
for ?id=abc, ?id=9 and no parameter. A reader name is escaped by c:out (?reader=<b>Ana</b> prints <h1>Bookshelf for <b>Ana</b></h1>).
Posting the form and following the redirect, with a cookie jar so that the session survives, shows the message once:
J=$(mktemp)
curl -s -c $J -b $J -d id=3 -o /dev/null -w '%{http_code}\n' http://localhost:18080/g5jstlcore/loan
curl -s -c $J -b $J http://localhost:18080/g5jstlcore/book.jsp?id=3
curl -s -c $J -b $J http://localhost:18080/g5jstlcore/book.jsp?id=3
The first request answers 302 (sendRedirect; a 303 is the strict Post/Redirect/Get answer, see the
servlet fundamentals page of this section) and a post with id=x answers 400. The second request prints the <p class="flash">Loan
requested for book 3</p> paragraph above the book, the third request does not, because c:remove cleared the session
attribute.
Source: Jakarta Standard Tag Library 3.0 — c:out and c:choose
Notes on the page:
-
c:catchas input validation. The only exception the page expects isNumberFormatExceptionfromparam.id + 0; it turns a bad id into a message and a normal HTTP 200 for the page, where an uncaught exception would be a 500. The better design is a controller servlet that validates the id and answers 400 or 404; the catch keeps the example on one page. -
c:setfor derived values (book,copies,age) so that the expressions are written once and the template reads as a description. The values stay in page scope; nothing leaks to request or session. -
c:ifwithvarcomputescanLoanonce; the form and any other part of the page that needs it reuse the name. -
Every user-controlled string goes through
c:out: the reader name, the id echoed in the error, the flash message. The numbers (book.year,age) are numbers and need no escaping, so the plain${book.year}is fine. -
The flash message is the standard use of
c:remove: put a message in the session, show it, remove it with an explicitscopeso that nothing else is cleared. -
State change is a POST with a validated id and a redirect afterwards. The production version also carries a CSRF token, which the servlet fundamentals page demonstrates with a filter.
-
The
actionusespageContext.request.contextPath, which is correct for the context path but does not add session rewriting; the URL page of this section coversc:url, which does both.
Security Notes
-
${...}does not escape;c:outandfn:escapeXmldo. Every value that can contain text typed by a user, including a book title read from a database or a request header, belongs inc:outunless it is deliberately markup. Use it in HTML text and in quoted attributes; use an encoder for the other contexts (above). -
Never switch escaping off with data.
escapeXmltakes any text, and everything except the word "true" disables it, including a typo, an empty attribute and anullexpression. WriteescapeXml="false"only on a literal, for text your own code produced, and consider whether it should be a tag file instead. -
Treat
targetandpropertyofc:setas code.propertymust be a literal; a value from the request must be validated and converted before it is stored.targetmust be an object that the page is allowed to modify: a request or session attribute of the current user, never a shared application-scoped bean or map (ac:seton an applicationMapchanges it for every user and is not thread safe). -
Do not build an expression from input. A tag attribute such as
value="${param.expr}"is safe, because the value is an operand, but passing request text to an API that evaluates it as an expression is EL injection; the library has no such tag, and thespring:evaltag that has is described on the Spring MVC with JSP page of this section. -
Tags do not make a page safe from scripting. Use the
scriptfreevalidator (or thescripting-invalidproperty group) to make sure a page that was converted to JSTL stays free of scriptlets; on Tomcat 11.0.26 a page withjakarta.tags.scriptfreedeclared and one scriptlet fails to translate withValidation error messages from TagLibraryValidator for [scriptfree] …: JSP page contains 1 scriptlet. -
JSPs stay under
WEB-INF. The examples of this page sit in the context root only so thatcurlreaches them; a JSP in the public tree can be requested with parameters nobody expected. -
Keep the jars current. JSTL 3.0.1 has had no release since 2023 and the Tomcat releases carry the security fixes of the container; an application should still follow the Tomcat security pages, for example CVE-2024-52318, "Incorrect JSP tag recycling leads to XSS", fixed in Tomcat 11.0.1 and 10.1.33: it concerns the way the container reuses tag handler instances, which every tag of this library depends on.
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
a title with |
|
|
|
anything but "true" is false |
omit the attribute; literal |
|
|
|
the second |
the reader is consumed |
print it once, or hold a |
|
|
a literal name |
|
only page, request, session and application |
one of the four |
|
|
add |
|
the page has |
let the page take part in the session, or use request scope |
|
a page-scope variable of the same name hides it |
one name, one scope; read |
|
the one-attribute form removes from all scopes |
always give |
|
|
check with |
|
a property without a setter |
add the setter, or set through a command method in the controller |
|
the value cannot be converted to the property type |
validate input first |
|
the |
use one form per tag |
|
a request-chosen property name |
literal property names |
|
attribute values are text unless the whole value is one expression |
|
|
the body is not buffered or rolled back |
put the risky tag first, or capture with |
|
the variable has page scope |
test within the same page |
|
only "true" is true |
compare explicitly, or have the controller compute a boolean |
two |
each |
|
a later |
every |
side-effect-free, non-throwing tests; compute risky values first |
|
|
wrap them |
|
text or tags between the children |
keep only white space |
|
the lone space is white-space-only template text |
put visible text between expressions |
|
the JSTL 1.0 "EL" library evaluates expressions itself |
use |
|
the JSTL jars are not in |
add the API and the implementation |
a missing |
an old |
replace it by the |
a scripting variable of a tag ( |
no JSTL tag creates a scripting variable |
use |
What Changed Recently
| Version | Change |
|---|---|
Jakarta Standard Tag Library 3.1 (Jakarta EE 12, under development, milestone 3.1.0-M1/M2) |
the release plan names one new feature, support for |
Jakarta Standard Tag Library 3.0 (Jakarta EE 10, 2022, still current in EE 11) |
the URIs are renamed to |
Jakarta Standard Tag Library 2.0 (Jakarta EE 9, 2020) |
the package changes from |
JSTL 1.2 (Java EE 5, 2006) |
unified expression language support, which is what allows deferred expressions such as |
JSTL 1.1 (JSP 2.0, 2003) |
the container evaluates EL, so the |
JSTL 1.0 (JSP 1.2, 2002) |
the first release: |
Three leftovers in the official material are worth knowing. The Tags 3.0 specification text still refers to the "Jakarta Server
Pages 3.0 web container" and "Expression Language 4.0" and to type-conversion "Section 1.23" of the EL specification (it is
section 1.25 of EL 6.0). The tag documentation (tagdocs) lists the c:out attributes with the type java.lang.String while the
specification says Object, boolean and Object, and its description of c:if still speaks of a "scripting variable", which
the library never creates. The Jakarta EE Tutorial calls the SQL, XML and internationalisation parts "archived", but the
specification of 3.0 and the 3.0.1 jar still contain them, with the same tags as before.
Where the Books Are Outdated
The section bibliography lists JSTL in Action (Manning, 2002), a JSTL 1.0 book for page authors on JSP 1.2, and Web
Development with JavaServer Pages (Manning, 2001), which mentions JSTL only as an announcement. The core-tag concepts that
JSTL in Action teaches are still right: c:out with escapeXml and default, c:set and c:remove, c:if,
c:choose with c:when and c:otherwise, c:catch, scoped variables and the var/scope conventions. What changed is
the environment around them.
| The books' era (JSTL 1.0, JSP 1.2) | Today (JSTL 3.0, Pages 4.0, EL 6.0) |
|---|---|
|
one library per area with |
EL works only inside JSTL attributes, not in template text |
container EL everywhere since JSP 2.0, so |
|
|
the |
scripting is switched off per page group with |
|
the model is built by a controller; the page changes a bean only for presentation state, and never with request-chosen names |
|
validate and convert in a servlet, filter or controller; |
scripting variables or |
scoped variables read with the EL; |
the 1.0 |
the container evaluates the EL in attributes of every tag, classic and simple |
Appendix A.1 of JSTL in Action points to chapter 2 for the expression language; the claim that EL does not work in template text |
the EL is chapter 3 of the book, and the claim holds only for JSTL 1.0 on JSP 1.2 |
the |
|
SQL and XML tags as a normal part of a page |
archived by the platform; use them for prototypes only |
Source: Manning — JSTL in Action
References
-
Jakarta Standard Tag Library 3.0 — 1.2 Multiple Tag Libraries
-
Jakarta Standard Tag Library 3.0 — 1.3 Container Requirement
-
Jakarta Standard Tag Library 3.0 — 2.3 Static vs Dynamic Attribute Values
-
Jakarta Standard Tag Library 3.0 — 2.7 Errors and Exceptions
-
Jakarta Standard Tag Library 3.0 — 3.1 Expressions and Attribute Values
-
Jakarta Standard Tag Library 3.0 — 3.5 Automatic Type Conversion
-
Jakarta Standard Tag Library 3.0 — Appendix A: Compatibility & Migration
-
Jakarta Standard Tag Library 3.0 — B.1 Changed between 2.0 and 3.0
-
Manning — Web Development with JavaServer Pages, Second Edition