Syntax, Comments & Escaping
|
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 (Jakarta EE 11), Jakarta Expression Language 6.0, Jakarta Standard Tag
Library 3.0 and Jakarta Servlet 6.1, running on Apache Tomcat 11.0.26 and Java 21. Everything was verified on
2026-10-08: the facts against sections 1.3, 1.5, 1.6, 3.3, 5.12, 6.2 and 10.1.3 of the Pages 4.0 specification and its
change log, and every example by building it into a Maven WAR (context path /g3syntax) and requesting it with curl
on Tomcat 11.0.26. The outputs shown are the outputs that run produced; lines that end with a dollar sign come from
curl | cat -e, where the sign marks the end of the line. A JSP file is a mixture of two things, text that the
container copies to the response untouched and elements that it executes, and almost every syntax question comes down
to telling the translator which is which. The running scenario is the Bookshelf: a servlet context listener puts a
list of three books (Dune, Emma, Ulysses) and a shop name into application scope, and each example is a small page that
displays them.
Elements and Template Text
The specification defines a page as a sequence of elements and template data (usually called template text). An element is an instance of an element type that the container knows; template text is everything else, "anything that the JSP translator does not know about". The translator copies template text to the response, in order, and replaces each element with whatever that element produces. Nothing in template text is escaped, interpreted or validated. It can be HTML, XML, CSV, JSON or a plain-text mail, which is why the same technology serves all of them.
There are three element types, and each has its own syntax.
| Element type | Syntax | When it acts | Example |
|---|---|---|---|
Directive |
|
Translation time; applies to the whole page, produces no output |
|
Action (standard or custom) |
|
Request time; the standard ones use the prefix |
|
Scripting element |
|
Request time (a declaration at class level); Java code glued around the template text |
|
Expression Language |
|
Request time; allowed in template text and in attribute values that accept expressions |
|
The first example is the smallest useful Bookshelf page. It contains one directive (page), one directive (taglib), one
standard-library action (c:forEach), EL expressions in template text, one scripting expression and a lot of plain HTML
template text.
elements.jsp: template text mixed with a directive, an action, EL and a scripting expression<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<h1>Bookshelf</h1>
<ul>
<c:forEach var="b" items="${books}">
<li>${b.title} (${b.year})</li>
</c:forEach>
</ul>
<p>Generated at <%= 1 + 1 %> o'clock</p>
A request for /g3syntax/elements.jsp returned HTTP 200 with the body below.
elements.jsp$
$
<h1>Bookshelf</h1>$
<ul>$
$
<li>Dune (1965)</li>$
$
<li>Emma (1815)</li>$
$
<li>Ulysses (1922)</li>$
$
</ul>$
<p>Generated at 2 o'clock</p>$
Three things are visible in that output. The two directives produced no text of their own, but the line breaks after
them are template text and were copied (the first two empty lines). The c:forEach body ran three times, and every
iteration copied the line break before and after <li> as well, which explains the blank lines between items.
Finally ${b.title} and <%= 1 + 1 %> were replaced by their values. The whitespace section below shows how to
control those blank lines.
The translator decides what it is looking at by the first characters of each construct and applies the first rule that matches (the specification calls this the greedy algorithm and gives the full EBNF in section 1.3.10). The rules, in the order of the grammar, are summarised in the figure.
The figure shows the decision the translator makes for every position in the file, and the last row is the important one:
an HTML comment, an unknown tag such as <br>, a <script> block or a JSON document is simply template text. Two
consequences follow. The container never validates your HTML, so a page can be malformed HTML or even not XML at all
(<br> without a closing slash works), and a tag whose prefix was never declared is not reported either. With a
taglib directive for c, a misspelled <c:foreach> is an error, but <cc:forEach var="b" items="${books}"> with an
undeclared prefix is template text that goes to the browser, and the ${books} inside its attribute is still evaluated
(the request for prefix.jsp returned the whole list printed inside the attribute). The pitfalls table repeats the point.
br.jsp: not well-formed XML is fine in standard syntax<%@ page contentType="text/html; charset=UTF-8" %>
<p>Not well-formed XML<br>but fine as a JSP page ${books[1].title}
The response was <p>Not well-formed XML<br>but fine as a JSP page Emma, with the entity and the unclosed
paragraph untouched. A JSP document, covered below, is the opposite case: the file is parsed as XML first and must be
well-formed.
Scoped Variables in Template Text
An EL expression in template text, such as ${shopName}, names a scoped variable: an attribute stored in one of the four
scopes of a page, page, request, session and application. A bare name is looked up in that order, and the first scope
that holds it wins; a prefixed name (${requestScope.label}, ${applicationScope.shopName}) reads one scope only. The
page below stores a variable called label in all four scopes with c:set, prints the bare name, prints the four prefixed
forms, removes the page-scope copy with c:remove and prints the bare name again. The full rules for scopes, the session
attribute of the page directive and the lifetime of each scope are on
Implicit Objects & Scopes.
scopes.jsp: one name in four scopes<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="label" value="from page scope" scope="page"/>
<c:set var="label" value="from request scope" scope="request"/>
<c:set var="label" value="from session scope" scope="session"/>
<c:set var="label" value="from application scope" scope="application"/>
<p>${label}</p>
<p>${pageScope.label} | ${requestScope.label} | ${sessionScope.label} | ${applicationScope.label}</p>
<c:remove var="label" scope="page"/>
<p>${label}</p>
<p>${shopName}</p>
scopes.jsp (blank lines left by the directive and the c:set actions are omitted)<p>from page scope</p>
<p>from page scope | from request scope | from session scope | from application scope</p>
<p>from request scope</p>
<p>Bookshelf "Corner" & Co.</p>
The bare name found the page-scope copy first, and found the request-scope copy once the page-scope copy was gone (observed on
Tomcat 11.0.26). The last line shows the shop name stored by the context listener in application scope, found by the bare name
because no narrower scope holds shopName. Because a bare name can be shadowed by a narrower scope, a variable that matters
(a user, a permission, a flag) is better read with its scope prefix.
Start Tags, End Tags, Names and Attribute Values
The rest of section 1.3 is a short list of rules inherited from XML. They explain most of the translation errors a beginner meets.
| Rule | Meaning | Error when broken |
|---|---|---|
Start and end in the same file |
An element with a start tag and an end tag, and a scripting element such as |
|
Empty elements |
|
none, but the body is executed when it is not empty |
Case-sensitive names |
Tag names and attribute names are case-sensitive as in XML: |
|
Quoted attribute values |
Attribute values are always quoted, with single or double quotes, and the same quoting rules apply to literals and to request-time expressions |
a translation error naming the attribute value |
One value per attribute |
An attribute may be given either as an XML attribute or with |
|
Names |
Action names must be XML names (NMTOKEN), attribute names follow the JavaBeans conventions, and names that start with |
depends on the name |
The jsp:attribute and jsp:body actions deserve a short example, because they are the one place where a page
passes markup as an attribute value. Both pages below were written to fail, so that the message is recorded. The first
sets value twice, the second spells the tag name with a capital letter.
err-attrdup.jsp and err-case.jsp: two translation errors<%@ page contentType="text/plain" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:out value="a">
<jsp:attribute name="value">b</jsp:attribute>
</c:out>
<%-- err-case.jsp, line 3 --%>
<c:Out value="x"/>
Tomcat answered HTTP 500 with a JasperException. The message of the first was /err-attrdup.jsp (line: [3], column:
[0]) The attribute [value] specified in the standard or custom action also appears as the value of the name attribute
in the enclosed jsp:attribute; the message of the second was /err-case.jsp (line: [3], column: [0]) No tag [Out]
defined in tag library imported with prefix [c]. The line and column point at the start tag of the element, and the
message is produced at translation time, so no output of the page is sent when it appears.
A scripting element must be closed in the file that opened it. The page below includes a fragment that opens a
scriptlet and never closes it, hoping that the including page supplies the %>.
err-split-file.jsp and the fragment WEB-INF/fragments/open.jspf<%@ page contentType="text/plain" %>
<%@ include file="/WEB-INF/fragments/open.jspf" %> %>
<%-- WEB-INF/fragments/open.jspf contains: --%>
<% if (true) {
The message was /err-split-file.jsp (line: [2], column: [0]) /WEB-INF/fragments/open.jspf (line: [1], column: [3])
Unterminated [<%] tag: Jasper names the including page and the fragment, with the position inside the fragment. The same
message, with the page itself in place of the fragment, appears for a file that ends in the middle of a scriptlet
(err-unclosed.jsp).
Standard Syntax and XML Syntax
Every element of the table above has a second spelling in XML syntax. The XML spellings exist so that a page can be
well-formed XML (a JSP document, normally in a .jspx file) and can therefore be processed by XML tools. They are also
legal inside an ordinary .jsp file in standard syntax, so a page can mix the two, although mixing is rarely a good idea.
| Element | Standard syntax | XML syntax |
|---|---|---|
Page directive |
|
|
Include directive |
|
|
Taglib directive |
|
no directive: an |
Declaration |
|
|
Scriptlet |
|
|
Expression |
|
|
Request-time attribute value |
|
|
Verbatim template text |
ordinary text |
|
EL expression |
|
|
JSP comment |
|
an XML comment, which is dropped |
The next page uses only the XML spellings in a .jsp file. It declares a counter, increments it, prints it and then
writes template text with jsp:text, where the entity < is the XML way to write a less-than sign.
xml-syntax.jsp: XML forms of the scripting elements and jsp:text<jsp:directive.page contentType="text/plain; charset=UTF-8"/>
<jsp:declaration>private int hits = 0;</jsp:declaration>
<jsp:scriptlet>hits++;</jsp:scriptlet>
Hits: <jsp:expression>hits</jsp:expression>
<jsp:text>A < B and ${books[0].title}</jsp:text>
The response was three empty lines (the line breaks after the directive, the declaration and the scriptlet) followed by
Hits: 1 and A < B and Dune. The entity was not decoded in the output: jsp:text passes its body through as template data,
and in a .jsp file in standard syntax the body is not run through an XML parser. The same jsp:text line in a JSP
document (text.jspx, the same three directive and text lines inside a root element) returned A < B and Dune, because
the XML parser decodes the entity first. This is one of the differences between the two worlds, and the
JSP Documents (XML Syntax) treats the XML side fully. The next example is a JSP document,
catalogue.jspx, which produces XML.
catalogue.jspx: a JSP document that generates XML<?xml version="1.0" encoding="UTF-8"?>
<catalogue xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="jakarta.tags.core" version="2.0">
<jsp:directive.page contentType="application/xml; charset=UTF-8"/>
<!-- XML comment: dropped from the output -->
<c:forEach var="b" items="${books}">
<book year="${b.year}">${b.title}</book>
</c:forEach>
<jsp:text><![CDATA[<raw & text>]]></jsp:text>
<jsp:text>\${literal} ${'${'}x}</jsp:text>
</catalogue>
The response was the following. The xmlns:jsp and xmlns:c declarations are consumed by the container and removed;
the whitespace-only text nodes between the elements are dropped, so the output has no blank lines; the XML comment is gone;
and the CDATA content is emitted verbatim, which means that <raw & text> is not well-formed XML in the response. The
last line shows both ways to produce a literal EL delimiter.
catalogue.jspx<?xml version="1.0" encoding="UTF-8"?>$
<catalogue version="2.0"><book year="1965">Dune</book><book year="1815">Emma</book><book year="1922">Ulysses</book><raw & text>${literal} ${x}</catalogue>
The XML spelling of a request-time attribute value, value="%= hits %", applies to custom actions. Tomcat 11.0.26 also
rewrites it on a template element such as <a href="%= 1 %">, producing href=" 1 ", although section 6.2.2 of the
specification says that on such an element it "does not represent a request-time attribute value" and should stay as
written. Do not rely on either behaviour; write ${...} in attribute values of template elements, which means the same
in both syntaxes.
A JSP document must be well-formed, so the standard-syntax scriptlet markers are a parse error there. The page
err-doc-scriptlet.jspx is a document with a jsp:directive.page line followed by <% int x = 1; %>; Tomcat reported
/err-doc-scriptlet.jspx (line: [4], column: [4]) The content of elements must consist of well-formed character data or
markup. The XML parser, not the JSP translator, raises that error.
Comments
There are two purposes for comments in a JSP page: documenting the page itself, so that the text must not reach the browser, and producing a comment in the generated document, so that it must. The specification gives each its own syntax, and the scripting language adds a third. Choosing the wrong one is the commonest comment bug, and the wrong choice usually leaks something.
| Kind | Syntax | In the response? | What is evaluated inside |
|---|---|---|---|
JSP comment |
|
No; also absent from the XML view |
Nothing: the body is ignored completely, so elements, EL and scriptlets in it never run |
HTML (or XML) comment in standard syntax |
|
Yes, as template text |
Everything: actions, scriptlets, |
Java comment inside a scriptlet |
|
No |
Nothing, but it is Java source, so the scriptlet around it runs |
XML comment in a JSP document |
|
No: "the body of the content is ignored completely" |
Nothing |
Source: Jakarta Pages 4.0 — 1.5 Comments
All three comment forms of a JSP page appear in the page below, together with a block that is commented out. The shop
name in application scope is Bookshelf "Corner" & Co. on purpose, to show what happens to markup characters.
comments.jsp: HTML comment, JSP comment, Java comments and a commented-out block<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!-- HTML comment, shop: ${shopName}, sum: <%= 20 + 22 %> -->
<%-- JSP comment: ${shopName} <%= 20 + 22 %> <c:out value="never run"/> --%>
<% /* Java comment, invisible in the output */ %>
<% // Java line comment %>
<%-- Commented-out block:
<c:forEach var="b" items="${books}">
<li>${b.title}</li>
</c:forEach>
--%>
<p>${books[0].title}</p>
comments.jsp$
$
<!-- HTML comment, shop: Bookshelf "Corner" & Co., sum: 42 -->$
$
$
$
$
<p>Dune</p>$
Only the HTML comment survives, and it contains evaluated data: the EL expression and the scripting expression were
executed and the result is in the comment. Every visitor who selects "view source" sees it. The shop name is also
unescaped, because EL in template text is never HTML-escaped (the escaping section below returns to this). Two rules of
thumb follow.
-
Use
<%-- --%>for every comment about the page itself. An HTML comment is a published message. -
Never put request-dependent or confidential data in an HTML comment, whether through
${...}or a scriptlet. Besides disclosing it, a value that contains the two characters that end a comment (a double dash followed by a greater-than sign) closes the comment early, and whatever follows is parsed as markup. That is a cross-site scripting hole whenever the value is controlled by a user.
A JSP comment cannot be nested. The first closing marker ends the comment, and the rest is template text:
comments-nesting.jsp: comments do not nest<%@ page contentType="text/plain; charset=UTF-8" %>
A<%-- outer <%-- inner --%> B --%>C
The response was A B --%>C: the comment ended at the first --%>, so the text B --%> (with its leading space) and the C that follows were copied as
template text. For the same reason, a block that already contains a JSP comment cannot be commented out with another JSP comment:
remove the inner comment first, or delete the block and let version control keep it. A comment that is not closed at all is a
translation error, Unterminated [<%--] tag, reported at the opening marker (err-nested.jsp).
Quoting and Escape Conventions
JSP has no single escape character. Section 1.6 lists a separate set of rules for each context in which the same characters mean something special, and the rules are small but easy to confuse. The table collects them. "EL enabled" means that the page evaluates EL, which is the default for a web application whose descriptor is Servlet 2.4 or later.
| Context | To write this | Write this | Notes |
|---|---|---|---|
Template text |
a literal |
|
only the opener needs quoting |
Template text, EL enabled |
a literal |
|
not required for a |
Inside a scripting element |
a literal |
|
otherwise it ends the element |
Inside an EL expression |
a literal |
|
no escape character exists inside EL; use a string literal. |
Attribute value |
a |
|
required there; a |
Attribute value |
a |
|
and |
Attribute value |
a |
|
same as elsewhere |
Attribute value |
either quote |
|
the entities are also available |
JSP document |
anything |
XML rules ( |
JSP quoting is not applied after the XML parse |
The page below exercises the template-text and scripting rules, one numbered line each.
escaping.jsp: quoting in template text and in scripting elements<%@ page contentType="text/plain; charset=UTF-8" %>
1. ${books[0].title}
2. \${books[0].title}
3. \\${books[0].title}
4. \#{books[0].title}
5. ${'${'}books[0].title}
6. ${'}'}
7. <\% scriptlet %>
8. <%= "Joe said %\>" %>
9. <%= "Joe said \"hi\"" %>
10. Price: \$5 and $5 and #5
11. a backslash \ stays; so does \n
12. \\\${books[0].title}
13. \\\\${books[0].title}
escaping.jsp$
1. Dune$
2. ${books[0].title}$
3. \${books[0].title}$
4. #{books[0].title}$
5. ${books[0].title}$
6. }$
7. <% scriptlet %>$
8. Joe said %>$
9. Joe said "hi"$
10. Price: $5 and $5 and #5$
11. a backslash \ stays; so does \n$
12. \\${books[0].title}$
13. \\\${books[0].title}$
Reading the output line by line: line 1 is evaluated; line 2 shows that \$ produces a literal ${...} that is not
evaluated; line 4 does the same for the deferred syntax; line 5 is the EL string-literal trick and produces the text
${books[0].title}, as the specification’s example ${'${'} predicts; line 6 shows ${'}'}; line 7 shows that <\% is
the only way to write <% in template text; line 8 shows that %\> becomes %> inside a scriptlet string (without the
backslash the string would end the scriptlet); line 9 is an ordinary Java string escape, because inside a scripting
element the text is Java; line 10 shows that a lone $ or # is left alone; and line 11 shows that there is no
escape for \n, so a backslash in template text is simply a backslash. The specification notes that the quoting rules
"do not allow for quoting special characters such as \n", and that the way to produce one is a Java expression.
Lines 3, 12 and 13 reveal a behaviour that the specification does not describe for template text. On Tomcat 11.0.26 a
pair of backslashes before ${ in template text collapses to a single backslash and the expression is then not
evaluated (line 3 printed \${books[0].title}, not \Dune), and each additional pair behaves the same way. Inside an
attribute value the rule of the specification applies and \\${...} evaluates the expression (see attributes.jsp
below). To print a backslash followed by an evaluated value in template text, produce the backslash with EL,
${'\\'}${books[0].title}, or avoid it.
The #{ Sequence Is Reserved
Since JSP 2.1 the character sequence #{ is reserved for deferred EL expressions. In a JSP page a deferred expression
makes no sense in template text, because only immediate evaluation can write text, so a #{ in template text is a
translation error unless it is escaped as \#{ or the page opts in.
deferred-bare.jsp and deferred-literal.jsp: the reserved sequence and the opt-in<%-- deferred-bare.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" %>
Total: #{books[0].title}
<%-- deferred-literal.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" deferredSyntaxAllowedAsLiteral="true" %>
Total: #{books[0].title}
The first page returned HTTP 500 with /deferred-bare.jsp (line: [2], column: [9]) #{...} is not allowed in template text, and the second returned HTTP 200 with the line Total: #{books[0].title} unchanged. The same switch exists for
a group of pages as the deferred-syntax-allowed-as-literal element of jsp-property-group. Prefer \#{ for the few
places where the text is meant literally; the opt-in hides real mistakes.
Quoting in Attribute Values
An attribute value is delimited by either single or double quotes, and a quote of the delimiter kind must be escaped
with a backslash inside the value. Request-time scripting expressions inside a value are Java, so a Java string needs its
own quotes, and each of the two levels of quoting multiplies the backslashes. The page below uses c:out with
escapeXml="false" so that the characters reach the response exactly as the container decoded them.
attributes.jsp: quoting in attribute values<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
[<c:out value="plain 'single' inside double"/>]
[<c:out value='plain "double" inside single'/>]
[<c:out value="escaped \"double\" and \\ backslash" escapeXml="false"/>]
[<c:out value='escaped \'single\'' escapeXml="false"/>]
[<c:out value="entities "double" and 'single'" escapeXml="false"/>]
[<c:out value="dollar \${not.el} hash \#{not.el}" escapeXml="false"/>]
[<c:out value='<%= "Joe said \\\"hi\\\"" %>' escapeXml="false"/>]
[<c:out value="<%= \"Joe said 'hello'\" %>" escapeXml="false"/>]
[<c:out value="<%= \"Joe said \\\"hello\\\"\" %>" escapeXml="false"/>]
[<c:out value="end expression %\>" escapeXml="false"/>]
[<c:out value="start <\% literal" escapeXml="false"/>]
[<c:out value="${shopName}"/>]
[<c:out value="${shopName}" escapeXml="false"/>]
[<c:out value="\\${books[0].title}" escapeXml="false"/>]
[<c:out value="\\\\${books[0].title}" escapeXml="false"/>]
[<c:out value="${'${'}x}" escapeXml="false"/>]
Source: Jakarta Pages 4.0 — 1.6 Quoting and Escape Conventions (Quoting in Attributes and its examples)
attributes.jsp[plain 'single' inside double]$
[plain "double" inside single]$
[escaped "double" and \ backslash]$
[escaped 'single']$
[entities "double" and 'single']$
[dollar ${not.el} hash #{not.el}]$
[Joe said "hi"]$
[Joe said 'hello']$
[Joe said "hello"]$
[end expression %>]$
[start <% literal]$
[Bookshelf "Corner" & Co.]$
[Bookshelf "Corner" & Co.]$
[\Dune]$
[\\Dune]$
[${x}]$
The first two lines also show c:out doing its job: it replaces <, >, &, ' and " by entity codes ('
and " for the quotes), which is why the plain lines differ from the others. In the attribute value, the pair
\\ becomes one backslash and ${...} is then evaluated, so \\${...} prints a backslash and the value, and
\\\\${...} prints two (the last three lines), which is the rule in section 1.6 and the opposite of what template text
did on line 3 above. The ninth line shows the Java and attribute levels together: a Java \" inside a double-quoted
attribute is written \\\", because the attribute level consumes one backslash and the Java level the rest. The one
illegal form worth memorising is the specification’s own: a request-time expression whose Java string uses the
attribute’s own quote kind without escaping it.
err-attr.jsp: an unescaped double quote inside a double-quoted attribute<%@ page contentType="text/plain" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:out value="<%= "hi!" %>"/>
Source: Jakarta Pages 4.0 — 1.6 Quoting and Escape Conventions (examples of illegal attribute values)
Jasper stopped with /err-attr.jsp (line: [3], column: [17]) Attribute value [ "hi!" ] is quoted with ["] which must be
escaped when used within the value. The fix is to switch the delimiter, value='<%= "hi!" %>', or to avoid the
scripting expression altogether with value="hi!".
Switching EL Off: isELIgnored
Quoting is the answer for an occasional literal ${. When a whole page, or a whole set of legacy pages, contains dollar
signs that must pass through, evaluation can be turned off instead. The default depends on the version of the
deployment descriptor: a web application with a Servlet 2.4 or later web.xml evaluates ${...}; one with a Servlet 2.3
or earlier descriptor ignores EL "for backward compatibility". The default can be changed per group of pages with the
el-ignored element of jsp-property-group (see Configuration and Property Groups) and per page with the isELIgnored attribute of the page directive;
the page attribute wins unless it is unspecified.
el-ignored (configuration) |
isELIgnored (page directive) |
Result |
|---|---|---|
unspecified |
unspecified |
ignored for a Servlet 2.3 or earlier descriptor, evaluated otherwise |
|
unspecified |
evaluated |
|
unspecified |
ignored |
any |
|
evaluated |
any |
|
ignored |
el-ignored.jsp: a page that passes the EL syntax through<%@ page contentType="text/plain; charset=UTF-8" isELIgnored="true" %>
${books[0].title} and #{books[0].title} and \${x}
The response was ${books[0].title} and #{books[0].title} and \${x}$: with EL ignored, nothing is evaluated, the #{
sequence does not raise the translation error, and the backslash of \${x} is kept, because without EL there is
nothing for it to quote. The setting also works the other way, and that is the migration trap discussed in "Where the
Books Are Outdated". A second WAR with a Servlet 2.3 web.xml (a DOCTYPE of the 2.3 DTD) and a page that contains
Sum: ${1 + 2} returned Sum: ${1 + 2} on Tomcat 11.0.26, whereas pages of the /g3syntax WAR, whose descriptor is
Servlet 6.1, evaluate their expressions. Tag files do not take this default: they always evaluate expressions unless the tag
directive says otherwise.
Escaping Is Not Output Encoding
The backslash rules decide what the translator sees; they do nothing for what the browser sees. ${shopName} in
template text inserts the value as it is, so a name that contains <script> is delivered as markup. The two lines at the
end of the attributes.jsp output above show both options side by side: c:out (or fn:escapeXml) produced
Bookshelf "Corner" & Co., while the raw expression, which is what plain ${...} does in template text,
produced Bookshelf "Corner" & Co.. Escaping on output, with c:out or fn:escapeXml(...), is the default rule for
every value that is not known to be safe; Security covers the contexts (HTML body,
attribute, JavaScript, URL, CSS) and the OWASP encoder library.
<%-- unsafe: the title is delivered as markup if it contains HTML --%>
<td>${b.title}</td>
<%-- safe in an HTML body and in a quoted attribute --%>
<td><c:out value="${b.title}"/></td>
<td title="${fn:escapeXml(b.title)}">...</td>
Whitespace
JSP follows the whitespace rule of XML: whitespace in template text is not significant but is preserved. Directives and JSP comments produce no output, but the line break after them is template text, which is why the page of the first example begins with two empty lines. In a response that is HTML the extra lines are harmless; in a plain-text, CSV or XML response they can be fatal (an XML declaration must be the first characters of a file).
whitespace.jsp and whitespace-trim.jsp: the same page, with and without trimming<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
Books: ${fn:length(books)}
<%-- whitespace-trim.jsp differs only in its first line: --%>
<%@ page contentType="text/plain; charset=UTF-8" trimDirectiveWhitespaces="true" %>
whitespace.jsp$
$
$
Books: 3$
whitespace-trim.jsp (no line break after the last line)$
Books: 3
The first page returned three empty lines (one per directive) and Books: 3 with its line break. The second returned one
empty line and Books: 3 with no line break at all: template text that consists only of whitespace is removed, and the
text \nBooks: (a line break and the word) is kept because it contains a non-whitespace character. That is the specification’s exact rule, and it
explains why trimming leaves a leading line break and drops the trailing one. The configuration is the
trim-directive-whitespaces element of jsp-property-group (false by default, no effect on JSP documents) or the
trimDirectiveWhitespaces attribute of the page and tag directives. Tomcat adds a servlet parameter of its own,
trimSpaces, with the values true, false, single and extended; single replaces such a run by one space and
extended also removes leading and trailing whitespace of template text and collapses runs of whitespace and
newlines to a single newline. The Tomcat documentation recommends single or
extended to shrink responses in production. In a JSP document the rule is different and not configurable: text nodes
that consist only of whitespace are dropped, except inside jsp:text, which is kept verbatim.
Template Text Between Scriptlets
The oldest JSP idiom, from the first generation of the technology, is to open a Java block in one scriptlet, write template text, and close the block in another. It is the reason why a scriptlet’s braces must balance across the page, and it is what a lot of existing code looks like, so the Bookshelf is shown in that form once and then in the current form.
blocks.jsp: Java blocks that surround template text (legacy, shown to be able to read it)<%@ page contentType="text/html; charset=UTF-8" import="java.util.List,org.example.bookshelf.Book" %>
<ul>
<% for (Book b : (List<Book>) application.getAttribute("books")) { %>
<% if (b.year() < 1900) { %>
<li class="classic"><%= b.title() %></li>
<% } else { %>
<li><%= b.title() %></li>
<% } %>
<% } %>
</ul>
el-vs-scriptlet.jsp: the same output with EL and the tag library<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<ul>
<c:forEach var="b" items="${books}">
<li class="${b.year lt 1900 ? 'classic' : ''}"><c:out value="${b.title}"/></li>
</c:forEach>
</ul>
The scriptlet page produced three items, with class="classic" only on Emma (1815); the EL page produced the same three
items, with an empty class="" on the others (the conditional expression always writes the attribute, whereas the scriptlet
version omits it). The braces in the first page span three
scriptlets, and a missing <% } %> shows up as a compile error of the generated servlet rather than a translation
error: err-scriptlet-split.jsp (an if block that is never closed) produced Syntax error, insert "}" to complete
Block against a line of the generated .java file. Scripting Elements (Legacy) explains why this
style is discouraged and how to disable it with scripting-invalid; the example is here only because the books use it
heavily and because its translation rules are the same as for every other element.
Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
|
HTTP 500, |
write |
Missing |
|
close every element in the file that opened it |
JSP comment never closed |
|
add |
Nested JSP comment |
output contains the tail of the inner comment, such as |
JSP comments do not nest; remove the inner one |
Unescaped quote in an attribute |
|
use the other delimiter, |
Wrong case in an action name |
|
action and attribute names are case-sensitive |
Undeclared prefix |
no error: |
declare the prefix with a |
Same attribute set twice |
|
give the value once |
Unbalanced Java block |
|
balance braces across scriptlets, or use tags |
|
|
use the |
Data in an HTML comment |
the comment, with the evaluated data, is in "view source" |
use |
|
markup from the value is delivered as markup |
wrap in |
|
a backslash and the unevaluated expression (Tomcat 11.0.26), unlike in an attribute value |
avoid; generate the backslash with |
Old |
|
declare Servlet 6.1 ( |
Fixing the error and reloading |
a later request can report |
read the first message and the Tomcat log; the page is retried when the file changes |
What Changed Recently
The rules on this page are old and have been stable. The change log in Appendix C of the Pages 4.0 specification lists no change to the core syntax, the comment forms or the quoting rules between JSP 3.1 and 4.0; the changes are the removals around them. The history that matters for syntax is:
-
JSP 2.0 reserved
${...}for EL in template text and attribute values, and introduced\$as the way to quote it. -
JSP 2.1 reserved
#{...}, which is why#{in template text is a translation error unless allowed. -
A web application with a Servlet 2.4 or later descriptor evaluates EL by default; older descriptors ignore it. This is the default that Tomcat 11 still applies to a Servlet 2.3
web.xml. -
JSP 3.0 (Jakarta EE 9) moved the API from
javax.servlet.jsptojakarta.servlet.jsp; the syntax did not change. Tag library identifiers moved too:jakarta.tags.coreand friends are the JSTL 3.0 URIs, and the oldhttp://java.sun.com/jsp/jstl/coreidentifiers are still accepted as identifiers (they are not links). -
JSP 3.1 (Jakarta EE 10) added the
errorOnELNotFoundpage attribute and deprecatedisThreadSafeandjsp:plugin. -
JSP 4.0 (Jakarta EE 11) requires Java 17, removes
isThreadSafeandjsp:plugin, and removes the deprecatedjakarta.servlet.jsp.elclasses,BodyTag.EVAL_BODY_TAGandJspException.getRootCause. None of these touches the syntax described on this page.
Where the Books Are Outdated
The books consulted for this section target JSP 1.2 and JSTL 1.0 (and Servlet 2.3); this page uses them only for the list of topics. What changed since:
| Area | Then and now |
|---|---|
EL in template text |
JSP 1.2 had no expression language; |
|
does not exist in the books; it is reserved and an error in template text |
|
mandatory in JSP 1.2 documents; the specification now says most JSP 4.0 documents will not use it |
XML spellings of scripting elements |
presented in the books as an alternative form; still valid, and the only form of scripting element allowed in a JSP document |
Dynamic HTML comments |
|
Typographic quotes in listings |
some code samples print curly quotes, which are not valid attribute delimiters if the code is copied; the quoting rules above need straight |
Scriptlets as the normal way to write loops and conditions |
replaced by JSTL and EL; the Java block form is legacy |
|
Servlet 2.3 DTD; replaced by the Servlet 6.1 schema, |
|
|