Tag Library Descriptors & EL 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 Pages 4.0 (chapter 7 "Tag Extensions", section 2.11 "Functions" and section 3.2 "Taglib
Map"), Jakarta Standard Tag Library 3.0 (chapter 14 "Tag Library Validators"; API jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api
3.0.2, implementation org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1), 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 the Pages 4.0
specification and the TLD XML Schema web-jsptaglibrary_4_0.xsd, and every example by building it into a Maven WAR (context
path /g6tld, with the tag handlers split between the WAR and a small tag-library JAR) and running it on Tomcat 11.0.26 with
curl; the outputs and error messages shown are the ones that run produced. A Tag Library Descriptor is the contract
between a tag library and the JSP container: an XML file that names the library (its URI and preferred prefix), lists the
tags with their handler classes, attributes, body types and variables, lists the EL functions with the static methods that
implement them, and optionally names a validator and listeners. The container reads it at translation time, long before any
request, and uses it to check every use of the library; page authors and tools read the same file. The running scenario is
the Bookshelf, a small catalogue of books; the library bs below has a rating tag, a newest tag and a handful of
functions. Writing the handler classes themselves is covered by the two custom-tags pages of this section
(simple tag handlers and classic tag handlers) and tag files by Tag Files & Layouts;
this page covers what surrounds them: the descriptor, its discovery, its packaging and its validation.
The Bookshelf Used on This Page
All the examples are views of one WAR, g6tld.war, deployed under /g6tld, plus a separate JAR, bookshelf-ui, that ends up
in WEB-INF/lib. A servlet mapped to /demo/* puts a small list of books in the request and forwards to
/WEB-INF/views/<name>.jsp; the listings below are those views (the page directive, <%@ page contentType="text/html;charset=UTF-8" %>,
is left out) and every output is what curl printed.
g6tld.war
WEB-INF/web.xml (jsp-config taglib map, section "The Explicit Map in web.xml")
WEB-INF/tlds/bookshelf.tld (tags, tag-file, functions, validator, listener)
WEB-INF/tlds/scriptfree.tld (ScriptFreeTLV configuration)
WEB-INF/tlds/permitted.tld (PermittedTaglibsTLV configuration)
WEB-INF/tags/pill.tag (tag file referenced from bookshelf.tld)
WEB-INF/prelude-scriptfree.jspf (include-prelude of a property group, section "ScriptFreeTLV and PermittedTaglibsTLV")
WEB-INF/classes/bookshelf/*.class (Book, BookFunctions, RatingTag, NewestTag, NewestInfo, RatingRangeTLV, TldListener, DemoServlet)
WEB-INF/lib/bookshelf-ui-1.0.0.jar (META-INF/bookshelf-ui.tld and bookshelf/ui/BadgeTag.class)
WEB-INF/lib/jakarta.servlet.jsp.jstl-api-3.0.2.jar, jakarta.servlet.jsp.jstl-3.0.1.jar
public record Book(long id, String title, String author, int year, int rating, String isbn) { }
@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, 5, "9780441172719"),
new Book(2, "Neuromancer", "William Gibson", 1984, 4, "9780441569595"),
new Book(3, "The Left Hand of Darkness", "Ursula K. Le Guin", 1969, 4, "9780441478125")));
req.setAttribute("blurb", "A desert planet, a spice, a boy who sees the future and a war of great houses.");
String page = req.getPathInfo().substring(1);
req.getRequestDispatcher("/WEB-INF/views/" + page + ".jsp").forward(req, resp);
}
}
What a Tag Library Descriptor Is and Who Reads It
A tag library is a collection of actions (and, since JSP 2.0, EL functions) made available to a page by a taglib directive
that names the library by a URI and chooses a prefix. The URI is only an identifier: it "may be any valid URI as long as it can
be used to uniquely identify the semantics of the tag library", and it is not downloaded. The descriptor is the XML document
that stands behind it. It has the file extension .tld, and the specification gives it five consumers.
| Reader | What it takes from the descriptor |
|---|---|
The JSP container, at translation time |
The URI to descriptor mapping, the handler class of every tag, the attributes (names, required, request-time or not, type), the body type, the scripting variables, the function signatures, the validator and the listeners. It enforces "all constraints described in the TLD" before it generates any Java: a page that breaks them is not compiled |
The JSP container, at start-up |
The |
Page authors |
The URI, the suggested prefix ( |
Authoring tools and IDEs |
Everything above plus the tool-only |
Other tag libraries and frameworks |
The same file, read without loading a single class, because the descriptor "can be read by tools without instantiating objects or loader classes" |
Three pieces cooperate. The taglib directive in the page gives a URI and a prefix; the descriptor says what the library
contains; and the Java classes (tag handlers, functions, a validator, TagExtraInfo classes, listeners) do the work. The
classes fall in two groups with different lifetimes: those instantiated at translation time (TagLibraryValidator,
TagExtraInfo) and those instantiated at request time (tag handlers). Both must be reachable from WEB-INF/classes or a JAR
in WEB-INF/lib, and the translation-time class loader sees the same classes as the request-time one.
The Schema and the Skeleton of a Descriptor
Since JSP 2.0 the descriptor is described by an XML Schema. For Jakarta Pages 4.0 it is
https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd, in the namespace https://jakarta.ee/xml/ns/jakartaee; the
version attribute of the root taglib element is required and fixed to 4.0 in that schema (the 3.0 and 3.1 schemas exist
next to it and fix 3.0 and 3.1). The root element is a sequence, in this order: the description group
(description, display-name, icon), tlib-version, short-name, uri, validator, listener, tag, tag-file,
function and taglib-extension.
| Element | Meaning |
|---|---|
|
Human and tool information; all optional |
|
Required. The version of the library itself as a dewey decimal ( |
|
Required. A mnemonic name that tools use as the preferred prefix; "do not use white space, and do not start with digits or underscore" |
|
Optional. The public URI that identifies the library; it is what the implicit map of the container is built from (see below), and "leave it empty if it does not apply" |
|
Optional, at most one. A |
|
Zero or more. Servlet event listener classes shipped with the library |
|
Zero or more. A tag implemented by a Java handler class |
|
Zero or more. A tag implemented by a tag file, located by |
|
Zero or more. An EL function implemented by a public static method |
|
Zero or more. Tool-only extension elements |
The names of tag and tag-file elements share one namespace (the schema declares name unique across both), and function
names are unique among functions. A minimal descriptor, the head of the Bookshelf library, looks like this.
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd"
version="4.0">
<description>Bookshelf tags and EL functions.</description>
<display-name>Bookshelf</display-name>
<tlib-version>1.0</tlib-version>
<short-name>bs</short-name>
<uri>https://example.org/jsp/bookshelf</uri>
<!-- validator, listener, tag, tag-file and function elements follow -->
</taglib>
Tomcat 11.0.26 did not reject a descriptor that breaks the schema. A descriptor in the old namespace
http://java.sun.com/xml/ns/javaee with version="2.1" and an element the schema does not know (jsp-version) was read
without a message and its function ran (the page t-oldns of the verification WAR printed A). That is convenient for
old libraries and unhelpful for new ones: a misspelt element is not reported, it is simply ignored, so a tag library author
should check the descriptor against the schema in the build. The XML Schema and the XML text are also the contract for tools,
which are stricter than Tomcat.
The tag Element
A tag element describes one action that is implemented by a Java class. The 4.0 schema fixes the order of its children:
the description group, name, tag-class, tei-class, body-content, variable, attribute, dynamic-attributes,
example and tag-extension.
| Element | Meaning |
|---|---|
|
The action name used after the prefix: |
|
Required. The class that implements |
|
Optional. A |
|
Required, no default since JSP 2.0. |
|
Zero or more. Scripting-variable information stated directly in the descriptor |
|
Zero or more. The attributes the tag accepts |
|
|
|
An informal example for tools |
| Value | The body of the tag |
|---|---|
|
Must be empty; the container rejects any body (and |
|
May contain template text, EL and action elements, but no scripting elements. "A reasonable default for simple tag handlers" when the tag can have a body |
|
May contain any JSP syntax including scriptlets. Only classic tag handlers can use it |
|
Is not interpreted as JSP at all: its text is passed to the handler uninterpreted (an embedded query language, for instance) |
The next listing is the handler class of the rating tag. It is a plain SimpleTagSupport subclass; its properties are the
attribute names, so the container calls setValue and setMax before doTag.
public class RatingTag extends SimpleTagSupport {
private int value;
private int max = 5;
public void setValue(int value) { this.value = value; }
public void setMax(int max) { this.max = max; }
@Override
public void doTag() throws JspException, IOException {
if (value < 0 || value > max) {
throw new JspTagException("rating " + value + " is outside 0.." + max);
}
getJspContext().getOut().write("★".repeat(value) + "☆".repeat(max - value));
}
}
<tag>
<description>Shows a rating as filled and empty stars.</description>
<name>rating</name>
<tag-class>bookshelf.RatingTag</tag-class>
<body-content>empty</body-content>
<attribute>
<description>The rating, from 0 to max.</description>
<name>value</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
<type>int</type>
</attribute>
<attribute>
<name>max</name>
<rtexprvalue>true</rtexprvalue>
<type>int</type>
</attribute>
</tag>
Attributes: required, rtexprvalue, type, fragment and Deferred Values
Each attribute child describes one attribute. The schema fixes the order: description, name, required, then either the
group rtexprvalue, type and one of deferred-value or deferred-method, or the single element fragment.
| Element | Meaning |
|---|---|
|
The attribute name, a Java identifier; it must match a setter of the handler ( |
|
|
|
Whether the value may be computed at request time: a scriptlet expression or, as the specification puts it in section 1.14.1, an EL expression |
|
The Java type of the value; |
|
|
|
The value is passed to the handler as a |
|
The value is passed as a |
Section 1.14.1 states the rule that makes rtexprvalue matter: "by default, except in tag files, all attributes have page
translation-time semantics. Attempting to specify a scriptlet expression or EL expression as the value for an attribute that
(by default or otherwise) has page translation time semantics is illegal, and will result in a fatal translation error."
The rating page below uses the tag twice per book, once with the default scale and once with max="10", and also the second
tag of the library, newest, whose descriptor entry is shown after it.
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="bs" uri="https://example.org/jsp/bookshelf" %>
<bs:newest books="${books}" var="newest"/>
<p>newest: ${newest.title} (${newest.year})</p>
<c:forEach var="book" items="${books}">
<p>${book.title}: <bs:rating value="${book.rating}"/> <bs:rating value="${book.rating}" max="10"/></p>
</c:forEach>
<p>newest: Neuromancer (1984)</p>
<p>Dune: ★★★★★ ★★★★★☆☆☆☆☆</p>
<p>Neuromancer: ★★★★☆ ★★★★☆☆☆☆☆☆</p>
<p>The Left Hand of Darkness: ★★★★☆ ★★★★☆☆☆☆☆☆</p>
Scripting Variables and the variable Element
A tag can create a variable for the page. A SimpleTag does it by calling getJspContext().setAttribute(name, value, scope);
the descriptor can additionally declare the variable so that scripting elements after the tag see it as a Java local
variable. The variable element has the children description, then one of name-given (a constant name) or
name-from-attribute (the name of an attribute whose translation-time value is the name of the variable), then
variable-class (java.lang.String by default), declare (true by default) and scope, one of NESTED (the default,
visible between the start and end tags), AT_BEGIN or AT_END (visible after the end tag).
public class NewestTag extends SimpleTagSupport {
private Collection<Book> books;
private String var;
public void setBooks(Collection<Book> books) { this.books = books; }
public void setVar(String var) { this.var = var; }
@Override
public void doTag() throws JspException, IOException {
Book newest = books.stream().max(Comparator.comparingInt(Book::year)).orElse(null);
getJspContext().setAttribute(var, newest, PageContext.PAGE_SCOPE);
}
}
<tag>
<description>Finds the most recent book and exposes it under var.</description>
<name>newest</name>
<tag-class>bookshelf.NewestTag</tag-class>
<body-content>empty</body-content>
<variable>
<name-from-attribute>var</name-from-attribute>
<variable-class>bookshelf.Book</variable-class>
<scope>AT_END</scope>
</variable>
<attribute>
<name>books</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
<type>java.util.Collection</type>
</attribute>
<attribute>
<name>var</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
The attribute var has rtexprvalue set to false, which is required for name-from-attribute: the name must be a
translation-time constant. The page above printed newest: Neuromancer (1984) because the Expression Language reads the
variable from the page scope whether or not it is declared; the variable element matters only to scriptlets and to tools.
Since JSP 2.0 pages should read such variables with EL, and the scriptlet form is on the Scripting Elements (Legacy) page.
TagExtraInfo: Variables and Translation-Time Checks in Java
When the variable’s name or type depends on more than a constant or an attribute value, or when the tag needs a check that a
descriptor cannot express, the descriptor names a TagExtraInfo class in tei-class. The container calls
getVariableInfo(TagData) to learn the variables (each a VariableInfo with a name, a class name, a declare flag and a scope
constant NESTED, AT_BEGIN or AT_END) and validate(TagData) to run extra checks; TagData gives the attribute values
that are known at translation time (getAttributeString("var")) and getId() identifies the action. The schema says that
a tag with variable children must not have a TagExtraInfo class that returns variables: use one mechanism or the other.
Section 7.4.1.3 adds that since JSP 2.0 validate is the preferred method, the older isValid still works, and implementing both
is not recommended.
public class NewestInfo extends TagExtraInfo {
@Override
public VariableInfo[] getVariableInfo(TagData data) {
return new VariableInfo[] {
new VariableInfo(data.getAttributeString("var"), "bookshelf.Book", true, VariableInfo.AT_END)
};
}
@Override
public ValidationMessage[] validate(TagData data) {
String var = data.getAttributeString("var");
if (var == null || !Character.isJavaIdentifierStart(var.charAt(0))) {
return new ValidationMessage[] {
new ValidationMessage(data.getId(), "var must be a Java identifier, not [" + var + "]")
};
}
return null;
}
}
<tag>
<name>latest</name>
<tag-class>bookshelf.NewestTag</tag-class>
<tei-class>bookshelf.NewestInfo</tei-class>
<body-content>empty</body-content>
<attribute>
<name>books</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
<type>java.util.Collection</type>
</attribute>
<attribute>
<name>var</name>
<required>true</required>
</attribute>
</tag>
<%@ taglib prefix="bs" uri="https://example.org/jsp/bookshelf" %>
<bs:latest books="${books}" var="newest"/>
<p>EL: ${newest.title}</p>
<p>scriptlet: <%= newest.title().toUpperCase() %></p>
<p>EL: Neuromancer</p>
<p>scriptlet: NEUROMANCER</p>
With var="1st" the page does not translate; Tomcat reports the message the validate method returned:
org.apache.jasper.JasperException: /WEB-INF/views/latest-bad.jsp (line: [3], column: [0])
<h3>Validation error messages from TagExtraInfo for [bs:latest]</h3><p>var must be a Java identifier, not [1st]</p>
Dynamic Attributes and tag-file Elements
Two more children of the tag library complete the list of ways to define an action.
A tag that mimics an HTML element cannot know in advance which attributes a page author will pass. Setting
<dynamic-attributes>true</dynamic-attributes> in the tag element, and implementing DynamicAttributes in the handler,
makes the container hand every attribute that is not declared to setDynamicAttribute(uri, localName, value). The
bookshelf-ui JAR has a badge tag that copies the extra attributes into a span.
public class BadgeTag extends SimpleTagSupport implements DynamicAttributes {
private final Map<String, Object> extra = new LinkedHashMap<>();
private String kind = "info";
public void setKind(String kind) { this.kind = kind; }
@Override
public void setDynamicAttribute(String uri, String localName, Object value) {
extra.put(localName, value);
}
@Override
public void doTag() throws JspException, IOException {
StringWriter body = new StringWriter();
if (getJspBody() != null) {
getJspBody().invoke(body);
}
StringBuilder html = new StringBuilder("<span class=\"badge badge-").append(kind).append('"');
extra.forEach((k, v) -> html.append(' ').append(k).append("=\"").append(v).append('"'));
html.append('>').append(body).append("</span>");
getJspContext().getOut().write(html.toString());
}
}
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd"
version="4.0">
<description>Reusable Bookshelf user interface tags, packaged in a JAR.</description>
<tlib-version>1.0</tlib-version>
<short-name>bsui</short-name>
<uri>https://example.org/jsp/bookshelf-ui</uri>
<tag>
<description>A small badge with free-form HTML attributes.</description>
<name>badge</name>
<tag-class>bookshelf.ui.BadgeTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>kind</name>
<rtexprvalue>true</rtexprvalue>
</attribute>
<dynamic-attributes>true</dynamic-attributes>
</tag>
</taglib>
<%@ taglib prefix="ui" uri="https://example.org/jsp/bookshelf-ui" %>
<ui:badge kind="warn" title="Sold out" data-sku="B-1">Dune</ui:badge>
<span class="badge badge-warn" title="Sold out" data-sku="B-1">Dune</span>
The handler writes the dynamic values as they come, without escaping, which is exactly what a library author must not do with
request data; a real badge would escape them with the Java side of fn:escapeXml or an encoder (see the pitfalls table).
A tag-file element does the same job for a tag file: name is the action name and path the location of the .tag file,
which "must begin with /WEB-INF/tags if the .tag file resides in the WAR, or /META-INF/tags if the .tag file resides in a JAR".
It exists so that a tag file can be given an explicit name, a URI shared with Java tags and a place in a library, instead of
being reached through the implicit library of a tagdir directive. How to write the tag file is on the tag-files page of
this section; the descriptor side is one element.
<%@ tag body-content="empty" %>
<%@ attribute name="text" required="true" %>
<span class="pill">${text}</span>
<tag-file>
<description>A small label implemented as a tag file.</description>
<name>pill</name>
<path>/WEB-INF/tags/pill.tag</path>
</tag-file>
<%@ taglib prefix="bs" uri="https://example.org/jsp/bookshelf" %>
<bs:pill text="New"/>
<span class="pill">New</span>
EL Functions: the function Element
Expression Language functions are qualified calls, ${ns:f(a1, a2)}, that the container maps to public static methods of
public classes. The map is the descriptor: section 2.11.2 says "each such function is given a name (as seen in the EL), and a
static method in a specific class that will implement the function". A function element has the children description,
display-name, icon, name, function-class, function-signature, example and function-extension.
| Element | Meaning |
|---|---|
|
The name seen in EL: |
|
The fully qualified name of a public class that holds the method |
|
The method as Java writes it, with a fully qualified return type, the method name and the fully qualified parameter types in parentheses, separated by commas: |
|
Informal usage for tools, such as |
The EL name and the Java method name are independent: a descriptor of the verification WAR declared name f for the method
initials, and ${t:f('abc')} printed A. The Bookshelf library gives each function the name of its method and has five of them.
public final class BookFunctions {
private BookFunctions() { }
public static String initials(String name) {
if (name == null || name.isBlank()) { return ""; }
StringBuilder out = new StringBuilder();
for (String part : name.trim().split("\\s+")) {
out.append(Character.toUpperCase(part.charAt(0)));
}
return out.toString();
}
public static String isbn13(String digits) {
if (digits == null || !digits.matches("\\d{13}")) {
throw new IllegalArgumentException("not an ISBN-13: " + digits);
}
return digits.substring(0, 3) + "-" + digits.charAt(3) + "-" + digits.substring(4, 7) + "-"
+ digits.substring(7, 12) + "-" + digits.charAt(12);
}
public static boolean isClassic(int year) { return year < 1980; }
public static int pages(int items, int perPage) { return (items + perPage - 1) / perPage; }
public static String excerpt(String text, int max) {
if (text == null || text.length() <= max) { return text; }
return text.substring(0, max).stripTrailing() + "...";
}
}
<function>
<description>Initials of a name.</description>
<name>initials</name>
<function-class>bookshelf.BookFunctions</function-class>
<function-signature>java.lang.String initials(java.lang.String)</function-signature>
<example>${bs:initials(book.author)}</example>
</function>
<function>
<name>isbn13</name>
<function-class>bookshelf.BookFunctions</function-class>
<function-signature>java.lang.String isbn13(java.lang.String)</function-signature>
</function>
<function>
<name>isClassic</name>
<function-class>bookshelf.BookFunctions</function-class>
<function-signature>boolean isClassic(int)</function-signature>
</function>
<function>
<name>pages</name>
<function-class>bookshelf.BookFunctions</function-class>
<function-signature>int pages(int, int)</function-signature>
</function>
<function>
<name>excerpt</name>
<function-class>bookshelf.BookFunctions</function-class>
<function-signature>java.lang.String excerpt(java.lang.String, int)</function-signature>
</function>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="bs" uri="https://example.org/jsp/bookshelf" %>
<ul>
<c:forEach var="book" items="${books}">
<li>${book.title} by ${bs:initials(book.author)} (${bs:isbn13(book.isbn)})<c:if test="${bs:isClassic(book.year)}"> classic</c:if></li>
</c:forEach>
</ul>
<p>pages: ${bs:pages(books.size(), 2)}</p>
<p>${bs:excerpt(blurb, 30)}</p>
<ul>
<li>Dune by FH (978-0-441-17271-9) classic</li>
<li>Neuromancer by WG (978-0-441-56959-5)</li>
<li>The Left Hand of Darkness by UKLG (978-0-441-47812-5) classic</li>
</ul>
<p>pages: 2</p>
<p>A desert planet, a spice, a bo...</p>
How a Function Call Is Resolved
Section 2.11.4 gives the algorithm, and the table below pairs each step with the error Tomcat 11.0.26 produced when the step failed. Binding happens when the expression is parsed, which for a page is during translation; the arguments are evaluated at request time and coerced to the types in the signature by the usual EL rules.
| Step and the mistake | What Tomcat 11.0.26 reported |
|---|---|
Find the TLD for the prefix; the prefix is unknown ( |
|
Find the |
|
Count the arguments ( |
|
Load |
|
Find the method named and typed by |
|
Two |
|
Run the method; it throws ( |
Request-time |
Call a function without a prefix ( |
Request-time |
Section 2.11.2 also requires that the method be static ("failure to satisfy these requirements shall result in a
translation-time error"). Tomcat 11.0.26 did not enforce it: a descriptor that pointed to a non-static method
(java.lang.String notStatic(java.lang.String)) translated, and the request failed with a
NullPointerException: Cannot invoke "Object.getClass()" because "obj" is null from Method.invoke. The practical consequence
is the same: such a page does not work, but the error is a confusing stack trace at request time, not a message at translation
time.
Functions and EL 6.0 Method Calls
Functions predate the features that now replace many of them. Since EL 2.2 a page can call a method on an object
(${book.title.toUpperCase()}), since EL 3.0 it can import a class and call its static methods
(${Math.max(a, b)} after an import, see the static access page), and
EL 6.0 keeps both. A function declared in a descriptor still has three advantages: it is bound and checked at translation time;
it hides which class implements it, so a page author does not need to know or import the class; and it is the only way to offer
a library of helpers to pages that run with EL method calls and class imports restricted by configuration (see the
EL configuration page). Where the helper belongs to a domain
class, a method on that class called from EL is simpler than a function.
How a taglib URI Becomes a Descriptor
The page says uri="https://example.org/jsp/bookshelf"; the container must find the file. Section 7.3.2 calls the answer
the TLD resource path and builds a taglib map from URIs to resource paths, in a fixed order of precedence:
The map itself is assembled from five sources, highest precedence first:
| # | Source | What it contributes |
|---|---|---|
1 |
The entries of the Jakarta EE platform, when the container is a compliant EE one |
The Standard Tag Library and Faces libraries; Tomcat is not an EE platform, so the JSTL JARs in |
2 |
The taglib map in |
An explicit URI to location pair |
3 |
Implicit entries from TLDs in JARs in |
Every |
4 |
Implicit entries from TLDs under |
Every |
5 |
Implicit entries known to the container |
Libraries that the container implements itself, which must behave as the descriptor in the specification says |
The numbers 3 and 4 are the reason a library JAR can be "dropped in and used immediately through its stable URIs". The map ignores duplicates: an implicit entry is added only when the map has no entry with the same URI.
Implicit Entries: Drop a Descriptor In and Use Its URI
For WEB-INF/tlds/bookshelf.tld nothing was configured: the container searched /WEB-INF and the JARs in /WEB-INF/lib for
.tld files, saw <uri>https://example.org/jsp/bookshelf</uri> and built the entry itself. That is why rating.jsp above and
jar.jsp in the JAR section work with only a taglib directive. The conditions, from section 7.3.4: the file extension is
.tld; it lives under /WEB-INF or a subdirectory, or inside a JAR in /WEB-INF/lib under /META-INF or a subdirectory; and
the uri element is present. A descriptor without uri is not registered and can only be reached by an explicit entry or by
a path. The order in which the files are searched is implementation specific "and should not be relied on", which matters when
two descriptors claim the same URI (see the pitfalls).
The Explicit Map in web.xml
The taglib element of jsp-config binds a URI to a location; section 3.2 says the body of taglib-uri may be an absolute
or a relative URI, "there should be no entries in web.xml with the same taglib-uri value", and taglib-location is the
location of the descriptor file. The jsp-config element also holds the jsp-property-group elements, described on the
configuration and property groups page. A relative
taglib-location without a leading slash is resolved against /WEB-INF/web.xml.
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
version="6.1">
<jsp-config>
<taglib>
<taglib-uri>/bs-legacy</taglib-uri>
<taglib-location>/WEB-INF/tlds/bookshelf.tld</taglib-location>
</taglib>
</jsp-config>
</web-app>
The explicit entry gives a short name that does not depend on the file location, which is the only advantage left over the
implicit map: it lets an application keep the old URI of a library that was renamed, or give two names to the same library
during a migration. The three directives of the next page point at the same descriptor in the three ways the map allows: the
explicit entry /bs-legacy, the fallback rule (a path starting with a slash that is not in the map is taken as the
resource path), and the implicit entry.
<%@ taglib prefix="a" uri="/bs-legacy" %>
<%@ taglib prefix="b" uri="/WEB-INF/tlds/bookshelf.tld" %>
<%@ taglib prefix="c" uri="https://example.org/jsp/bookshelf" %>
<p>${a:initials('Frank Herbert')} ${b:initials('William Gibson')} ${c:initials('Ursula Le Guin')}</p>
<a:rating value="3"/>
<p>FH WG ULG</p>
★★★☆☆
The fallback "is convenient for quick development at the expense of less flexibility and accountability": the path is in every page, and moving the file means editing all the pages. Section 7.3.9 adds one more rule about the first source of the table: a container "must always use the mapping specified for a URI in the web.xml deployment descriptor if present", so an application that wants its own copy of a well-known library on an EE platform removes the container’s mapping rather than the library.
What an Unresolved URI Looks Like
A URI that no source provides is a fatal translation error; the specification says so, and Tomcat 11.0.26 phrases it like this
(page unknown-uri, uri="https://example.org/jsp/nothing"):
org.apache.jasper.JasperException: The absolute uri: [https://example.org/jsp/nothing] cannot be resolved in either web.xml or the jar files deployed with this application
The same message appears when the library JAR is missing from WEB-INF/lib, when its descriptor has no uri, when the
descriptor is in a place the scanner does not look (see the next section) and when the JAR is on the skip list. A taglib
directive that follows the first use of its prefix is a different error: The prefix [bs] specified in this tag directive has
been previously used by an action in file [/WEB-INF/views/prefix-late.jsp] line [2].
Packaging: Descriptors in JARs and Where the Container Looks
A packaged tag library is a JAR with at least one descriptor. Section 7.2.1 requires every JSP container and authoring tool to accept it, and 7.3.1 fixes the locations:
| Location | Rule |
|---|---|
Inside a JAR |
Under |
In the web application |
Under |
Not allowed |
In |
The same chapter states that "JSP 1.1 allowed only a single TLD, in META-INF/taglib.tld`". That file name is still meaningful:
the TLD resource path of a taglib directive may name a JAR instead of a `.tld, and then the descriptor is that file inside
the JAR (rule 7.3.2); it is the one use of the old file name that the current text keeps.
The verification WAR has a loose misplaced.tld in WEB-INF/classes (copied there by Maven from src/main/resources) with a
valid uri; a page that used it failed with the unresolved-URI error above, which confirms that Tomcat does not search
WEB-INF/classes. The descriptor under WEB-INF/tlds and the one in the JAR were found. The JAR is built by an ordinary
Maven module and added as a dependency:
<!-- tags/pom.xml: the JAR with META-INF/bookshelf-ui.tld in src/main/resources -->
<artifactId>bookshelf-ui</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>jakarta.servlet.jsp</groupId>
<artifactId>jakarta.servlet.jsp-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- web/pom.xml: the WAR that uses it -->
<dependency>
<groupId>org.example</groupId>
<artifactId>bookshelf-ui</artifactId>
<version>1.0.0</version>
</dependency>
How Tomcat Scans and How to Speed It Up
Tomcat finds the descriptors in JARs by scanning the JARs that its JAR scanner is configured to visit (those in
WEB-INF/lib first of all) for META-INF/*.tld. Scanning hundreds of JARs that do not contain tag libraries is
slow, and Tomcat tells you so with an INFO message logged by org.apache.jasper.servlet.TldScanner: "At least one JAR was
scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned
but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time."
The remedy is the Jar Scan Filter of the Context: the tldSkip and tldScan attributes are comma-separated file-name
patterns (* and ? wildcards, case-sensitive), defaultTldScan says what happens to JARs that match neither, and the defaults
come from the tomcat.util.scan.StandardJarScanFilter.jarsToSkip and jarsToScan properties of conf/catalina.properties.
The consequence cuts both ways: a library JAR whose name matches a skip pattern is never scanned and its URI is unresolved,
and a JAR that should be scanned but is skipped looks exactly like a missing library.
Validation: Three Layers
Section 7.4 distinguishes where a page can be validated and why: for request-time semantics ("a subelement may require the information from some enclosing element"), for authoring-tool support, and for methodological constraints ("a development group may want to constrain the way some features are used"). The Pages 4.0 specification gives a layered answer, and the Bookshelf library uses all three layers.
| Mechanism | When | What it can check |
|---|---|---|
The attribute and body rules in the descriptor |
Translation |
Mandatory attributes, which attributes exist, whether an attribute accepts a request-time expression, whether the body must be empty. "All constraints described in the TLD must be enforced" |
|
Translation, per tag |
The attribute values known at translation time of one action, and the scripting variables |
|
Translation, whole page |
Anything about the page: the order, nesting and counts of actions, the use of other libraries, the presence of scripting |
A |
Request time |
Values only known at request time; "if uncaught, this object will invoke the errorpage mechanism" |
The first layer needs no code, and its errors are the most common ones a library user meets. Each of these pages failed to translate on Tomcat 11.0.26 with the message in the right-hand column:
| The page | The message |
|---|---|
|
|
|
|
|
|
|
|
|
|
A tag whose descriptor says |
|
TagLibraryValidator
A TagLibraryValidator looks at the whole page, through its XML view: the page converted to the XML form that the
specification defines for JSP documents (see the JSP documents page).
The descriptor names the class and its initialisation parameters; the container instantiates it, calls
setInitParameters(Map) with the init-param values and then validate(prefix, uri, PageData) once for every page that
imports the library. PageData.getInputStream() is the XML view, and every element in it carries a jsp:id attribute
(namespace http://java.sun.com/JSP/Page) that the container assigns, so that a message can point at an element. The
method returns null for a valid page or an array of ValidationMessage(id, message) objects.
The Bookshelf validator rejects rating actions whose literal value exceeds a limit the descriptor configures. It reads the
XML view with a namespace-aware SAX parser, with DOCTYPE declarations disallowed and secure processing on (the input is
generated by the container, but a validator is a parser and should be configured like one).
public class RatingRangeTLV extends TagLibraryValidator {
private static final String JSP_NS = "http://java.sun.com/JSP/Page";
@Override
public ValidationMessage[] validate(String prefix, String uri, PageData page) {
int allowed = Integer.parseInt(String.valueOf(getInitParameters().getOrDefault("maxAllowed", "5")));
List<ValidationMessage> problems = new ArrayList<>();
try (InputStream xml = page.getInputStream()) {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.newSAXParser().parse(xml, new DefaultHandler() {
@Override
public void startElement(String ns, String local, String qName, Attributes atts) {
if (!uri.equals(ns) || !"rating".equals(local)) {
return;
}
String value = atts.getValue("value");
if (value != null && value.matches("\\d+") && Integer.parseInt(value) > allowed) {
problems.add(new ValidationMessage(atts.getValue(JSP_NS, "id"),
"<" + prefix + ":rating> value " + value
+ " is above the allowed maximum " + allowed));
}
}
});
} catch (Exception e) {
return new ValidationMessage[] {new ValidationMessage(null, "cannot read the XML view: " + e)};
}
return problems.isEmpty() ? null : problems.toArray(new ValidationMessage[0]);
}
}
<validator>
<description>Rejects literal rating values above maxAllowed.</description>
<validator-class>bookshelf.RatingRangeTLV</validator-class>
<init-param>
<param-name>maxAllowed</param-name>
<param-value>5</param-value>
</init-param>
</validator>
<%@ taglib prefix="bs" uri="https://example.org/jsp/bookshelf" %>
<bs:rating value="7"/>
org.apache.jasper.JasperException:
<h3>Validation error messages from TagLibraryValidator for [bs] in [/WEB-INF/views/bad-range.jsp]</h3><p>4: <bs:rating> value 7 is above the allowed maximum 5</p>
Three things in that output deserve attention. The 4: is the jsp:id of the element, an identifier the container
numbers, and not a line number (the action is on line 3 of the page); tools that turn it into a position need the container’s
map. The validator saw only the literal 7: <bs:rating value="${7}"/> and <bs:rating value="${books[0].rating + 3}"/> both
passed translation and failed at request time with the handler’s own JspTagException: rating 7 is outside 0..5 and
rating 8 is outside 0..5, which is why the handler keeps its check: a validator can only inspect what the page text says.
And the validator ran for every page that imports bs, even pages that do not use rating: validation is per page, not per
tag, so it must be quick and must not assume that the tag it checks is present.
ScriptFreeTLV and PermittedTaglibsTLV
Chapter 14 of the Standard Tag Library specification provides two ready-made validators ("reusable validation: generic
validation that custom tag-library authors might wish to incorporate in their own tag libraries"). They live in the API JAR, in
the package jakarta.servlet.jsp.jstl.tlv, and a library only needs a descriptor that names them. A library can exist
"simply to provide site-specific validation logic": it has no tags.
| Class | What it enforces |
|---|---|
|
A page without scripting elements. Parameters |
|
A page that imports only listed libraries. The parameter |
The Standard Tag Library implementation JAR already contains two such descriptors, META-INF/scriptfree.tld (URI
jakarta.tags.scriptfree, all four parameters false, so even top-level expressions are rejected) and
META-INF/permittedTaglibs.tld (URI jakarta.tags.permittedTaglibs). A site that wants other settings writes its own
descriptor, as the specification’s chapter does. The Bookshelf one allows expressions but not scriptlets or declarations:
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd"
version="4.0">
<description>Validates pages to prohibit scriptlets and declarations.</description>
<tlib-version>1.0</tlib-version>
<short-name>scriptfree</short-name>
<uri>https://example.org/jsp/scriptfree</uri>
<validator>
<validator-class>jakarta.servlet.jsp.jstl.tlv.ScriptFreeTLV</validator-class>
<init-param>
<param-name>allowDeclarations</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowScriptlets</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowExpressions</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>allowRTExpressions</param-name>
<param-value>true</param-value>
</init-param>
</validator>
</taglib>
<%@ taglib prefix="sf" uri="https://example.org/jsp/scriptfree" %>
<% int x = 1; %>
<p>${x}</p>
org.apache.jasper.JasperException:
<h3>Validation error messages from TagLibraryValidator for [scriptfree] in [/WEB-INF/views/scriptfree-bad.jsp]</h3><p>null: JSP page contains 1 scriptlet.</p>
With the library imported, a page with <p>${1 + 1} and <%= 2 + 2 %></p> is accepted (it printed <p>2 and 4</p>), because
the descriptor sets allowExpressions to true; the packaged jakarta.tags.scriptfree rejects the same page with
null: JSP page contains 1 expression. The null: is the jsp:id of the message, which ScriptFreeTLV does not set.
The second validator is configured in the same way:
<validator>
<validator-class>jakarta.servlet.jsp.jstl.tlv.PermittedTaglibsTLV</validator-class>
<init-param>
<param-name>permittedTaglibs</param-name>
<param-value>
jakarta.tags.core
https://example.org/jsp/bookshelf
https://example.org/jsp/permitted
</param-value>
</init-param>
</validator>
A page that imports the validator library and jakarta.tags.fmt fails with
taglib p (https://example.org/jsp/permitted) allows only the following taglibs to be imported: [https://example.org/jsp/bookshelf, https://example.org/jsp/permitted, jakarta.tags.core],
and a page that imports only jakarta.tags.core and the validator library printed ok. The validator library’s own URI has
to be in the list: a page that imports it has imported it.
Two limits matter. These validators are policy for page authors: they stop a careless scriptlet, not a malicious one (a page
that a hostile author controls does not need to import the library at all; the control that holds is to forbid scripting by
configuration, scripting-invalid in a property group, or by not letting such authors write JSPs, as the
configuration page explains). And they see the page through
the XML view, so they check what the page says and not what the EL expressions in it will do. jsp-config is also the natural
place to require the validator, as the next listing shows.
A property group whose include-prelude file carries the taglib directive imports the validator library into every page
the group matches, so the page author cannot forget it. The verification WAR matched one page with an exact pattern:
<jsp-property-group>
<url-pattern>/WEB-INF/views/pg-bad.jsp</url-pattern>
<include-prelude>/WEB-INF/prelude-scriptfree.jspf</include-prelude>
</jsp-property-group>
<%@ taglib prefix="sf" uri="https://example.org/jsp/scriptfree" %>
The page pg-bad.jsp itself contains only a scriptlet and no taglib directive, and Tomcat 11.0.26 rejected it with
Validation error messages from TagLibraryValidator for [scriptfree] in [/WEB-INF/views/pg-bad.jsp] and
null: JSP page contains 1 scriptlet.
Listeners in a Descriptor
A tag library can ship Servlet event listeners. The listener element holds a listener-class, and the container, which is
"required to locate all TLD files, read their listener elements, and treat the event listeners as extensions of those listed
in web.xml", instantiates and registers them before the application starts. The order among them is undefined, and resource
injection works in them as it does in tag handlers. The Bookshelf listener leaves a marker in the application scope.
public class TldListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
event.getServletContext().setAttribute("bookshelfTldListener", "started by the tag library");
}
}
<listener>
<listener-class>bookshelf.TldListener</listener-class>
</listener>
The page functions.jsp ends with <p>listener: ${applicationScope.bookshelfTldListener}</p> and printed
<p>listener: started by the tag library</p>: the listener ran with no entry in web.xml and no annotation. That is useful for
a library that needs to initialise itself (a cache, a bundle, a registry), and a supply-chain consideration for everybody
else: any JAR dropped in WEB-INF/lib with a descriptor and a listener element runs code at start-up, without being named
anywhere in the application’s own configuration. Review a library’s descriptors as well as its classes.
Tool Extensions
The descriptor has three extension points, taglib-extension on the library, tag-extension on a tag or tag file and
function-extension on a function, for information that only tools use (the specification’s example is a fictitious company
that attaches sound files to tags). The contract protects portability: "JSP containers must not alter their behavior based on
the content, the presence, or the absence of a particular Tag or Tag Library Extension Element", and a library whose
extension element says mustUnderstand="true" is invalid, so "any attempt to use an invalid tag library must produce a
translation error". An extension is declared by importing an XML namespace into the descriptor; nothing about it reaches a
running page.
Customising a Library at Deployment
Section 7.5.3 admits that there is "no convenient place in web.xml" for the settings of a tag library (the specification’s
example is a database login) and suggests that a library author put them "in a well-known location at some resource in the
WEB-INF/ portion of the Web Application" and read them with ServletContext.getResource. That advice is from the JSP 1.x
era and predates context-param conventions, @Resource injection and the container’s own configuration. Today a library
reads context parameters or JNDI entries (the Standard Tag Library does it with its jakarta.servlet.jsp.jstl.* context
parameters, see the internationalisation page), and never stores credentials
in a descriptor, in web.xml or in the WAR: they come from the environment or from the container.
Pitfalls and Error Messages
| Mistake | What happens | Fix |
|---|---|---|
A descriptor without |
No implicit map entry: |
Add the |
The |
Never found (verified for |
|
The library JAR matches a Tomcat skip pattern, or the library JAR is not in |
Unresolved URI, with no hint that scanning skipped the JAR |
Check |
Two descriptors with the same |
Tomcat 11.0.26 used the function of the second file ( |
Give every library a unique URI; look for duplicates when a JAR is vendored twice under different names |
A |
Same as above, with the wrong behaviour only in some deployments |
Use a URI under a domain you control, with the library name and, if you want, a version |
An EL expression or scriptlet in an attribute whose |
|
Set |
|
Schema-invalid descriptor (the schema allows one group or the other); Tomcat does not check the schema |
Pick one |
Forgetting |
Schema-invalid descriptor, and a tag that is |
Declare it on every tag: |
|
|
|
Wrong number of arguments in a function call |
|
Match the |
A |
|
Use fully qualified types, no parameter names, and the exact Java types; primitives by name |
A function method that is not |
Tomcat 11.0.26: no translation error; |
Make it |
A function that throws |
|
Treat functions as total (return a default) or catch the cause in the page with |
A validator that assumes the tag is present, or that parses the XML view with a default-configured XML parser |
Errors on pages that do not use the tag; a parser that resolves doctypes and external entities is an unnecessary risk |
Check for the element, and disable doctypes and external entities, as |
Using |
|
Report the element or attribute, and the tag name, in the message text |
Heavy work, or request-specific state, in a validator or |
They run at translation time, when the page is compiled, not per request |
Keep them stateless and fast |
|
Ignored by Tomcat; it is not in the 3.0 or 4.0 schema |
Drop it; the schema |
Dynamic-attribute values written to the output without escaping, as the badge example does for the sake of brevity |
Cross-site scripting through an attribute name or value |
Escape output in the handler (the Java equivalent of |
The |
|
Put directives at the top |
A listener in a descriptor that depends on the order of other listeners |
The order is undefined |
Make listeners independent |
Secrets in a descriptor (an |
They sit in the JAR, in the WAR and in the repository |
Read them from the environment or JNDI in the code |
What Changed Recently
| Version | Change |
|---|---|
JSP 1.1 (1999) |
Tag extensions with classic handlers; the descriptor is a DTD-based file, a single |
JSP 1.2 (2001) |
Several descriptors per JAR; the |
JSP 2.0 (2003) |
The descriptor becomes an XML Schema; |
JSP 2.1 (2006) |
|
Jakarta Pages 3.0 (EE 9, 2020) |
The package is |
Jakarta Pages 3.1 (EE 10, 2022) |
|
Jakarta Pages 4.0 (EE 11, 2025) |
|
Jakarta Tags 3.0 (EE 10) |
The library URIs are |
Pages 4.1 (under development for Jakarta EE 12) |
Milestone work (the specification page lists "4.1-M1", and the Maven API has a 4.1.0-M2 milestone); no change to the descriptor is part of this page’s baseline |
In practice, moving a library from Java EE to Jakarta EE touches three things: the Java imports in handlers, validators and
TagExtraInfo classes (javax.servlet.jsp becomes jakarta.servlet.jsp), the descriptor namespace and schema location if the
descriptor uses the old ones (Tomcat 11.0.26 accepted a 2.1 descriptor in the old namespace, so the change is for tools and
for tidiness), and, for libraries compiled against BodyTag.EVAL_BODY_TAG, the recompilation against EVAL_BODY_BUFFERED. The
URIs of the Standard Tag Library moved from http://java.sun.com/jsp/jstl/* to jakarta.tags.*; on Tomcat 11.0.26 with
JSTL 3.0.1 the old http://java.sun.com/jsp/jstl/core URI still resolved (legacy-uri printed OK in the verification WAR),
because the JSTL JAR still ships the old descriptors. They are identifiers, not links.
Where the Books Are Outdated
-
Web Development with JavaServer Pages (2nd edition, 2001) teaches classic tag handlers and the JSP 1.2 descriptor: a DTD document type declaration,
tlibversion,jspversion,shortname,tagclass,bodycontent, and ataglibmap directly underweb-appin a Servlet 2.3web.xml. The element names of the 1.2 DTD (tagclass,bodycontent,tlibversion) are read by Tomcat 11.0.26 (a 1.2 descriptor with<tagclass>worked in the verification WAR), but they are the old form; current descriptors usetag-class,body-contentandtlib-version, the Jakarta namespace and a schema, and thetaglibmap belongs injsp-config. ItsrtexprvalueandTagExtraInfomaterial still applies. Its default ofJSPforbodycontentno longer exists. -
The same book’s chapter on validating tag libraries is current in its mechanics (
TagLibraryValidator,PageData,ValidationMessage,jsp:id, descriptors packaged underMETA-INF) and outdated in its package names: the validator classes live injakarta.servlet.jsp.tagextnow. -
JSTL in Action (2002) describes JSTL 1.0 and its
_rttwin libraries (c_rt,fmt_rt, …), which exist so that the tags can take Java expressions; since JSP 2.0 the EL works in every request-time attribute and the twins are gone fromjakarta.tags.*. It shows custom tags that evaluate EL manually with Jakarta Taglibs'ExpressionEvaluatorManager; today a handler declares the attribute type and the container converts. ItsScriptFreeTLVandPermittedTaglibsTLVchapter is still accurate for the classes; the package isjakarta.servlet.jsp.jstl.tlvand the URIs of its examples arejakarta.tags.*. -
Both books install libraries with descriptors in
WEB-INFand entries inweb.xml; since JSP 1.2 theurielement and the implicit map make the entry unnecessary, and the JAR is simply dropped inWEB-INF/lib. -
Neither book covers
tag-file,dynamic-attributes,fragmentattributes or EL functions (JSP 2.0 and later), and they predate the deprecation of scripting, so they use scripting variables andvariableelements where today’s pages use EL. -
Tutorials that still write
<%@ taglib uri="/WEB-INF/tlds/foo.tld" prefix="foo" %>are using the fallback path rule of section 7.3.6. It works, but a stableuriis the form to use.
Official Documentation Inconsistencies
The specifications' own examples and statements need attention; the discrepancies were found by comparing them with the schema and by running them.
-
Chapter 14 of the Tags 3.0 specification shows descriptors with
<jsp-version>2.0</jsp.version>(the end tag does not match the start tag, so the document is not well formed),<short-name>jstl taglibs only</scriptfree>(again a mismatched end tag, and a short name with white space, which the schema forbids), and an elementjsp-versionthat does not exist in the 3.0 or 4.0 schema. Copying them verbatim gives a parse error; theversion="3.0"attribute of the root is what states the JSP version. Tomcat ignores an unknownjsp-versionelement in a well-formed descriptor. -
The same chapter lists the classes as
jakarta.servlet.jsp.jstl.tlv.ScriptFreeand...PermittedTaglibsin the overview and asScriptFreeTLVandPermittedTaglibsTLVin the examples. The classes areScriptFreeTLVandPermittedTaglibsTLV. -
Section 7.3.1 of the Pages 4.0 specification still gives the schema as
http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_1.xsd, and section 7.3.10’s example uses thejavaeenamespace andversion="2.1"; the schema of Pages 4.0 ishttps://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd. -
Section 7.2.3 says "it is a fatal translation error for a URI attribute value to resolve to two different TLD resource paths". Two
.tldfiles inWEB-INF/tldswith the sameuriwere accepted by Tomcat 11.0.26 without a message, and the second one won. The order of the search is "implementation-specific and should not be relied on", so the winner is not a feature. -
Section 2.11.2 says a function method that is not public static is a translation-time error. Tomcat 11.0.26 reported it at request time (see "How a Function Call Is Resolved").
-
The XML Schema comment for the
rtexprvalueelement speaks of "scriptlet expressions"; the same property controls EL expressions as well (section 1.14.1).
References
-
Jakarta Pages 4.0 specification, chapter 7 (Tag Extensions), sections 2.11 and 3.2, appendix C
-
Jakarta Pages 4.0 — 7.3.1 Identifying Tag Library Descriptors
-
Jakarta Pages 4.0 — 7.3.10 Tag and Tag Library Extension Elements
-
Jakarta Pages 4.0 — 2.11.2 Tag Library Descriptor Information
-
Jakarta Pages 4.0 — 3.1 JSP Configuration Information in web.xml
-
Jakarta Standard Tag Library 3.0 — 14 Tag Library Validators
-
Apache Tomcat 11 — Jasper 2 JSP Engine, Production Configuration