JSTL Core Set Tag Gotcha
Migrating a JSP-heavy project from JSTL 1.0 to 1.2, some odd behavior broke at least one bit of JSP logic. After much debugging, it was determined that the problem lie with the JSTL Core taglib, specifically in the Set tag.
In our case, the tag was being used to set an attribute to the value of another attribute’s inner property, as the following shows:
<c:set var="something" value="${somethingElse.property}"/>
Even more, the same attribute ame was being used in different JSPs, connected by including one in another, although storing the attribute in different scopes, as the following shows:
<c:set var="something" value="${somethingElse.property}" scope="request" /> <c:set var="something" value="${anotherElse.property}" />
In this case, the including page contained the first line, the request-scope set value. The included file contained the second, implicit scope value. The page was included with the request-time <c:import url=”included.jsp”/>, which “calls” the included page. The included page gets its own page context, but shares the request and session. When things worked well, the request value was unmodified, and “returning” from the included page left the page attribute behind. The discovered problem occurred when the value of the property resolved to NULL. In this case, not only was the page attribute “unset,” but it happened that the request attribute was also lost.
It was unsettling to find that what seemed to be a default page context action would affect the other scopes, but it turns out to be the case!
The following JSP shows this with a simple example. A few values are set, just to show what’s happening, and then the attributes are modified to show the results.
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <c:set var="something" value="Saved in Session" scope="session" /> <c:set var="something" value="Saved in Request" scope="request" /> <c:set var="something" value="Saved in Page" scope="page" /> <html> <head> <style type="text/css"> .value { color: green; } </style> <title>Test</title> </head> <body> <p> Session: <span class="value"><%=session.getAttribute("something")%></span> <br />Request: <span class="value"><%=request.getAttribute("something")%></span> <br />Page: <span class="value"><%=pageContext.getAttribute("something")%></span> <br />EL: <span class="value">${something}</span> </p> <c:set var="something" value="${nothing}" scope="page" /> <p> Session: <span class="value"><%=session.getAttribute("something")%></span> <br />Request: <span class="value"><%=request.getAttribute("something")%></span> <br />Page: <span class="value"><%=pageContext.getAttribute("something")%></span> <br />EL: <span class="value">${something}</span> </p> <c:set var="something" value="${nothing}" /> <p> Session: <span class="value"><%=session.getAttribute("something")%></span> <br />Request: <span class="value"><%=request.getAttribute("something")%></span> <br />Page: <span class="value"><%=pageContext.getAttribute("something")%></span> <br />EL: <span class="value">${something}</span> </p> </body> </html>
The actions are pretty simple.
- The first block shows all of the values as initially set. Since the page attribute is set, the EL resolves to the page-scoped value.
- The page-scope value is set to something that resolves to NULL (note this doesn’t set the value to NULL, but removes the value from the attributes Map, which will resolve to NULL in future uses) with an explicit scope definition. This correctly clears the page attribute, but leaves the other alone, as is demonstrated by the EL evaluating to the request-scoped value.
- The final block sets the attribute with an implicit scope definition; the documentation says this will default to the page scope (which it does), but what the documentation fails to note is that the other scopes are also removed! This is demonstrated by all of the “null” entries, and the empty EL (EL doesn’t show NULL values).
Here’s what the output looks like:
Session: Saved in Session Request: Saved in Request Page: Saved in Page EL: Saved in Page Session: Saved in Session Request: Saved in Request Page: null EL: Saved in Request Session: null Request: null Page: null EL:
This “undocumented feature” is found in the implementation of the org.apache.taglibs.standard.tag.common.core.SetSupport.doEndTag() which has the following bit of close in it:
if (scopeSpecified) pageContext.removeAttribute(var, scope); else pageContext.removeAttribute(var);
It would seem that it should be the case that if the scope isn’t specified that the page scope would be used, as that is the default according to the documentation. It isn’t the case, though, so when not specified the attribute looks to be entirely removed. Checking deeper, at least in the Tomcat source, the pageContext.removeAttribute(var) ends up in the JSTL org.apache.jasper.runtime.PageContextImpl which finally results in executing the following:
private void doRemoveAttribute(String name) { removeAttribute(name, PAGE_SCOPE); removeAttribute(name, REQUEST_SCOPE); if( session != null ) { try { removeAttribute(name, SESSION_SCOPE); } catch(IllegalStateException ise) { // Session has been invalidated. // Ignore and fall throw to application scope. } } removeAttribute(name, APPLICATION_SCOPE); }
As we can see, it removes the attributes from all of the scopes, even the application (the Servlet’s Context) scope, which we didn’t test! As disconcerting as this is, the solution is trivial to implement: explicitly name the page scope where the attribute may collide and where the value may evaluate to NULL. As long as both of those don’t happen, the problem doesn’t occur. If they do both happen, though, no matter where the <c:set> is encountered, the request and session attributes are lost.
This has been tested on Tomcat v6, v7, and Weblogic v11.
how to invalidate session using c:foreach
invalidate session?