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.

A servlet that prints the JSP configuration the container read from web.xml
@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());
        }
    }
}
Output of 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=[]

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 taglib elements in web.xml, the subject of this section

3

Implicit entries deduced from the <uri> of every .tld file under WEB-INF and inside the JARs of WEB-INF/lib (TLDs in JARs under META-INF)

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.

The taglib map entry in web.xml
<jsp-config>
  <taglib>
    <taglib-uri>urn:bookshelf:text</taglib-uri>
    <taglib-location>/WEB-INF/tlds/bookshelf.tld</taglib-location>
  </taglib>
</jsp-config>
WEB-INF/tlds/bookshelf.tld, which declares no uri element of its own
<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>
books.jsp uses the alias, and the result
<%@ 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:

A taglib URI that resolves nowhere
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-pattern is a JSP file, whatever its extension. That is how a file such as cover.svg or page.htm is 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 include directive. The exceptions are page-encoding and is-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 every jsp-property-group; its settings come from the tag directive in the file.

  • Page-level settings override groups. Where the same property exists as an attribute of the page directive, the attribute in the page wins, except for the conflicts that are translation errors (an encoding named twice with different values, shown below).

Table 1. The twelve properties of a jsp-property-group (Jakarta Pages 4.0, section 3.3; jsp_4_0.xsd)
Element Default Page-directive override What it does

el-ignored

false for a Servlet 2.4 or later web.xml (true for 2.3 or earlier)

isELIgnored

Pass ${...} and #{...} through verbatim instead of evaluating them

error-on-el-not-found

false

errorOnELNotFound

Throw PropertyNotFoundException for an unknown EL identifier instead of returning null (new in Pages 3.1)

page-encoding

none (ISO-8859-1 for the page itself if nothing else says otherwise)

pageEncoding

Character encoding of the JSP source file

scripting-invalid

false

none (the group is the only switch)

Make scriptlets, declarations and expressions a translation error

is-xml

false

none (.jspx and .tagx are XML by convention)

Treat the files as JSP documents in XML syntax

include-prelude

none, repeatable

none

A file included at the start of every page of the group

include-coda

none, repeatable

none

A file included at the end of every page of the group

deferred-syntax-allowed-as-literal

false

deferredSyntaxAllowedAsLiteral

Allow the characters #{ as plain text in template text

trim-directive-whitespaces

false

trimDirectiveWhitespaces

Remove template text that contains only whitespace

default-content-type

none

contentType

Content type of the response when the page does not state one

buffer

container default (8 KB in Tomcat)

buffer

Buffering of the out writer: none or a size such as 32kb

error-on-undeclared-namespace

false

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

*.jsp, *.jspf, *.svg

Every file with that extension, anywhere in the application

Path prefix

/admin/*, /WEB-INF/views/*

Every file below that directory, at any depth

Exact

/admin/report.jsp

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-mapping pattern 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-prelude and include-coda of every matching group applies, in the order the groups appear in web.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.

flowchart TD url["Request for /legacy/old.jsp"] --> sm{"servlet-mapping or
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"]
Which pattern wins: the extra descriptor of the second Bookshelf WAR
<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>
Table 2. Results on Tomcat 11.0.26 (every file contains one EL expression, a scriptlet, or both)
Request Matching patterns Observed

/exact/a.jsp

*.jsp, /exact/*, /exact/a.jsp

<p>3</p>: the exact pattern wins, so el-ignored is false and EL is evaluated

/exact/b.jsp

*.jsp, /exact/*

<p>${1+2}</p>: the path prefix wins, EL is ignored; the prelude of *.jsp is still included

/scripts/ok.jsp

*.jsp, /scripts/*

A scriptlet runs and prints 3: the prefix group sets scripting-invalid to false against the extension group’s true. The *.jsp prelude and content type still apply (merge)

/top.jsp

*.jsp

HTTP 500: Scripting elements ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) are disallowed here.

/pick/x.jsp

/pick/* as a group and as a servlet-mapping (identical)

The JSP is compiled and served (<p>3 via jsp-property-group</p>): the group wins over the servlet mapping

/other/x.jsp

*.jsp as the JSP servlet mapping, /other/* as a servlet mapping

servlet plain for /g3config2/other/x.jsp: the more specific servlet pattern beats the extension mapping, so the file is not compiled

/page.htm

*.htm

The .htm file is compiled as a JSP and answers <p>3 .htm</p> with text/html;charset=UTF-8: a group makes any file a JSP, and the prelude of *.jsp is not added because that pattern does not match

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:

Table 3. EL evaluation for JSP pages (Pages 4.0, Table JSP.3-1)
Group el-ignored page directive isELIgnored Result

unspecified

unspecified

Ignored if the web.xml is 2.3 or earlier, evaluated otherwise

false

unspecified

Evaluated

true

unspecified

Ignored

any

false

Evaluated

any

true

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.

A directory of legacy pages whose ${...} text must not be evaluated
<jsp-property-group>
  <url-pattern>/legacy/*</url-pattern>
  <el-ignored>true</el-ignored>
</jsp-property-group>
legacy/old.jsp and legacy/old-on.jsp (the second one overrides the group in the page)
<p>Total: ${1 + 2}</p>
<%@ page isELIgnored="false" %>
<p>Total: ${1 + 2}</p>
Rendered output (the comments come from the include-prelude and include-coda of the *.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.

The admin area has no scripting and two more includes
<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>
admin/bad.jsp contains a scriptlet expression, admin/report.jsp does not
<p>count: <%= 1 + 2 %></p>
Observed: HTTP 500 with this message for bad.jsp
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.

Table 4. Unknown-identifier handling for JSP pages (Pages 4.0, Table JSP.3-4)
Group error-on-el-not-found page directive errorOnELNotFound Unknown identifier

unspecified or false

unspecified

null is returned by the resolver

true

unspecified

PropertyNotFoundException

any

false

null

any

true

PropertyNotFoundException

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).

The strict area of the Bookshelf
<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>
strict/missing.jsp, and strict/missing-off.jsp that opts out in the page
<p>Owner: ${owner}</p>
<%@ page errorOnELNotFound="false" %>
<p>Owner: [${owner}]</p>
Observed: missing.jsp answers HTTP 500, caused by this exception; missing-off.jsp answers 200 with <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.

One place for the source encoding
<jsp-property-group>
  <url-pattern>*.jsp</url-pattern>
  <page-encoding>UTF-8</page-encoding>
</jsp-property-group>
enc.jsp disagrees with the group
<%@ page pageEncoding="ISO-8859-1" %>
x
Observed: HTTP 500 with this message
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.

Two groups that both match /admin/report.jsp, in this order
<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>
admin/report.jsp contains one line, and the response is
<!-- 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:

Observed for a group whose include-prelude names a missing file
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.

A cover image rendered as a JSP document, enabled by the group
<jsp-property-group>
  <url-pattern>*.svg</url-pattern>
  <is-xml>true</is-xml>
</jsp-property-group>
cover.svg, a JSP document with no jsp:root
<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>
Response (Tomcat 11.0.26 answered with the content type 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.

A directory that may contain #{...} as text
<jsp-property-group>
  <url-pattern>/literal/*</url-pattern>
  <deferred-syntax-allowed-as-literal>true</deferred-syntax-allowed-as-literal>
</jsp-property-group>
The same one-line page in literal/hash.jsp and in literal-off.jsp (outside the group)
<p>Placeholder: #{title}</p>
Observed: hash.jsp is served unchanged (200); literal-off.jsp is HTTP 500
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:

A page outside any literal group
<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.

Two 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}
Observed with 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.

A directory with a larger buffer (the page prints its own buffer size)
<jsp-property-group>
  <url-pattern>/buf/*</url-pattern>
  <buffer>32kb</buffer>
</jsp-property-group>
buffer=${pageContext.out.bufferSize}
Observed: buf/size.jsp prints 32768, size-default.jsp outside the group prints 8192
buffer=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.

strict/ns.jsp uses the prefix z without declaring it
<p>x</p>
<z:greeting/>
Observed: HTTP 500 with this message
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.

Table 5. The six JSTL configuration settings (Jakarta Tags 3.0, sections 8.11, 9.11 and 10.9)
Context parameter and variable name Config constant Type Used by

jakarta.servlet.jsp.jstl.fmt.locale

FMT_LOCALE

String or Locale

Locale of the i18n and formatting actions, replacing the browser’s Accept-Language; set by fmt:setLocale

jakarta.servlet.jsp.jstl.fmt.fallbackLocale

FMT_FALLBACK_LOCALE

String or Locale

Used when none of the preferred locales matches an available bundle

jakarta.servlet.jsp.jstl.fmt.localizationContext

FMT_LOCALIZATION_CONTEXT

String (a bundle base name) or LocalizationContext

Default resource bundle of fmt:message and of the formatting actions; set by fmt:setBundle

jakarta.servlet.jsp.jstl.fmt.timeZone

FMT_TIME_ZONE

String or TimeZone

Time zone of fmt:formatDate and fmt:parseDate; set by fmt:setTimeZone

jakarta.servlet.jsp.jstl.sql.dataSource

SQL_DATA_SOURCE

String (JNDI path or JDBC parameter string) or DataSource

Data source of sql:query, sql:update, sql:transaction

jakarta.servlet.jsp.jstl.sql.maxRows

SQL_MAX_ROWS

Integer

Upper bound of rows returned by sql:query

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.

Application-wide JSTL settings in web.xml
<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>
books.jsp: the bundle 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>
Observed output (the French locale from the context parameter; the narrower page-scope 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.

TzServlet sets a request-scoped time zone with Config
@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);
    }
}
WEB-INF/views/tz.jsp
<%@ 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)

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).

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.

The baseline: UTF-8 everywhere, no scripting, strict EL, tidy output
<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

Table 6. Configuration pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom or error Cause Fix

Every URL of the application answers 404 after a deployment that logged Invalid <url-pattern> [/buf/*.jsp] in servlet mapping (observed)

A wildcard in the middle of a path in a url-pattern

Use /buf/* or *.jsp; check the log of the failed deployment

JasperException: ... Scripting elements ( <%!, ... ) are disallowed here. (observed)

scripting-invalid is true for the page’s group

Replace the scriptlet with EL and tags, or move the page to a group that allows it

jakarta.el.PropertyNotFoundException: Unknown identifier [owner] (observed)

error-on-el-not-found is on and the name is a typo or the attribute is missing

Fix the name; guard optional data with c:if, or set errorOnELNotFound="false" in one page

#{...} is not allowed in template text (observed)

A literal #{ in a page

Write \#{ (observed to print #{), or set deferred-syntax-allowed-as-literal for that group

Page-encoding specified in jsp-property-group [UTF-8] is different from that specified in page directive [ISO-8859-1] (observed)

Two different encodings declared for one page

Declare the encoding once, in the group

JSP file [/WEB-INF/jspf/nothing.jspf] not found for every page of a group (observed)

An include-prelude or include-coda that names a file that does not exist

Fix the path; it is context-relative and starts with /

${...} is printed as text in a page that should evaluate it (observed)

A group, usually an old one, has el-ignored true; or the web.xml still declares Servlet 2.3

Change the group, or add isELIgnored="false", and move the descriptor to version="6.1"

<z:tag/> appears in the page source of the response

A missing taglib directive; the default is to ignore unknown prefixes

error-on-undeclared-namespace set to true

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 getJspConfigDescriptor() and compare the patterns

A file with a non-JSP extension is compiled, or executed as a JSP (observed with *.htm)

A group’s url-pattern makes every match a JSP file

Keep patterns narrow, and never use a bare *

A servlet mapped with a longer path than *.jsp captures a JSP file (observed with /other/*)

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)

<jsp-config> of a library’s web-fragment.xml changes the application’s own pages

An extension pattern in the fragment’s group matches the application’s files

Use path patterns in a subdirectory named after the fragment

<taglib> directly under <web-app> is ignored or rejected

A JSP 1.2-era descriptor

Move it inside <jsp-config>, or delete it and rely on the TLD’s <uri>

Tag-file behaviour does not follow a group

Groups never apply to tag files

Set the behaviour with the tag directive of the file

What Changed Recently

Table 7. Changes to JSP configuration since the Servlet 2.3 era
Change Detail

jsp-config and jsp-property-group (JSP 2.0, Servlet 2.4)

The jsp-config element moved the taglib map inside it and introduced groups, with el-ignored, scripting-invalid, page-encoding, is-xml and the implicit includes as the first properties; el-ignored defaults to false for a 2.4 or later descriptor

Deferred syntax (JSP 2.1)

#{ is reserved for EL; deferred-syntax-allowed-as-literal was added for the old text

Later 2.x additions

trim-directive-whitespaces, default-content-type, buffer and error-on-undeclared-namespace were added after 2.1; the Pages 4.0 change log covers only versions from 3.0, so their exact release is not restated here

Servlet 3.0

The container exposes jsp-config through getJspConfigDescriptor() and web fragments can add groups (additive merge)

Pages 3.1 (Jakarta EE 10)

error-on-el-not-found and the errorOnELNotFound directive attributes; isThreadSafe deprecated

Pages 4.0 (Jakarta EE 11)

Java 17 minimum; jsp:plugin and the related actions, isThreadSafe and the deprecated jakarta.servlet.jsp.el classes removed; ErrorData gains the query string and method. No change to jsp-config elements

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 jakarta.servlet.jsp.jstl.* (the Config constants carry the same values); the javax.servlet.jsp.jstl.* names belong to JSTL 1.x

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.

Table 8. JSP 1.2-era configuration and its equivalent today
The books' era Today (Pages 4.0, Servlet 6.1)

<taglib> with taglib-uri and taglib-location directly under <web-app>, required for every library

Inside <jsp-config>; usually unnecessary, because TLDs with a <uri> are discovered automatically in WEB-INF and in JARs, and the JSTL libraries are built into the platform

A Servlet 2.3 web.xml with a <!DOCTYPE web-app PUBLIC ...> DTD, which turns EL off by default

The web-app_6_1.xsd schema, version="6.1", EL on by default

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> with the core, fmt, sql, x and fn URIs of JSTL 1.0 and 1.1

jakarta.tags.core and the other jakarta.tags.* URIs; the old java.sun.com addresses are only identifiers and the javax.servlet.jsp.jstl.* classes no longer exist

JSTL 1.0 context parameters named javax.servlet.jsp.jstl.fmt.locale and the rest

The same six settings under jakarta.servlet.jsp.jstl.*; Config is in jakarta.servlet.jsp.jstl.core

The _rt twin libraries (c_rt, fmt_rt) to accept Java expressions in attributes

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 ScriptFreeTLV validator

scripting-invalid in a jsp-property-group

Page encoding, content type and buffer repeated in every page

page-encoding, default-content-type, buffer

A header and footer pulled in by a copied jsp:include or include directive in each page

include-prelude and include-coda

No way to say that a directory of pages is XML or has no EL

is-xml, el-ignored

The container’s own configuration file (server.xml, jsp.xml) for JSP engine switches

jsp-config for what the specification standardises, init-param values of the JSP servlet for engine tuning

References