Configuration and Property Groups
|
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 3, "JSP Configuration"), Jakarta Servlet 6.1 (deployment descriptor,
web fragments and ServletContext.getJspConfigDescriptor) and Jakarta Standard Tag Library 3.0 (section 2.8, "Configuration
Data"), running on Apache Tomcat 11.0.26 and Java 21. Everything was verified on 2026-10-08: the facts against the
specifications, the jsp_4_0.xsd schema and the Javadoc, and every example by building it into a Maven WAR and running it
on Tomcat 11.0.26 with curl; the outputs and error messages shown are the ones that run produced. Most of what a JSP page
can say about itself, with the page directive, can also be said once for a whole directory or file extension in
web.xml: the page encoding, the content type, whether scripting is allowed, whether EL is evaluated, a header and a footer
to include in every page. That is the job of the jsp-config element. The running scenario is the Bookshelf, a small
catalogue of books whose JSP pages start in a flat directory and are then split into an admin/ area, a legacy/ area
and a strict/ area, each with its own settings.
Where JSP Configuration Lives
A web application can carry general JSP configuration in its deployment descriptor, WEB-INF/web.xml, in a single
jsp-config element (a child of web-app) with two kinds of children: taglib elements, which map a tag-library URI to
a tag library descriptor, and jsp-property-group elements, which attach properties to a set of files. The same element
can appear in a web-fragment.xml inside a library JAR; the Servlet specification says that jsp-property-group elements
from fragments are additive, and recommends that fragments use path patterns, not extension patterns, together with a
subdirectory named after the fragment, so a fragment’s group cannot capture the application’s own JSP files.
The configuration is part of the container contract, not of the JSP engine. Since Servlet 3.0 the servlet container reads
jsp-config from web.xml and every fragment and makes it available through ServletContext.getJspConfigDescriptor(); the
JSP engine (Jasper in Tomcat, WaSP in GlassFish, Jetty’s Apache JSP) asks the container instead of re-parsing the
descriptor. The returned JspConfigDescriptor is read-only: it has getTaglibs() and getJspPropertyGroups() and no
setters, so there is no programmatic way to add a property group. Configuration is declarative, in the descriptor.
The element order inside jsp-property-group is fixed by the schema (jsp_4_0.xsd): the description group, one or more
url-pattern, then el-ignored, error-on-el-not-found, page-encoding, scripting-invalid, is-xml,
include-prelude, include-coda, deferred-syntax-allowed-as-literal, trim-directive-whitespaces,
default-content-type, buffer and error-on-undeclared-namespace. Tomcat 11.0.26 does not validate web.xml by
default and accepted a group with el-ignored placed before url-pattern (verified), but a validating tool, an IDE or
another container may not, so keep the schema order.
The Bookshelf assembles one jsp-config element in the sections that follow. This servlet reads it back and prints it,
which is also the quickest way to check what the container really parsed.
@WebServlet("/jspconfig")
public class ConfigServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
JspConfigDescriptor cfg = getServletContext().getJspConfigDescriptor();
for (TaglibDescriptor t : cfg.getTaglibs()) {
resp.getWriter().printf("taglib %s -> %s%n", t.getTaglibURI(), t.getTaglibLocation());
}
for (JspPropertyGroupDescriptor g : cfg.getJspPropertyGroups()) {
resp.getWriter().printf("group %s elIgnored=%s scriptingInvalid=%s isXml=%s prelude=%s coda=%s%n",
g.getUrlPatterns(), g.getElIgnored(), g.getScriptingInvalid(), g.getIsXml(),
g.getIncludePreludes(), g.getIncludeCodas());
}
}
}
curl http://localhost:18080/g3config/jspconfig on Tomcat 11.0.26 (a property that is not declared prints null)taglib urn:bookshelf:text -> /WEB-INF/tlds/bookshelf.tld
group [*.jsp] elIgnored=null scriptingInvalid=null isXml=null prelude=[/WEB-INF/jspf/prelude1.jspf] coda=[/WEB-INF/jspf/coda1.jspf]
group [/legacy/*] elIgnored=true scriptingInvalid=null isXml=null prelude=[] coda=[]
group [/admin/*] elIgnored=null scriptingInvalid=true isXml=null prelude=[/WEB-INF/jspf/prelude2.jspf] coda=[/WEB-INF/jspf/coda2.jspf]
group [/strict/*] elIgnored=null scriptingInvalid=null isXml=null prelude=[] coda=[]
group [/literal/*] elIgnored=null scriptingInvalid=null isXml=null prelude=[] coda=[]
group [*.svg] elIgnored=null scriptingInvalid=null isXml=true prelude=[] coda=[]
group [/buf/*] elIgnored=null scriptingInvalid=null isXml=null prelude=[] coda=[]
Source: Jakarta Servlet 6.1 — 8.2.3 Assembling the Descriptor from web.xml, web-fragment.xml and Annotations
The web.xml itself is the Servlet 6.1 one, shown with the others in
Servlet Fundamentals: the jakartaee namespace and version="6.1". Note
that the JSP 1.2-era <taglib> element placed directly under <web-app> no longer exists; it moved inside jsp-config
in Servlet 2.4 (see Where the Books Are Outdated).
The Taglib Map
A taglib directive names a tag library by URI: <%@ taglib prefix="c" uri="jakarta.tags.core" %>. The container turns
that URI into a TLD resource path through the taglib map, a table from URI to a resource path inside the web
application. The map is built from several sources, listed here from highest to lowest precedence as the Pages
specification states them (section 7.3.2):
| Order | Source of map entries |
|---|---|
1 |
The tag libraries that are part of the Jakarta EE platform, when the container is platform-compliant: the Jakarta Tags (JSTL) libraries and the Jakarta Faces libraries |
2 |
The explicit |
3 |
Implicit entries deduced from the |
4 |
Implicit entries known to the container itself |
Entries 3 and 4 are why you almost never write a taglib element today: a TLD that declares <uri> is found
automatically, and so is every library inside a JAR. The explicit map still has two uses. It is the only way to give a
tag library a URI that its TLD does not declare, for example an alias or an application-internal name, and it is the
way to point a URI at one specific TLD when several candidates exist.
A taglib element has exactly two children. taglib-uri is the URI to be used in taglib directives; it may be an
absolute URI or a relative one, and the specification forbids two entries with the same taglib-uri.
taglib-location is the context-relative location of the TLD file (or of a JAR with META-INF/taglib.tld).
The Bookshelf keeps a TLD with one EL function and gives it the application-internal name urn:bookshelf:text.
<jsp-config>
<taglib>
<taglib-uri>urn:bookshelf:text</taglib-uri>
<taglib-location>/WEB-INF/tlds/bookshelf.tld</taglib-location>
</taglib>
</jsp-config>
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee" version="4.0">
<tlib-version>1.0</tlib-version>
<short-name>bs</short-name>
<function>
<name>shout</name>
<function-class>org.example.bookshelf.Text</function-class>
<function-signature>java.lang.String shout(java.lang.String)</function-signature>
</function>
</taglib>
<%@ taglib prefix="bs" uri="urn:bookshelf:text" %>
<h1>${bs:shout('bookshelf')}</h1>
<h1>BOOKSHELF!</h1>
Two behaviours around the map are worth knowing, and both were observed on Tomcat 11.0.26. A URI that is not in the map is
interpreted as a direct path to the TLD, the fallback meant for casual use during development (spec 7.3.6.2), so
uri="/WEB-INF/tlds/bookshelf.tld" worked and printed BY PATH!. A URI that matches nothing at all is a fatal translation
error:
org.apache.jasper.JasperException: The absolute uri: [urn:bookshelf:nothing] cannot be resolved in either web.xml or the jar files deployed with this application
Tag libraries, TLD files and EL functions are the subject of Tag Library Descriptors & EL Functions; here only the mapping
matters. The two JSTL validators that a TLD can name, ScriptFreeTLV and PermittedTaglibsTLV, are enforced through a
TLD, not through jsp-config, and are described with the TLD elements (Jakarta Tags 3.0, chapter 14, "Tag Library
Validators", shows the <validator> entries).
Property Groups: What They Are
A JSP property group is a collection of properties that apply to a set of files that represent JSP pages. A group is a
jsp-property-group element with one or more url-pattern children (which files) and any number of property elements
(which settings). Four points of the specification define how groups behave.
-
A group makes its files JSP files. There is an implicit property: every resource that matches a group’s
url-patternis a JSP file, whatever its extension. That is how a file such ascover.svgorpage.htmis compiled by the JSP engine instead of being served as a static file (both observed below). -
Most properties apply to a translation unit. A translation unit is the requested JSP file together with every file it includes through the
includedirective. The exceptions arepage-encodingandis-xml, which apply to each file separately, because an included file may be stored in a different encoding or be an XML document. -
Groups never affect tag files. A tag file (
.tag,.tagx) ignores everyjsp-property-group; its settings come from thetagdirective in the file. -
Page-level settings override groups. Where the same property exists as an attribute of the
pagedirective, the attribute in the page wins, except for the conflicts that are translation errors (an encoding named twice with different values, shown below).
| Element | Default | Page-directive override | What it does |
|---|---|---|---|
|
|
|
Pass |
|
|
|
Throw |
|
none ( |
|
Character encoding of the JSP source file |
|
|
none (the group is the only switch) |
Make scriptlets, declarations and expressions a translation error |
|
|
none ( |
Treat the files as JSP documents in XML syntax |
|
none, repeatable |
none |
A file included at the start of every page of the group |
|
none, repeatable |
none |
A file included at the end of every page of the group |
|
|
|
Allow the characters |
|
|
|
Remove template text that contains only whitespace |
|
none |
|
Content type of the response when the page does not state one |
|
container default (8 KB in Tomcat) |
|
Buffering of the |
|
|
none |
Make a tag with an undeclared prefix a translation error (JSP documents already do this) |
Besides these properties a group carries url-pattern and the optional description, display-name and icon of the
schema’s description group, which have no effect on the page. The page directive attributes
themselves are described on Directives; here only their relation to the groups matters.
URL Patterns, Precedence and Which Group Applies
A group applies to the resources that match any of its url-pattern values. The patterns use the same syntax as the
Servlet specification’s chapter 12 (URL mapping), with one difference: they are bound at translation time, against the
path of the JSP file, not at request time against the request URL. Three forms are used in practice.
| Form | Example | Matches |
|---|---|---|
Extension |
|
Every file with that extension, anywhere in the application |
Path prefix |
|
Every file below that directory, at any depth |
Exact |
|
That one file |
Mixing a wildcard in the middle of a path is not a valid pattern. Tomcat 11.0.26 refused to start a web application whose
group used /buf/*.jsp, with java.lang.IllegalArgumentException: Invalid <url-pattern> [/buf/*.jsp] in servlet mapping
in the log; the application then answered 404. To cover the JSP files of one directory, use /buf/*.
Several groups, and the servlet mappings, can all match one file. The Pages specification (section 3.3) says:
-
If a resource matches a
servlet-mappingpattern and a group pattern, the most specific pattern applies, with the rules of the Servlet specification: an exact match beats a longest path prefix, which beats an extension. -
If the patterns are identical, the group wins over the
servlet-mapping. -
If at least one group holds the most specific matching pattern, the resource is a JSP file, and the properties of that group apply.
-
In addition, once the resource is a JSP file, every
include-preludeandinclude-codaof every matching group applies, in the order the groups appear inweb.xml.
The wording "the properties of that group apply" suggests that the other matching groups are ignored. Tomcat 11.0.26 does not do that for the ordinary properties: it decides each property separately, taking it from the most specific matching group that declares it. A less specific group’s value therefore still applies when the more specific group is silent about it. The experiments below make the difference visible, and they are the behaviour of Tomcat; a container that follows the sentence literally would give the outcome the specification describes. Declaring in the most specific group every property that matters removes the difference.
property group:
which pattern is most specific?"} sm -->|"servlet-mapping is more specific
(for example /other/*)"| servlet["The mapped servlet answers;
the file is not compiled as a JSP"] sm -->|"a property group matches
(or equal pattern: group wins)"| jsp["The resource is a JSP file"] jsp --> best["Most specific group:
exact > longest path prefix > extension"] best --> props["Each property comes from the most specific group
that declares it (Tomcat)"] jsp --> incl["Every matching group contributes
include-prelude and include-coda,
in web.xml order"] props --> page["The page directive overrides
a group value where an attribute exists"] incl --> page page --> out["Translation unit compiled and run"]
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>true</scripting-invalid>
<default-content-type>text/html;charset=UTF-8</default-content-type>
<include-prelude>/WEB-INF/jspf/prelude1.jspf</include-prelude>
</jsp-property-group>
<jsp-property-group>
<url-pattern>/exact/*</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
<jsp-property-group>
<url-pattern>/exact/a.jsp</url-pattern>
<el-ignored>false</el-ignored>
</jsp-property-group>
<jsp-property-group>
<url-pattern>/scripts/*</url-pattern>
<scripting-invalid>false</scripting-invalid>
</jsp-property-group>
<jsp-property-group>
<url-pattern>*.htm</url-pattern>
<default-content-type>text/html;charset=UTF-8</default-content-type>
</jsp-property-group>
<jsp-property-group>
<url-pattern>/pick/*</url-pattern>
<el-ignored>false</el-ignored>
</jsp-property-group>
<servlet-mapping>
<servlet-name>plain</servlet-name>
<url-pattern>/pick/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>plain</servlet-name>
<url-pattern>/other/*</url-pattern>
</servlet-mapping>
| Request | Matching patterns | Observed |
|---|---|---|
|
|
|
|
|
|
|
|
A scriptlet runs and prints 3: the prefix group sets |
|
|
HTTP 500: |
|
|
The JSP is compiled and served ( |
|
|
|
|
|
The |
Deactivating EL: el-ignored
The sequence ${expr} was not reserved before JSP 2.0, and #{expr} was not reserved before JSP 2.1, so an old page
can contain such text with no intention of calling the expression language. The el-ignored property passes both
sequences through verbatim for the matching files. The default depends on the version of the descriptor: a web.xml in
Servlet 2.3 or earlier format ignores EL (backward compatibility), a Servlet 2.4 or later one evaluates it. Since the
Bookshelf descriptor is version="6.1", EL is on, and a group is needed only for the pages that must stay as they are.
The page can override the group with <%@ page isELIgnored="false" %>. Tag files have no default: they always
evaluate EL unless the tag directive says isELIgnored="true". The specification states the whole truth table:
Group el-ignored |
page directive isELIgnored |
Result |
|---|---|---|
unspecified |
unspecified |
Ignored if the |
|
unspecified |
Evaluated |
|
unspecified |
Ignored |
any |
|
Evaluated |
any |
|
Ignored |
Whether EL is on also decides whether the quote sequences \$ and \# are recognised: with EL evaluation enabled they
produce a literal $ and #; with EL disabled they are not recognised at all, and the backslash stays in the output.
${...} text must not be evaluated<jsp-property-group>
<url-pattern>/legacy/*</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
<p>Total: ${1 + 2}</p>
<%@ page isELIgnored="false" %>
<p>Total: ${1 + 2}</p>
*.jsp group)<!-- prelude1 -->
<p>Total: ${1 + 2}</p>
<!-- coda1 -->
<!-- prelude1 -->
<p>Total: 3</p>
<!-- coda1 -->
Disabling EL is a compatibility tool, not a security control. If the goal is to stop an injected ${...} from being
evaluated, remember that the sequence in the source of a page is the author’s, and that EL never re-evaluates request
data it prints. The risk of EL is in expressions built from user input, which Security discusses.
Disabling Scripting: scripting-invalid
scripting-invalid set to true makes scriptlets, declarations and expressions a translation error for the matching files.
Scripting is on by default. JSP 2.0 introduced the element as an easier replacement for the older practice of
enforcing the rule with a tag-library validator (ScriptFreeTLV in the Jakarta Tags TLDs). It is the single most useful
hardening switch in the descriptor (Scripting Elements (Legacy) shows what to write instead): a project that has moved to EL and tags can turn scripting off for every page and
stop a scriptlet from ever re-entering the code base.
<jsp-property-group>
<url-pattern>/admin/*</url-pattern>
<scripting-invalid>true</scripting-invalid>
<include-prelude>/WEB-INF/jspf/prelude2.jspf</include-prelude>
<include-coda>/WEB-INF/jspf/coda2.jspf</include-coda>
</jsp-property-group>
<p>count: <%= 1 + 2 %></p>
org.apache.jasper.JasperException: /admin/bad.jsp (line: [1], column: [14]) Scripting elements ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) are disallowed here.
The error is raised at translation time, so the page fails on the first request (or at precompilation), never in the
middle of a response. The setting applies to the whole translation unit, which means that a scriptlet inside a file
included with the include directive is rejected too. A tag file is not affected by the group and keeps whatever
scripting its own directives allow.
Unknown Identifiers: error-on-el-not-found
By default an EL expression that names an identifier nobody defined resolves to null, and the output is an empty string.
That hides typos: ${owner} for a misspelt request attribute looks like an empty field, not a bug. Jakarta Pages 3.1
added an option to raise jakarta.el.PropertyNotFoundException instead. The group element error-on-el-not-found sets it
for matching pages; the page directive attribute errorOnELNotFound and the tag directive attribute of the same name
override it. Tag files have no group default: they return null unless their own directive says true.
Group error-on-el-not-found |
page directive errorOnELNotFound |
Unknown identifier |
|---|---|---|
unspecified or |
unspecified |
|
|
unspecified |
|
any |
|
|
any |
|
|
The scope of the setting is the identifier, not the property: it applies to a name that no resolver can resolve (the
NotFoundELResolver at the end of the chain). A property that is missing from an existing bean still raises
PropertyNotFoundException regardless of the setting, as the Expression Language chapter explains (see Expression Language Basics).
<jsp-property-group>
<url-pattern>/strict/*</url-pattern>
<error-on-el-not-found>true</error-on-el-not-found>
<error-on-undeclared-namespace>true</error-on-undeclared-namespace>
</jsp-property-group>
<p>Owner: ${owner}</p>
<%@ page errorOnELNotFound="false" %>
<p>Owner: [${owner}]</p>
<p>Owner: []</p>jakarta.el.PropertyNotFoundException: Unknown identifier [owner]
Turning it on is a good development-time default. In production the 500 page it produces is an ordinary error page, so the error-page handling described on Error Handling applies to it.
Page Encoding: page-encoding
page-encoding sets the pageEncoding of the files of a group: the character set in which the source files are stored,
so the container decodes them correctly. Its valid values are those of the pageEncoding attribute. It matters only for
pages in standard syntax; a JSP document in XML syntax takes its encoding from the XML declaration or a byte-order mark.
Without any statement the page is read as ISO-8859-1, which is why a UTF-8 file with a non-ASCII title can show
mojibake when nothing configures the encoding.
It is a translation-time error to name different encodings in the pageEncoding attribute of a page and in a group that
matches the page, and also to name different encodings in the XML declaration of a document and in a matching group. Naming the
same encoding twice is legal. A group should therefore be the single place that declares the encoding, and a page that needs
a different one must sit in a different group.
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<page-encoding>UTF-8</page-encoding>
</jsp-property-group>
<%@ page pageEncoding="ISO-8859-1" %>
x
org.apache.jasper.JasperException: /enc.jsp (line: [1], column: [2]) Page-encoding specified in jsp-property-group [UTF-8] is different from that specified in page directive [ISO-8859-1]
The response encoding, the request encoding and the byte-order-mark detection algorithm are separate subjects. The request side is not a jsp-config property at all: it is the Servlet element <request-character-encoding> of web.xml (and <response-character-encoding> for the response), which is why no property group sets it. All of them are covered on
Character Encoding, together with default-content-type (below).
Implicit Includes: include-prelude and include-coda
include-prelude and include-coda name context-relative files that the container includes, as if with the include
directive, at the start and at the end of every JSP page of the group. They are the configuration equivalent of
"every page begins with the same taglib directives and ends with the same footer", and the standard way to remove
that boilerplate. Both elements can repeat inside one group, in which case the files are included in the order written.
When more than one group matches a page, the preludes of all of them are included, in the order in which the groups appear in
web.xml; the codas likewise, in order. The specification points out that this is unlike the other
properties, where a single group’s pattern is selected. A prelude or coda follows the rules of a statically included JSP
segment: start and end tags must be in the same file, so a prelude cannot open an HTML element that only the coda closes
if that element is built from a JSP tag. The conventional extension for such fragments is .jspf, and the files
belong under WEB-INF so they cannot be requested directly.
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<include-prelude>/WEB-INF/jspf/prelude1.jspf</include-prelude>
<include-coda>/WEB-INF/jspf/coda1.jspf</include-coda>
</jsp-property-group>
<jsp-property-group>
<url-pattern>/admin/*</url-pattern>
<include-prelude>/WEB-INF/jspf/prelude2.jspf</include-prelude>
<include-coda>/WEB-INF/jspf/coda2.jspf</include-coda>
</jsp-property-group>
<!-- prelude1 -->
<!-- prelude2 -->
<p>report 3</p>
<!-- coda1 -->
<!-- coda2 -->
The coda order observed is coda1 then coda2, in descriptor order, and not the nesting order (coda2 first) that a
reader might expect from "the outer group closes last". If the order matters, put both files in one group, in the order
needed. A prelude that does not exist is an error for every page of the group:
org.apache.jasper.JasperException: /missing/p.jsp (line: [1], column: [1]) JSP file [/WEB-INF/jspf/nothing.jspf] not found
JSP Documents: is-xml
is-xml marks the files of a group as JSP documents: XML-syntax pages that must be well-formed XML. The default extension
.jspx (and .tagx for tag files) already means "document". The element exists for two reverse cases, both shown in
the specification: setting is-xml to false for *.jspx to treat those files as ordinary pages, and setting it to
true for another extension, such as *.svg, so that a generator of XML output is written as a document. Like
page-encoding, it applies per file and not per translation unit. The syntax of documents (jsp:root, jsp:output,
namespaces) is the subject of JSP Documents (XML Syntax); here only the switch matters.
<jsp-property-group>
<url-pattern>*.svg</url-pattern>
<is-xml>true</is-xml>
</jsp-property-group>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:c="jakarta.tags.core" width="120" height="40" version="2.0">
<text x="4" y="24">${'Dune'}</text>
</svg>
text/xml;charset=UTF-8)<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="2.0" height="40" width="120"><text y="24" x="4">Dune</text></svg>
Two details of the response are worth reading closely. The unused xmlns:c declaration was dropped from the output, and the
content type is text/xml, not the image/svg+xml that an SVG response normally carries, so a document meant to be
served as an image needs its content type set explicitly.
Deferred Syntax as Literal: deferred-syntax-allowed-as-literal
From JSP 2.1 the sequence #{ is reserved for deferred EL expressions, so a page that uses it in template text, for
example a placeholder such as #{title} meant for a client-side template, fails to translate. The element
deferred-syntax-allowed-as-literal (default false), or the page attribute deferredSyntaxAllowedAsLiteral, allows it
as plain text. The related compatibility rule (spec 3.4) is that #{expr} as the value of a tag attribute is evaluated
only when the TLD declares jsp-version 2.1 or later; in a TLD that declares an older version it is a string literal.
#{...} as text<jsp-property-group>
<url-pattern>/literal/*</url-pattern>
<deferred-syntax-allowed-as-literal>true</deferred-syntax-allowed-as-literal>
</jsp-property-group>
<p>Placeholder: #{title}</p>
org.apache.jasper.JasperException: /literal-off.jsp (line: [1], column: [19]) #{...} is not allowed in template text
When only a few places need the literal, the quote sequences are cheaper than a group. With EL evaluation enabled, \#
produces a # and \$ a $, so no configuration is involved:
<p>a \#{title} b \${x}</p>
<p>a #{title} b ${x}</p>
Removing Whitespace: trim-directive-whitespaces
Whitespace in template text is preserved, so every taglib directive, every c:set and every blank line between actions
adds a line break to the response. trim-directive-whitespaces (default false), or the page attribute
trimDirectiveWhitespaces, removes template text that consists only of whitespace. Text that contains anything else is kept
exactly. It has no effect on JSP documents. In the specification’s example, two taglib lines followed by
Hello World! produce two blank lines without the setting and none with it; in Tomcat 11.0.26 that exact case already
printed no blank lines in the test application, so the visible effect is shown below with actions in the page.
c:set actions between a directive and the text, with and without the setting<jsp-property-group>
<url-pattern>/trim/*</url-pattern>
<trim-directive-whitespaces>true</trim-directive-whitespaces>
</jsp-property-group>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="a" value="1"/>
<c:set var="b" value="2"/>
Total: ${a + b}
cat -v: the file in /trim/ yields one leading line break, the same file outside the group yields three(trim)
Total: 3
(no trim)
Total: 3
The setting is safe for HTML. It is not safe for content where blank lines matter, such as plain text or Markdown
generated by a page, and for <pre> blocks built from separate actions; set it per group, not blindly for everything.
Content Type and Buffer: default-content-type and buffer
default-content-type supplies the response content type of the group’s pages when the page has no contentType
attribute. Its values are those of the contentType attribute, including the charset. Without it, and without any
contentType, Tomcat 11.0.26 answered text/html;charset=UTF-8 in the test application (observed with no group at all);
the Pages specification still names ISO-8859-1 as the fallback character encoding for page decoding. Stating
the value in a group makes the answer independent of the container’s default, which matters for portability.
buffer sets the buffering of the page’s out writer for the group: the same values as the buffer attribute of the
page directive, none or a size in kilobytes such as 32kb. The default is container-specific; Tomcat’s is 8 KB
(observed 8192). A larger buffer lets a page set headers, cookies or an error status after producing a lot of
output, because nothing is committed until the buffer fills; none makes every write go straight to the response and is
rarely what you want.
<jsp-property-group>
<url-pattern>/buf/*</url-pattern>
<buffer>32kb</buffer>
</jsp-property-group>
buffer=${pageContext.out.bufferSize}
buf/size.jsp prints 32768, size-default.jsp outside the group prints 8192buffer=32768
buffer=8192
Both settings are overridden by the contentType and buffer attributes of the page directive. The relation of
default-content-type to pageEncoding, to <response-character-encoding> in web.xml and to fmt:requestEncoding
belongs to Character Encoding.
Undeclared Namespaces: error-on-undeclared-namespace
In a JSP page in standard syntax a tag whose prefix was never declared is, by default, silently ignored: the text
<z:greeting/> is copied to the output as plain template text. That turns a forgotten taglib directive into an
invisible bug. error-on-undeclared-namespace (default false) makes it a translation error, as it already is for JSP
documents. The strict area of the Bookshelf turns it on, in the group shown in the error-on-el-not-found section.
<p>x</p>
<z:greeting/>
org.apache.jasper.JasperException: /strict/ns.jsp (line: [2], column: [0]) A custom tag was encountered with an undeclared namespace [z]
JSTL Configuration Settings and Context Parameters
The configuration of the standard tag library is a different mechanism from jsp-config, but it lives in the same
descriptor and is often needed together, so it is part of this page. Jakarta Tags 3.0 (section 2.8) defines
configuration settings: values that change how the actions behave and that can be set in two ways. Application-wide, as a
context-param in web.xml whose name is the setting’s name and whose value is a string. For one scope, as a
configuration variable stored in the page, request, session or application scope, with the class
jakarta.servlet.jsp.jstl.core.Config. A configuration variable overrides the context parameter for as long as its scope
lasts. The class hides the real attribute name, so that a variable set in the request scope does not collide with a
same-named variable in the session; use Config, never setAttribute, to set them. The actions that read these settings are described on JSTL i18n & Messages and JSTL Formatting.
| Context parameter and variable name | Config constant |
Type | Used by |
|---|---|---|---|
|
|
|
Locale of the i18n and formatting actions, replacing the browser’s |
|
|
|
Used when none of the preferred locales matches an available bundle |
|
|
|
Default resource bundle of |
|
|
|
Time zone of |
|
|
|
Data source of |
|
|
|
Upper bound of rows returned by |
Source: Jakarta Tags 3.0 API — Config
A further variable, jakarta.servlet.jsp.jstl.fmt.request.charset, is not a setting you configure: fmt:requestEncoding
stores the request encoding it detected in the session scope under that name. The Config class offers get, set and
remove for each scope (PageContext, ServletRequest, HttpSession, ServletContext) and find, which a tag
handler uses: it looks in the page, request, session and application scopes in turn and finally in the context
parameter of the same name. Because get looks only at the variable in the given scope, a servlet that wants the
context parameter reads it with ServletContext.getInitParameter(Config.FMT_TIME_ZONE), as the example does. The sql actions, and why a data
source belongs outside the view, are explained on the JSTL SQL page of this section.
The Bookshelf sets the French locale, the Madrid time zone and a bundle for the whole application, then overrides each at the narrowest scope that needs it.
<context-param>
<param-name>jakarta.servlet.jsp.jstl.fmt.locale</param-name>
<param-value>fr</param-value>
</context-param>
<context-param>
<param-name>jakarta.servlet.jsp.jstl.fmt.timeZone</param-name>
<param-value>Europe/Madrid</param-value>
</context-param>
<context-param>
<param-name>jakarta.servlet.jsp.jstl.fmt.localizationContext</param-name>
<param-value>bookshelf.messages</param-value>
</context-param>
bookshelf/messages.properties has greeting=Bonjour, messages_en.properties has greeting=Hello<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<p><fmt:message key="greeting"/> | <fmt:formatNumber value="1234.5"/> | <fmt:formatDate value="${now}" pattern="z"/></p>
<p>locale-override: <fmt:setLocale value="en_US" scope="page"/><fmt:message key="greeting"/> | <fmt:formatNumber value="1234.5"/></p>
fmt:setLocale wins afterwards)<p>Bonjour | 1 234,5 | CEST</p>
<p>locale-override: Hello | 1,234.5</p>
The same thing from Java, for a servlet that decides the zone per request. The servlet stores a configuration variable in the
request scope with Config.set; the JSP’s fmt:formatDate picks it up with no change.
@WebServlet("/tz")
public class TzServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String appZone = getServletContext().getInitParameter(Config.FMT_TIME_ZONE); // the context-param
if ("tokyo".equals(req.getParameter("zone"))) {
Config.set(req, Config.FMT_TIME_ZONE, TimeZone.getTimeZone("Asia/Tokyo"));
}
req.setAttribute("appZone", appZone);
req.setAttribute("moment", java.util.Date.from(java.time.Instant.parse("2026-10-08T12:00:00Z")));
req.getRequestDispatcher("/WEB-INF/views/tz.jsp").forward(req, resp);
}
}
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
context-param: ${initParam['jakarta.servlet.jsp.jstl.fmt.timeZone']}
context-param via Java: ${appZone}
12:00 UTC is <fmt:formatDate value="${moment}" type="time" timeStyle="short"/> (<fmt:formatDate value="${moment}" pattern="zzz"/>)
curl http://localhost:18080/g3config3/tz and curl "http://localhost:18080/g3config3/tz?zone=tokyo" (only the last line differs)context-param: Europe/Madrid
context-param via Java: Europe/Madrid
12:00 UTC is 14:00 (CEST)
context-param: Europe/Madrid
context-param via Java: Europe/Madrid
12:00 UTC is 21:00 (JST)
Source: Jakarta Tags 3.0 API — Config
The sql.dataSource and sql.maxRows context parameters follow the same pattern, sql.maxRows taking an integer. They were
declared and deployed successfully in the test application, but no database was used, so their behaviour is only described,
not demonstrated. A data source defined in web.xml with a JDBC parameter string puts the database URL, user and
password in the descriptor; use a JNDI data source from the container instead, and never commit a password to the
file (the placeholder rule of this section applies to every descriptor shown).
A Recommended Baseline for the Bookshelf
Putting the settings together, this is the group a new project should start from. Everything in it is declared in one place, so that a page behaves the same on any container and a new developer reads the rules from the descriptor.
<jsp-config>
<jsp-property-group>
<display-name>Bookshelf pages</display-name>
<url-pattern>/WEB-INF/views/*</url-pattern>
<page-encoding>UTF-8</page-encoding>
<scripting-invalid>true</scripting-invalid>
<error-on-el-not-found>true</error-on-el-not-found>
<error-on-undeclared-namespace>true</error-on-undeclared-namespace>
<trim-directive-whitespaces>true</trim-directive-whitespaces>
<default-content-type>text/html;charset=UTF-8</default-content-type>
<include-prelude>/WEB-INF/jspf/taglibs.jspf</include-prelude>
</jsp-property-group>
</jsp-config>
The baseline puts the views under WEB-INF, so they cannot be requested directly, and the group pattern /WEB-INF/views/*
(a valid path prefix) selects them. A taglibs.jspf prelude with the taglib directives removes the first lines of
every page. That baseline was not run as a single descriptor; each of its elements was exercised individually in the groups above.
Development-time engine parameters (recompilation checks, generated-source retention) and precompilation are not
jsp-config settings; they are init-param values of the JSP servlet and a build step, covered on the Jasper
configuration page of this section.
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
Every URL of the application answers 404 after a deployment that logged |
A wildcard in the middle of a path in a |
Use |
|
|
Replace the scriptlet with EL and tags, or move the page to a group that allows it |
|
|
Fix the name; guard optional data with |
|
A literal |
Write |
|
Two different encodings declared for one page |
Declare the encoding once, in the group |
|
An |
Fix the path; it is context-relative and starts with |
|
A group, usually an old one, has |
Change the group, or add |
|
A missing |
|
A property group seems to have no effect |
A more specific group sets that property differently (Tomcat decides per property), or the page directive overrides it |
Print the parsed configuration with |
A file with a non-JSP extension is compiled, or executed as a JSP (observed with |
A group’s |
Keep patterns narrow, and never use a bare |
A servlet mapped with a longer path than |
The more specific servlet mapping beats the extension mapping |
Do not map servlets over directories that contain JSPs, or add a group with the same pattern (the group wins on equal patterns) |
|
An extension pattern in the fragment’s group matches the application’s files |
Use path patterns in a subdirectory named after the fragment |
|
A JSP 1.2-era descriptor |
Move it inside |
Tag-file behaviour does not follow a group |
Groups never apply to tag files |
Set the behaviour with the |
What Changed Recently
| Change | Detail |
|---|---|
|
The |
Deferred syntax (JSP 2.1) |
|
Later 2.x additions |
|
Servlet 3.0 |
The container exposes |
Pages 3.1 (Jakarta EE 10) |
|
Pages 4.0 (Jakarta EE 11) |
Java 17 minimum; |
Pages 4.1 |
4.1.0-M2 is a milestone on Maven Central; it is not required for anything on this page |
Tags 3.0 |
The six configuration settings are named |
Where the Books Are Outdated
The JSP and JSTL books consulted for this section (listed in the section bibliography) describe the configuration of the JSP 1.2 and JSTL 1.0 era. The concepts they teach (a tag-library URI mapped to a TLD, application-wide defaults for the tag library, the wish to keep scripting out of pages) survived; the mechanisms did not.
| The books' era | Today (Pages 4.0, Servlet 6.1) |
|---|---|
|
Inside |
A Servlet 2.3 |
The |
|
|
JSTL 1.0 context parameters named |
The same six settings under |
The |
One library per area with EL in attributes; the twins are no longer part of the specification |
Scripting kept out of pages by convention or by the |
|
Page encoding, content type and buffer repeated in every page |
|
A header and footer pulled in by a copied |
|
No way to say that a directory of pages is XML or has no EL |
|
The container’s own configuration file ( |
|
References
-
Jakarta Pages 4.0 — 3.1 JSP Configuration Information in web.xml
-
Jakarta Pages 4.0 — 3.3.9 Removing Whitespaces from Template Text
-
Jakarta Pages 4.0 — 3.3.12 Raising Errors for Undeclared Namespaces
-
Jakarta Pages 4.0 — 3.4 Backwards Compatibility with JSP 2.0
-
Jakarta Servlet 6.1 — 8.2.3 Assembling the Descriptor from web.xml, web-fragment.xml and Annotations
-
Apache Tomcat 11 — Jasper 2 JSP Engine How To, Configuration