JSTL Functions
|
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 (Jakarta Tags) 3.0, chapter 15 "Functions: function tag
library", Jakarta Pages 4.0 section 2.11 "Functions" and Jakarta Expression Language 6.0, running on Apache Tomcat
11.0.26 with the jakarta.servlet.jsp.jstl-api 3.0.2 API and the org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1
implementation on Java 21. Everything was verified on 2026-10-08: the facts against the Tags 3.0 specification, and every
example by building it into a Maven WAR, running it on Tomcat 11.0.26 and requesting it with curl; the outputs and error
messages shown are the ones that run produced. The function library is the one JSTL library that has no tags: it adds
sixteen static methods to the Expression Language, so that a page can measure a collection, trim, cut, search, replace,
split, join and escape strings inside ${...} without a scriptlet. It is small, stable since 2003 and easy to misuse in
three ways: the null and range rules are not those of java.lang.String, fn:split is not String.split, and
fn:escapeXml escapes for HTML text and quoted attributes only. The running scenario is the Bookshelf, a small
catalogue of books; the data the functions read comes from the scopes explained on the
Implicit Objects & Scopes page and the syntax they plug into is
described on the Expression Language Basics page. The sibling
JSTL pages (core, iteration, URLs, i18n, formatting, SQL and XML) cover the tag libraries that these functions
usually appear inside.
The Bookshelf Used on This Page
All the examples below are views of one WAR, deployed as g5jstlfunctions.war under the context path
/g5jstlfunctions. A servlet mapped to /demo/* puts the same small object graph in the request and forwards to
/WEB-INF/views/<name>.jsp; every listing below is one of those views (the page and taglib directives are shown once
and left out afterwards), and every output is what curl printed.
public record Book(long id, String title, String author, int year) { }
@WebServlet("/demo/*")
public class DemoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setAttribute("books", List.of(
new Book(1, " Dune ", "Frank Herbert", 1965),
new Book(2, "Neuromancer", "William Gibson", 1984),
new Book(3, "The Left Hand of Darkness", "Ursula K. Le Guin", 1969)));
req.setAttribute("tags", new String[] {"sci-fi", "classic", "award"});
req.setAttribute("tagList", List.of("sci-fi", "classic"));
req.setAttribute("review", "Great <b>book</b> & \"classic\" it's");
req.setAttribute("isbn", "978-0-441-17271-9");
Map<String, Integer> stock = new LinkedHashMap<>();
stock.put("Dune", 3);
stock.put("Neuromancer", 0);
req.setAttribute("stock", stock);
req.setAttribute("none", null);
String page = req.getPathInfo().substring(1);
req.getRequestDispatcher("/WEB-INF/views/" + page + ".jsp").forward(req, resp);
}
}
The first title has two leading and two trailing spaces on purpose, and the third author has a middle initial: both are used below to show where the functions help and where they surprise.
What the Function Library Is
An EL function is a public static Java method that a tag library descriptor (TLD) publishes under a name, so that an
expression can call it as prefix:name(args). The Pages 4.0 specification (section 2.11) defines the mechanism: the
function is qualified by the prefix of the tag library that declares it, the mapping is bound into the expression when the
page is parsed, and the method is looked up in the function-class named by the TLD. Jakarta Tags 3.0 uses that
mechanism to ship one library of general-purpose string and collection helpers.
| Item | Value |
|---|---|
URI |
|
Conventional prefix |
|
TLD in the implementation JAR |
|
Implementation class |
|
Number of functions |
16: |
Where it needs a JAR |
Tomcat does not bundle Jakarta Tags: a WAR on Tomcat adds |
Version added |
JSTL 1.1 with JSP 2.0 (2003); the set of 16 has not changed since |
The fn: library is the only part of JSTL that works outside a tag attribute: because it is an EL function it can be
used anywhere an expression is allowed, in template text, in the attributes of standard and custom actions, in tag files
and in the EL of JSP documents. It needs the taglib directive (or xmlns in a JSP document) like every other library, and
without it the page fails at translation time.
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
prefix fn, URI jakarta.tags.functions"] --> B["TLD fn.tld in the JSTL JAR
(name, function-class, function-signature)"] B --> C["Page is translated:
fn:substring(a, b, c) is checked against the TLD"] C --> D["Request time:
Functions.substring(String, int, int) is invoked"] D --> E["Result goes back into the
surrounding EL expression"] C -. "wrong name or arity" .-> F["Translation error:
ELException or JasperException"] D -. "wrong argument type" .-> G["ELException:
Problems calling function"]
The diagram shows why two different kinds of mistakes fail in two different places. A function that does not exist in the
TLD, or is called with the wrong number of arguments, is rejected when the page is translated, so the first request to
that JSP fails and the error names the function. A call whose arguments cannot be coerced to the declared parameter types
fails when the request is processed, with an ELException whose message starts with Problems calling function. The
sibling Tag Library Descriptors & EL Functions page shows how the same mechanism is used to publish a function of your own.
The Sixteen Functions
The specification groups the functions by domain: length, case, substring, trimming, replacement, searching, splitting and
joining, and XML escaping. The table lists every function with its declared signature (from fn.tld), the null rule of the
specification and the verified behaviour. In every function a null String argument is processed as the empty string, so
the library never throws a NullPointerException for a missing value, which is the single biggest difference from calling
the same method on a String through EL 6.0.
| Function | Signature | Returns | Behaviour |
|---|---|---|---|
|
|
|
Number of items, or the number of characters of a String; |
|
|
|
Same as |
|
|
|
As |
|
|
|
|
|
|
|
Same for the end of the string; an empty suffix is always true |
|
|
|
0-based index of the first occurrence, |
|
|
|
|
|
|
|
Text before the first occurrence; empty when the substring is absent or empty |
|
|
|
Text after the first occurrence; empty when the substring is absent, the whole string when the substring is empty |
|
|
|
Replaces every occurrence of a literal substring; not a regular expression; replaced text is not reprocessed; an empty |
|
|
|
Tokenizes with |
|
|
|
Concatenates the elements with the separator; a |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The same escaping as |
All Sixteen in One View
The view below calls every function on the data of the Bookshelf. It uses text/plain so that the output can be read as
printed, and trimDirectiveWhitespaces so that the directive lines leave no blank lines.
all.jsp (excerpt, one line of the view per group of functions)<c:set var="t" value="${books[0].title}"/>
length=${fn:length(t)} trim=[${fn:trim(t)}] length(trim)=${fn:length(fn:trim(t))}
upper=${fn:toUpperCase(fn:trim(t))} lower=${fn:toLowerCase('Dune')}
length(books)=${fn:length(books)} length(tags)=${fn:length(tags)} length(stock)=${fn:length(stock)} length(null)=${fn:length(none)}
contains=${fn:contains('Neuromancer','man')} containsIC=${fn:containsIgnoreCase('Neuromancer','NEURO')}
startsWith=${fn:startsWith(isbn,'978')} endsWith=${fn:endsWith(isbn,'-9')}
indexOf=${fn:indexOf(isbn,'-')} indexOf-missing=${fn:indexOf(isbn,'x')} indexOf-empty=${fn:indexOf(isbn,'')}
before=[${fn:substringBefore(isbn,'-')}] after=[${fn:substringAfter(isbn,'-')}]
replace=[${fn:replace(isbn,'-','')}]
split=${fn:length(fn:split(isbn,'-'))} join=${fn:join(tags,' | ')}
escape=${fn:escapeXml(review)}
Printed by curl http://localhost:18080/g5jstlfunctions/demo/all:
length=8 trim=[Dune] length(trim)=4
upper=DUNE lower=dune
length(books)=3 length(tags)=3 length(stock)=2 length(null)=0
contains=true containsIC=true
startsWith=true endsWith=true
indexOf=3 indexOf-missing=-1 indexOf-empty=0
before=[978] after=[0-441-17271-9]
replace=[9780441172719]
split=5 join=sci-fi | classic | award
escape=Great <b>book</b> & "classic" it's
Two details of that output matter later. fn:length(stock) counts the entries of a Map, because a map is one of the
types c:forEach can iterate; and fn:escapeXml produced " for the double quote and ' for the
apostrophe, the numeric references that c:out also produces, not the named " and '.
fn:length: Collections, Arrays, Maps and Strings
A feature that JSTL 1.0 lacked and that Tags 3.0 section 15.1.1 explains is the size of a collection: Collection.size()
is not a JavaBeans property, so ${books.size} fails, and before EL 3.0 there was no way to call it. fn:length takes
anything that c:forEach can loop over and returns its size, and for a String it returns the number of characters (UTF-16
code units, as String.length()). It also accepts an Iterator or an Enumeration, because c:forEach does (the sequence is read to the end).
<h1>Bookshelf (${fn:length(books)} books)</h1>
array=${fn:length(tags)} map=${fn:length(stock)} null=${fn:length(none)} empty=${fn:length('')}
Printed: array=3 map=2 null=0 empty=0 (a String[] of three tags, a Map of two entries, a null and an empty string).
The rule is the one of c:forEach, so a record or a bean is not accepted. The failure shows its origin in the stack trace
(Functions.length), not in the page:
${fn:length(books[0])} books[0] is a Book record
jakarta.el.ELException: Problems calling function [fn:length]
jakarta.servlet.jsp.JspTagException: Don't know how to iterate over supplied "items" in <forEach>
org.apache.taglibs.standard.functions.Functions.length(Functions.java:218)
Case, Trimming and Searching
fn:trim, fn:toUpperCase and fn:toLowerCase delegate to the String methods of the same name. The Bookshelf’s first
title carries two spaces on each side; trimming it before comparing, sorting or building a URL is the most common use of
the library, and the specification’s own example trims a value before putting it in c:url. A trimmed value used as the
value of a parameter avoids the + signs that the encoder would otherwise produce for the spaces.
<c:set var="slug" value="${fn:toLowerCase(fn:replace(fn:trim(books[0].title), ' ', '-'))}"/>
<c:url var="searchUrl" value="/search">
<c:param name="q" value="${fn:trim(books[0].title)}"/>
</c:url>
slug=${slug}
url=${searchUrl}
Printed output (the session identifier is added by c:url on the first request of a client that has not sent a cookie yet,
a rewriting rule that the JSTL URLs, Imports & Redirects page of this section explains):
slug=dune
url=/g5jstlfunctions/search;jsessionid=25CA3D98D6DD525DA218908ACB244855?q=Dune
The searching functions answer yes or no questions. fn:contains and fn:containsIgnoreCase are the functions behind a
simple filter; fn:startsWith and fn:endsWith suit prefixes such as an ISBN group or a file extension, and fn:indexOf
is the building block for the cutting functions below. All of them treat an empty second argument as matching at the start
(or end), and all of them treat null as the empty string, which gives results that differ from a null-safe String
call:
contains=${fn:contains(none,'a')} contains-null-sub=${fn:contains('a',none)}
startsWith=${fn:startsWith(none,'')} indexOf=${fn:indexOf(none,'a')}
upper=[${fn:toUpperCase(none)}] trim=[${fn:trim(none)}] subst=[${fn:substring(none,0,3)}]
undefined=[${fn:toUpperCase(missingVar)}]
contains=false contains-null-sub=true
startsWith=true indexOf=-1
upper=[] trim=[] subst=[]
undefined=[]
fn:containsIgnoreCase upper-cases both arguments with the default locale of the JVM, so on a JVM whose default locale is
Turkish or Azerbaijani the letters i and I do not match each other (the same dotted and dotless i rule as
String.toUpperCase()); set the default locale of the container explicitly if the pages compare locale-sensitive text.
Cutting Strings: substring, substringBefore and substringAfter
fn:substring(string, begin, end) follows String.substring with one major difference: it never throws. The
specification says that a begin below 0 becomes 0, an end below 0 or beyond the length becomes the length, and an end
before begin or a begin beyond the last character gives the empty string. The specification suggests -1 as the
end for "to the end of the string", and Tomcat behaves exactly so:
substring(2,6)=[${fn:substring('Neuromancer',2,6)}]
substring(3,-1)=[${fn:substring('Neuromancer',3,-1)}]
substring(-5,4)=[${fn:substring('Neuromancer',-5,4)}]
substring(3,100)=[${fn:substring('Neuromancer',3,100)}]
substring(20,25)=[${fn:substring('Neuromancer',20,25)}]
substring(5,2)=[${fn:substring('Neuromancer',5,2)}]
substring(2,6)=[urom]
substring(3,-1)=[romancer]
substring(-5,4)=[Neur]
substring(3,100)=[romancer]
substring(20,25)=[]
substring(5,2)=[]
The function has exactly three parameters. Calling it with two, as the introductory example of chapter 15 does (see Pitfalls and Error Messages), is a translation error, not a shortcut for "to the end".
fn:substringBefore and fn:substringAfter cut around the first occurrence of a delimiter and return the empty string
when the delimiter is absent, so they cannot tell "nothing after the delimiter" from "no delimiter". With an empty
delimiter substringBefore returns "" and substringAfter returns the whole input, consistently with fn:indexOf
returning 0 for an empty substring.
before=[${fn:substringBefore(isbn,'-')}] after=[${fn:substringAfter(isbn,'-')}]
before-missing=[${fn:substringBefore(isbn,'x')}] after-missing=[${fn:substringAfter(isbn,'x')}]
after-empty=[${fn:substringAfter(isbn,'')}] before-empty=[${fn:substringBefore(isbn,'')}]
before=[978] after=[0-441-17271-9]
before-missing=[] after-missing=[]
after-empty=[978-0-441-17271-9] before-empty=[]
The specification’s example of extracting the text between brackets combines fn:substring with fn:indexOf. On the
Bookshelf it extracts the year from a display string:
<c:set var="text" value="Neuromancer (1984)"/>
${fn:substring(text, fn:indexOf(text, '(')+1, fn:indexOf(text, ')'))}
Printed: 1984.
Replacing, Splitting and Joining
fn:replace replaces every occurrence of a literal substring. It is not a regular expression, so a dot is a dot; the
replaced text is not scanned again, so replace('aaa','aa','a') gives aa, not a; an empty before returns the input
unchanged; and a null after removes the matches.
replace=[${fn:replace(isbn,'-','')}]
replace-regexchars=[${fn:replace('a.b.c','.','/')}]
replace-empty-before=[${fn:replace('abc','','X')}]
replace-null-after=[${fn:replace('a-b-c','-',none)}]
replace-noreproc=[${fn:replace('aaa','aa','a')}]
replace=[9780441172719]
replace-regexchars=[a/b/c]
replace-empty-before=[abc]
replace-null-after=[abc]
replace-noreproc=[aa]
fn:split returns a String[] and breaks the string with java.util.StringTokenizer. Two consequences are easy to get
wrong. The second argument is a set of delimiter characters, not a delimiter string, so fn:split('a, b;c', ',; ') gives
three tokens. And consecutive delimiters do not produce empty tokens, so fn:split('a,,b', ',') has two elements where
String.split(",") has three. An empty input or an empty delimiter set returns an array with a single element, the input.
split=${fn:length(fn:split(isbn,'-'))}
split-multi=${fn:length(fn:split('a, b;c',',; '))}
split-adjacent=${fn:length(fn:split('a,,b',','))}
split-empty-str=${fn:length(fn:split('',','))}
split-empty-delim=${fn:length(fn:split('a,b',''))}
split=5
split-multi=3
split-adjacent=2
split-empty-str=1
split-empty-delim=1
The array that fn:split returns is an ordinary value, so it plugs into c:forEach and fn:join. A c:forEach over the
words of a title, with the varStatus properties of the iteration page, and a join that re-assembles a split string:
<c:forEach var="w" items="${fn:split(books[2].title,' ')}" varStatus="s">${s.count}:${w}<c:if test="${!s.last}">,</c:if></c:forEach>
join-split=${fn:join(fn:split('x y z',' '),'+')}
years=${fn:join(fn:split('1965,1984,1969', ','), ' / ')}
1:The,2:Left,3:Hand,4:of,5:Darkness
join-split=x+y+z
years=1965 / 1984 / 1969
fn:join is declared for String[], and that is what it needs. A String[] (the tags attribute) works, with a null
array giving the empty string and a null separator giving no separator at all (sci-ficlassicaward). A List<String>
does not: EL does not coerce a list to an array, so the call fails when the request is processed, in the way described in
Pitfalls and Error Messages. Joining a list is one of the cases where an EL 6.0 method call is the better tool (see
EL 6.0 Method Calls and the Role of the Functions Today).
fn:escapeXml and Output Escaping
fn:escapeXml(string) returns the string with the five markup characters replaced, with exactly the conversions of
c:out when its escapeXml attribute is true. The specification introduces it as the concise alternative to c:out
when the escaped value is needed as an expression and not as output, for example as the value of an attribute of a custom
action, and it uses it to avoid cross-site scripting when echoing a request parameter. The two forms produce the same
text; combining them escapes twice:
escape.jsp (curl 'http://localhost:18080/g5jstlfunctions/demo/escape?q=%3Cscript%3E%22x')raw: ${review}
fn: ${fn:escapeXml(review)}
c:out: <c:out value="${review}"/>
c:out+fn:<c:out value="${fn:escapeXml(review)}"/>
c:out(noescape)+fn: <c:out value="${fn:escapeXml(review)}" escapeXml="false"/>
attr: <input name="q" value="${fn:escapeXml(param.q)}">
attr-raw:<input name="q" value="${param.q}">
raw: Great <b>book</b> & "classic" it's
fn: Great <b>book</b> & "classic" it's
c:out: Great <b>book</b> & "classic" it's
c:out+fn:Great &lt;b&gt;book&lt;/b&gt; &amp; &#034;classic&#034; it&#039;s
c:out(noescape)+fn: Great <b>book</b> & "classic" it's
attr: <input name="q" value="<script>"x">
attr-raw:<input name="q" value="<script>"x">
The last two lines are the security point of the page. Plain EL, ${param.q}, writes the request parameter into the
page as it is, and <script>"x in a quoted attribute closes the attribute and opens a tag; fn:escapeXml makes the
same value inert. Remember what the function is: an HTML (and XML) text and quoted-attribute encoder. It does not
protect a value placed in a <script> block, in an event handler such as onclick, in a style attribute, in a URL, or
in an unquoted attribute. In a JavaScript string it leaves the backslash alone (a Windows path C:\temp\new is printed as
C:\temp\new, which JavaScript reads with a tab and a newline in it), and the HTML references it produces
(') are not decoded inside a script element. For those contexts use c:url and c:param for URLs, send data
for JavaScript as JSON and let the script read it, or use a context-aware encoder library such as the OWASP Java Encoder
(org.owasp.encoder:encoder-jakarta-jsp 1.5.0), which provides separate functions for HTML content, attributes,
JavaScript, CSS and URLs.
var p = '${fn:escapeXml(path)}';
var q = '${path}';
var p = 'C:\temp\new';
var q = 'C:\temp\new';
Whether to use fn:escapeXml or c:out is mostly a matter of where the result goes. For output, c:out is clearer, has
a default attribute and is the form that auditors search for. For an attribute of another action, fn:escapeXml avoids
the c:set and jsp:attribute detours that the specification shows. Neither is applied automatically: Jakarta Pages 4.0 has no setting that escapes EL output, so every
value that comes from a request or a database has to pass through one of them. The page on
syntax, comments and escaping shows the other places where text
reaches the page, and the EL configuration page lists the
EL settings that do exist.
A Complete Bookshelf View
The view below is a realistic use of the functions: a catalogue that shows the number of books, filters by a trimmed query parameter, builds a CSS-friendly slug and a short excerpt, and lists the tags. It is the only example of the page that uses HTML output, and it escapes every value that comes from data.
list.jsp<h1>Bookshelf (${fn:length(books)} books)</h1>
<c:set var="q" value="${fn:trim(param.q)}"/>
<ul>
<c:forEach var="b" items="${books}">
<c:set var="title" value="${fn:trim(b.title)}"/>
<c:if test="${empty q or fn:containsIgnoreCase(title, q)}">
<li data-slug="${fn:toLowerCase(fn:replace(title, ' ', '-'))}">${fn:escapeXml(title)}, surname ${fn:substringAfter(b.author, ' ')}, initial ${fn:substring(b.author, 0, 1)}</li>
</c:if>
</c:forEach>
</ul>
<p>Tags: ${fn:join(tags, ', ')}</p>
<p>Excerpt: ${fn:substring(fn:trim(books[2].title), 0, 12)}${fn:length(books[2].title) > 12 ? '...' : ''}</p>
curl http://localhost:18080/g5jstlfunctions/demo/list printed:
<h1>Bookshelf (3 books)</h1>
<ul>
<li data-slug="dune">Dune, surname Herbert, initial F</li>
<li data-slug="neuromancer">Neuromancer, surname Gibson, initial W</li>
<li data-slug="the-left-hand-of-darkness">The Left Hand of Darkness, surname K. Le Guin, initial U</li>
</ul>
<p>Tags: sci-fi, classic, award</p>
<p>Excerpt: The Left Han...</p>
With ?q=dune only the first item remains (the filter trims the query and ignores case), and a query that is only
markup, such as ?q=%3Cscript%3E, matches nothing and prints nothing because the query is only compared, never output.
The view also shows a limit of the library. The third author is "Ursula K. Le Guin", and fn:substringAfter(author, ' ')
cuts at the first space, so the "surname" is K. Le Guin. There is no substringAfterLast, lastIndexOf, startsWith
with an offset or regular expression anywhere in the library: such rules belong in Java code (the servlet or a view model)
and not in the page.
EL 6.0 Method Calls and the Role of the Functions Today
The functions were designed when EL could not call methods. Since EL 2.2 (Pages 2.2) and in EL 6.0 any public method of an
object can be called from an expression, and the Java String methods are available on every string, so many of the
functions have a direct equivalent. The equivalents are not identical: a method call on a null value yields an empty
result, as ${none.contains('x')} printing nothing, but a null as an argument is passed to the Java method and fails.
| Function | EL method call | Difference |
|---|---|---|
|
|
The function is the only one that handles every type with one name, and the only one that is null-safe |
|
|
Same result for a non-null value |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The method throws |
|
none |
No method-call equivalent; keep using the function or |
upper=${'Dune'.toUpperCase()} contains=${'Neuromancer'.contains('man')} length=${'Neuromancer'.length()}
strip=[${' Dune '.strip()}]
split-regex=${'a.b.c'.split('\\.').length} fnsplit-dot=${fn:length(fn:split('a.b.c','.'))}
size=${books.size()} tags-length=${tags.length} join=${String.join(', ', tagList)}
iter-length=${fn:length(tagList.iterator())}
null-contains=${fn:contains(none,'x')}
upper=DUNE contains=true length=11
strip=[Dune]
split-regex=3 fnsplit-dot=3
size=3 tags-length=3 join=sci-fi, classic
iter-length=2
null-contains=false
Source: Jakarta Expression Language 6.0 — Resolution of model objects and their properties or methods
The practical rule is therefore: use fn: where the value may be null or of an unknown type (a request parameter, an
optional bean property, a collection of unknown kind), for fn:escapeXml, for fn:length and for code that must keep
working in a page that is also compiled by a tool that does not support EL method calls. Use method calls where the types are
known and a richer operation is needed (String.join, strip, collections, streams, the topic of the
EL lambdas, collections and streams page). A page that
mixes both styles is common and works; the one inconsistency to keep in mind is the null handling.
Pitfalls and Error Messages
What Changed Recently
| Version | Change |
|---|---|
JSTL 1.0 (2002) |
No function library, and no EL outside JSTL attributes: string operations and the size of a collection needed scriptlets or custom tags |
JSTL 1.1 and JSP 2.0 (2003) |
The |
JSTL 1.2 and EL 2.2 (2009) |
No new functions; EL 2.2 adds method calls on objects, which duplicates many of them |
Jakarta Tags 2.0 (EE 9, 2020) |
The |
Jakarta Tags 3.0 (EE 10, April 2022) |
URIs renamed to |
Jakarta Tags 3.0.2 API and 3.0.1 implementation (current on 2026-10-08) |
Maintenance releases; the implementation JAR still carries the old TLDs, so |
Jakarta Tags 3.1 (under development for Jakarta EE 12) |
Milestone 3.1.0-M1 (API artifact 3.1.0-M2 on Maven Central); planned items are |
Moving a page from Java EE to Jakarta EE therefore changes only the taglib URI (and only if the old one is to be left
behind): http://java.sun.com/jsp/jstl/functions still works, but it is a taglib identifier only, not a reachable web
page, and new code uses jakarta.tags.functions. The javax.servlet.jsp.jstl.* Java API, if used in custom code, has to
change to jakarta.servlet.jsp.jstl.*.
Where the Books Are Outdated
-
JSTL in Action (2002) describes JSTL 1.0: there are no
fn:functions in it, because they arrived with JSTL 1.1. The techniques it uses for string work (a scriptlet, a custom tag or ac:setwithc:out) are replaced by the functions on this page, and by EL method calls on Jakarta EE 11. Its taglib URIs (http://java.sun.com/jstl/core, and the_rttwin libraries) are replaced byjakarta.tags.*. -
Web Development with JavaServer Pages (2nd edition, 2001) targets JSP 1.2, where EL does not exist outside JSTL attributes and there are no EL functions at all; string handling is done with scriptlets and the
java.lang.Stringmethods. -
Any tutorial that declares the library with
http://java.sun.com/jsp/jstl/functionsis Java EE era; the current form isjakarta.tags.functions. -
Tutorials written before EL 2.2 say that the size of a collection is available only through
fn:length; on Jakarta EE 11${list.size()}also works, althoughfn:lengthis still the null-safe form.
Official Documentation Inconsistencies
The specification’s own examples need attention, and the discrepancies were found by running them.
-
The introductory example of chapter 15 shows
${fn:substring(text, fn:length(text)-10)}, with two arguments. The function is declared with three, and Tomcat 11.0.26 rejects the page at translation time withFunction [fn:substring] specifies [3] params, but [2] were declared. A working form is${fn:substring(text, fn:length(text)-10, -1)}. -
The same chapter shows
${fn:escapeXml(param:userName)}with a colon betweenparamand the name; the EL parser rejects the page (ParseException: Encountered ":"). The working expression is${fn:escapeXml(param.userName)}. -
The example
${fn:replace(text, '-', '•')}writes a numeric character reference to U+0095, a C1 control character. Browsers map it to a bullet for historical reasons (the Windows-1252 table), but HTML 5 reports it as a parse error;•is the reference for a bullet. -
The text of
fn:endsWithsays "If substring is null" where the parameter is calledsuffix: a copy-paste error that does not change the behaviour.
References
-
Jakarta Standard Tag Library 3.0 specification, chapter 15 (Functions) and Appendix B (Changes)
-
Jakarta Standard Tag Library 3.0 — Chapter 15, Functions: function tag library
-
Jakarta Standard Tag Library 3.0 — String manipulation functions
-
Jakarta Expression Language 6.0 — Resolution of model objects and their properties or methods