Internet Programming with Java Course
3.11. Java Standard Tag Library (JSTL)
JSTL 1.0: Standardizing JSP, Part 1
by Hans Bergsten, author of JavaServer Pages, 2nd Edition
June 11, 2002 started a new phase for JSP developers. That's when the JSP Standard Tag Library (JSTL) 1.0 specification was released. The Apache Taglibs project followed up with a reference implementation a few days later.
JSTL answers developers' demand for a set of standardized JSP custom actions to handle the tasks needed in almost all JSP pages, including conditional processing, internationalization, database access, and XML processing. This will speed up JSP development by more or less eliminating the need for scripting elements and the inevitable hard-to-find syntax errors, and by freeing up time previously spent on developing and learning zillions of project-specific custom actions for these common tasks.
This article is the first in a series of articles about how JSTL can simplify your life when using JSP, in applications large and small. In this article, I give you an overview of JSTL and show you how to use the most common JSTL actions. Future installments will focus on the internationalization and database access actions, how a servlet controller can interact with these actions, and how to use the JSTL classes as a base for your own custom actions. If you have suggestions for other areas you'd like to learn more about, please let me know.
JSTL Libraries Overview
JSTL 1.0 specifies a set of custom tag libraries based on the JSP 1.2 API. There are four separate tag libraries, each containing custom actions targeting a specific functional area. This table lists each library with its recommended tag prefix and default URI:
Description |
Prefix |
Default URI |
Core |
c |
http://java.sun.com/jstl/core |
XML Processing |
x |
http://java.sun.com/jstl/xml |
I18N & Formatting |
fmt |
http://java.sun.com/jstl/fmt |
Database Access |
sql |
http://java.sun.com/jstl/sql |
The Core library contains actions for everyday tasks, such as including or excluding a piece of a page depending on a runtime condition, looping over a collection of items, manipulating URLs for session tracking, and correct interpretation by the target resource, as well as actions for importing content from other resources and redirecting the response to a different URL.
The XML library contains actions for -- you guessed it -- XML processing, including parsing an XML document and transforming it using XSLT. It also provides actions for extracting pieces of a parsed XML document, looping over a set of nodes, and conditional processing based on node values.
Internationalization (i18n) and general formatting are supported by the actions in the I18N & Formatting library. You can read and modify information stored in a database with the actions provided by the Database Access library.
Over time, you can expect all Web containers to include an implementation of the JSTL libraries, so no additional code will need to be installed. Until that happens, you can download and install the JSTL reference implementation (RI) instead. It's developed within the Apache Taglibs project as a library named Standard. The link to the Standard library page is included in the Resources section at the end of this article. Installing the RI is easy: just copy all of the JAR files from the distribution's lib directory to the WEB-INF/lib directory for your Web application. Note that JSTL 1.0 requires a JSP 1.2 container, so make sure your container is JSP-1.2-compliant before you try this.
To use a JSTL library, whether it's the implementation included with the container or the RI, you must declare the library using a taglib directive, just as you would for a regular custom tag library:
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
Note that you should always use the default URI, even though the JSP specification allows you to override it. A container is allowed to generate optimized code for the JSTL action in the class corresponding to a JSP page. This can result in better performance than when the generated code calls out to tag handlers through the standard API. It's only when you use the default URI, however, that a container is able to use an optimized implementation.
The JSTL Expression Language
In addition to the tag libraries, JSTL 1.0 defines a so-called Expression Language (EL). The EL is a language for accessing runtime data from various sources. Its syntax is considerably more user-friendly than Java, which is the only language supported directly by the JSP 1.2 specification. All JSTL actions recognize EL expressions in their attribute values, and custom actions may be developed to do the same. It is expected that the EL will be incorporated into the next version of the JSP specification to encourage its use for data access over the Java language. If so, you will be able to use EL expressions in an action attribute value, and even in template text.
If you've used JavaScript, you should feel right at home with the EL. The EL borrows the JavaScript syntax for accessing structured data as either a property of an object (with the . operator) or as a named array element (with the ["name"] operator). JavaBeans component properties and java.util.Map entries, using the key as the property name, can be accessed this way. Here are some examples:
${myObj.myProperty}$
${myObj["myProperty"]}$
${myObj[varWithTheName]}$
As shown here, an EL expression must always be enclosed within ${ and } characters. The first two expressions access a property named myProperty in an object represented by a variable named myObj. The third expression access a property with a name that's held by a variable. Instead of a single variable, this syntax can be used with any expression that evaluates to the property name.
The array access operator is also used for data represented as a collection of indexed elements, such as a Java array or a java.util.List:
${myList[2]}$
${myList[aVar + 1]}$
In addition to the property and array element operators and the arithmetic, relational, and logical operators, a special operator for testing if an object is "empty" or not can be used in an EL expression. The following table lists all operators:
Operator |
Description |
. |
Access a property |
[] |
Access an array/list element |
() |
Group a subexpression |
+ |
Addition |
- |
Subtraction or negation of a number |
/ or div |
Division |
% or mod |
Modulo (remainder) |
== or eq |
Test for equality |
!= or ne |
Test for inequality |
< or lt |
Test for less than |
> or gt |
Test for greater than |
<= or le |
Test for less than or equal |
>= or gt |
Test for greater than or equal |
&& or and |
Test for logical AND |
|| or or |
Test for logical OR |
! or not |
Unary Boolean complement |
empty |
Test for empty value (null, empty string, or an empty collection) |
What you don't find in the EL are statements such as assignments, if/else, or while. Action elements are used for this type of functionality in JSP, and the EL is not intended to be a general-purpose programming language, just a data access language.
Literals and variables are, of course, also part of the language. The EL provides the following literals, similar to what you find in JavaScript, Java, and other languages:
Literal Type |
Description |
String |
Enclosed with single or double quotes. A quote of the same type within the string must be escaped with backslash: (\' in a string enclosed with single quotes; \" in a string enclosed with double quotes). The backslash character must be escaped as \\ in both cases. |
Integer |
An optional sign (+ or -) followed by digits between 0 and 9. |
Floating Point |
The same as an integer literal, except that a dot is used as the separator for the fractional part and an exponent can be specified as e or E, followed by an integer literal. |
Boolean |
true or false. |
Null |
null. |
Any object in one of the JSP scopes (page, request, session, or application) can be used as a variable in an EL expression. For instance, if you have an bean with a firstName property in the request scope under the name customer, this EL expression represents the value of that bean's firstName property:
${customer.firstName}
But it doesn't stop there. The EL also makes request information and general container information available as a set of implicit variables:
Variable |
Description |
param |
A collection of all request parameters as a single string value for each parameter. |
paramValues |
A collection of all request parameters as a string array value for each parameter. |
header |
A collection of all request headers as a single string value for each header. |
headerValues |
A collection of all request headers as a string array value for each header. |
cookie |
A collection of all request cookies as a single javax.servlet.http.Cookie instance value for each cookie. |
initParams |
A collection of all application init parameters as a single string value for each parameter. |
pageContext |
An instance of the javax.servlet.jspPageContext class. |
pageScope |
A collection of all page scope objects. |
requestScope |
A collection of all request scope objects. |
sessionScope |
A collection of all session scope objects. |
applicationScope |
A collection of all application scope objects. |
The first five implicit variables in the table give you access to the parameter values, headers, and cookies for the current request. Here's an example of how to access a request parameter named listType and the User-Agent header:
${param.listType}
${header['User-Agent']}
Note how you must use the array syntax for the header, because the name includes a dash; with the property syntax, it would be interpreted as the value of the variable expression header.User minus the value of the variable named Agent.
The initParameter variable provides access to init parameters that are defined for the application in the web.xml file. The pageContext variable has a number of properties that provide access to the servlet objects that represent the request, response, session, application, etc. Look at the JSP specification to learn more about these properties.
The final four variables are collections containing all objects in each specific scope. You can use these to limit the search for an object to just one scope instead of searching all scopes, which is the default if no scope is specified. In other words, if there's an object named customer in the session scope, the first two expressions here find the same object and the third comes up empty:
${customer}
${sessionScope.customer}
${requestScope.customer}
All JSTL actions accept EL expressions as attribute values, for all attributes except var and scope, because these attribute values may be used for type checking at translation time in a future version. There's one additional JSTL action attribute that does not take an EL expression value, but it's only used in the XML library, so let's ignore that for now. One or more EL expressions can be used in the same attribute value, and fixed text and EL expressions can be mixed in the same attribute value:
First name: <c:out value="${customer.firstName}" />
<c:out value="First name: ${customer.firstName}" />
Before we jump in and look at examples using the Core actions, let me qualify something I said earlier: all JSTL actions in the EL library set accept EL expressions. There's actually a parallel set of JSTL libraries, referred to as the RT library set, that only accept the old-style Java expressions:
First name: <c_rt:out value="<%= customer.getFirstName() %>" />
I encourage you to use the EL libraries instead, but if you're curious, you can read about the RT libraries in the JSTL spec or in my book, JavaServer Pages (O'Reilly, 2nd edition 2002).
Conditional Processing and Iterations
Let's look at some examples of how you can use the JSTL conditional and iteration actions: <c:if>; the <c:choose>, <c:when>, and <c:otherwise> triple; and <c:forEach>. Along the way, we also use the basic output and variable setting actions: <c:out> and <c:set>.
<c:if> allows you to conditionally include, or process, a piece of the page, depending on runtime information. This sample includes a personal greeting if the user is a repeat visitor, as indicated by the presence of a cookie with the user's name:
<c:if test="${!empty cookie.userName}">
Welcome back <c:out value="${cookie.userName.value}" />
</c:if>
The test attribute value is an EL expression that checks if the cookie is present. The empty operator combined with the "not" operator (!) means it evaluates to true if the cookie is not present, causing the element body to be processed. Within the body, the <c:out> action adds the value of the cookie to the response. It's that easy.
Looping through a collection of data is almost as simple. This snippet iterates through a collection of rows from a database with weather information for different cities:
<c:forEach items="${forecasts.rows}" var="city">
City: <c:out value="${city.name}" />
Tomorrow's high: <c:out value="${city.high}" />
Tomorrow's low: <c:out value="${city.low}" />
</c:forEach>
The EL expression for the items value gets the value of the rows property from an object represented by the forecasts variable. As you will learn in future articles, the JSTL database actions represent a query result as an instance of a class named javax.servlet.jsp.jstl.sql.Result. This class can be used as a bean with a number of properties. The rows property contains an array of java.util.SortedMap instances, each one representing a row with column values. The <c:forEach> action processes its body once for each element in the collection specified by the items attribute. Besides arrays, the action works with pretty much any data type that represents a collection, such as instances of java.util.Collection and java.util.Map.
If the var attribute is specified, the current element of the collection is made available to actions in the body as a variable with the specified name. Here it's named city and, since the collection is an array of maps, this variable contain a new map with column values every time the body is processed. The column values are added to the response by the same type of <c:out> actions that you saw in the previous example.
To illustrate the use of the remaining conditional actions, let's extend the iteration example to only process a fixed set of rows for each page request, and add "Previous" and "Next" links back to the same page. The user can then scroll through the database result, looking at a few rows at a time, assuming the Result object is saved in the session scope. Here's how to only process some rows:
<c:set var="noOfRows" value="10" />
<c:forEach items="${forecasts.rows}" var="city"
begin="${param.first}" end="${param.first + noOfRows - 1}">
City: <c:out value="${city.name}" />
Tomorrow's high: <c:out value="${city.high}" />
Tomorrow's low: <c:out value="${city.low}" />
</c:forEach>
The <c:set> action sets a variable to the value specified by the value attribute; either a static value, as in this example, or an EL expression. You can also specify the scope for the variable with the scope attribute (page, request, session or application). In this example, I set a variable named noOfRows to 10 in the page scope (the default). This is the number of rows to show for each request.
The <c:forEach> in this example takes the same values for the items and var attributes as before, but I have added two new attributes.
Next we add the "Previous" and "Next" links:
<c:choose>
<c:when test="${param.first > 0}">
<a href="foreach.jsp?first=<c:out value="${param.first - noOfRows}"/>">
Previous Page</a>
</c:when>
<c:otherwise>
Previous Page
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${param.first + noOfRows < forecasts.rowsCount}">
<a href="foreach.jsp?first=<c:out value="${param.first + noOfRows}"/>">
Next Page</a>
</c:when>
<c:otherwise>
Next Page
</c:otherwise>
</c:choose>
The <c:choose> groups one or more <c:when> actions, each specifying a different Boolean condition. The <c:choose> action tests each condition in order and allows only the first <c:choose> action with a condition that evaluates to true to process its body. The <c:choose> body may also contain a <c:otherwise>. Its body is processed only if none of the <c:when> actions' conditions are true.
In this example, the first <c:when> action tests if the first parameter is greater than 0, i.e. if the page displays a row subset other than the first. If that's true, the <c:when> action's body adds a link back to the same page with the first parameter set to the index of the previous subset. If it's not true, the <c:otherwise> action's body is processed, adding just the text "Previous Page" as a placeholder for the link. The second <c:choose> block provides similar logic for adding the "Next Page" link.
URL Processing
The previous example works fine as long as cookies are used for session tracking. That's not a given; cookies may be turned off, or not supported, in the browser. It's therefore a good idea to enable the container to use URL rewriting as a backup for cookies. URL rewriting, as you may know, means putting the session ID in all URLs used in links and forms in the page. A rewritten URL looks something like this:
myPage.jsp;jspsessionid=ah3bf5e317xmw5
When the user clicks on a link like this, the session ID is sent to the container as part of the URL. The JSTL Core library includes the <c:url> action, which takes care of URL rewriting for you. This is how you can use it to improve the generation of the "Previous Page" link from the previous example:
<c:url var="previous" value="foreach.jsp">
<c:param name="first" value="${param.first - noOfRows}" />
</c:url>
<a href="<c:out value="${previous}"/>">Previous Page</a>
The <c:url> supports a var attribute, used to specify a variable to hold the encoded URL, and a value attribute for the URL to be encoded. URL query string parameters can be specified using nested <c:param> actions. Special characters in the parameters specified by nested elements are encoded (if needed) and then added to the URL as query string parameters. The final result is passed through the URL rewriting process, adding a session ID if cookie-based session tracking is disabled. In this example, the fully encoded URL is then used as the href attribute value in the HTML link element.
The <c:url> also performs another nice service. As you may be aware, relative URLs in HTML elements must either be relative to the page that contains them or to the root directory of the server (if they start with a slash). The first part of a URL path for a JSP page is called the context path, and it may vary from installation to installation. You should therefore avoid hard-coding the context path in the JSP pages. But sometimes you really want to use a server-relative URL path in HTML elements; for instance when you need to refer to an image file that's located in an /images directory shared by all JSP pages, no matter where in the document structure the page reside. The good news is that if you specify a URL starting with a slash as the <c:url> value, it converts it to a server-relative path. For instance, in an application with the context path /myApp, the <c:url> action converts the path to /myApp/images/logo.gif:
<c:url value="/images/logo.gif" />
There are a few more actions related to URLs in the Core library. The <c:import> action is a more flexible action than the standard <jsp:include> action. You can use it to include content from resources within the same Web application, from another Web application in the same container, or from another server, using protocols like HTTP and FTP. The <c:redirect> action lets you redirect to another resource in the same Web application, in another Web application, or on a different server. Both actions are straightforward to use, so I leave it as an exercise for you to try them out.
Conclusion
In this installment, I have described the JSTL basic building blocks: the set of libraries and the Expression Language. I have also provided examples of how to use most of the actions in the Core library. You can download the JSTL reference implementation and use it with any JSP-1.2-compatible Web container to experiment with these actions. The RI includes a number of examples to help you get started.
In upcoming articles, we will look at the remaining JSTL libraries, including configuration options and how to control their behavior from a controller servlet when using an MVC framework such as Struts. I will also show you how classes defined by the JSTL specification can be used to simplify developing your own custom actions.
JSTL 1.0: What JSP Applications Need, Part 2
Part 1 of this series gave you an overview of JSTL -- the new specification of commonly-needed JSP tag libraries -- and showed you how to use the core JSTL actions. In this article, I'll dig a bit deeper and discuss how JSTL can help you with internationalization and database access. The bulk of the article does not require any Java programming knowledge, but the sections that deal with how servlets and other Java classes interact with the JSTL actions do. If you're not a Java programmer, you can just skip those sections.
Large Web sites often need to please visitors from all over the world, and serving up content in just one language doesn't cut it. To develop a Web site that provides a choice of languages, you have two options:
Often you end up with a mixture of these techniques, using separate pages for mostly static, large amounts of content and shared pages when the amount of content is small but dynamic (e.g., a page with a few fixed labels displayed in different languages and all other data coming from a database).
Preparing an application for multiple languages is called internationalization (commonly abbreviated as i18n) and making content available for a specific language is called localization (or l10n). To do this right, you need to consider other things besides the language. How dates and numbers are formatted differ between countries, and even within countries. Colors, images, and other nontextual content may also need to be adapted to the customs of a certain region. The term locale refers to a set of rules and content suitable for a specific region or culture.
JSTL includes a set of actions to simplify internationalization, primarily when shared pages are used for multiple languages. Let's first look at how to properly format dates and numbers. This example formats the current date and a number based on the rules for a default locale.
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
<html>
<body>
<h1>Formatting with the default locale</h1>
<jsp:useBean id="now" class="java.util.Date" />
Date: <fmt:formatDate value="${now}" dateStyle="full" />
Number: <fmt:formatNumber value="${now.time}" />
</body>
</html>
The first line is the taglib
directive for the JSTL library that contains the i18n and formatting
actions. The default prefix, used here, is fmt
.
To get a value to format, I then create a java.util.Date
object that represents the current date and time, and save it as a variable
named now
.
We're now prepared to do some locale-sensitive formatting. The JSTL <fmt:formatDate>
action formats
the now
variable value,
assigned to the value
attribute value using the type of EL expression you learned about in the previous
article in this series. The dateStyle
attribute tells the action how to format the date.
You can use any of the values default
,
short
, medium
, long
, or full
for the dateStyle
attribute.
Exactly what type of formatting these values represent varies depending on the
locale. For the English locale, full
results in a string like Thursday, August 29, 2002. In this example, I
have only formatted the date part, but you can include the time part as well
(or format just the time part), and define customized formatting rules for both
the date and time instead of the locale-dependent styles:
<fmt:formatDate value="${now}" type="both"
pattern="EEEE, dd MMMM yyyy, HH:mm" />
The pattern
attribute takes a custom formatting pattern of the same type as the java.text.SimpleDateFormat
class. The
pattern used here results in Thursday, 29 August 2002, 17:29 with the
English locale when I write this. The <fmt:formatNumber>
action supports similar attributes to specify how to format a number using
locale-dependent styles for a regular number, currency value, or a percentage,
as well as using customized patterns of different kinds. You can learn more
about all of the formatting options in the JSTL specification or in my JavaServer Pages, 2nd
Edition book.
Back to the main question: how is the locale for formatting the date and the
number determined? If you use the JSTL actions exactly as in this example,
doing nothing else, the formatting locale is determined by comparing the
locales specified in a request header named Accept-Language
with the locales supported by the Java runtime environment.
The header, if present, contains one or more locale specifications (in the form of a language code and possibly a country code), with information about their relative priority. The user uses browser configurations to define which locale specifications to send. The request locales are compared in priority order with the ones offered by the Java runtime, and the best match is selected.
If no match is found, the formatting action looks for the so-called fallback-locale configuration setting. A configuration setting is a value set either by a context parameter in the application's web.xml file or by a JSTL action or Java code in one of the JSP scopes. To set the fallback locale in the web.xml file, include these elements:
<context-param>
<param-name>
javax.servlet.jsp.jstl.fmt.fallbackLocale
</param-name>
<param-value>
de
</param-value>
</context-param>
With this setting, the German locale (specified by the de
language code as the parameter value)
is used if none of the locales specified by the request are supported by the
Java runtime.
JSTL also lets you set a hardcoded default locale for the application that you can then override when needed. This gives you full control over which locale is used, instead of relying on the whims of the visitor's browser configuration. The default locale is also a configuration setting. It can be specified with a context parameter in the web.xml like this:
<context-param>
<param-name>
javax.servlet.jsp.jstl.fmt.locale
</param-name>
<param-value>
en
</param-value>
</context-param>
This setting establishes English as the default locale for the application, resulting in the previous JSP page example always formatting the date and the number according to the English rules.
To override the default locale configuration setting, you can use the <fmt:setLocale>
action. It sets
the default locale within a specific JSP scope. Here's an example that sets the
default locale for the page scope, based on a cookie that keeps track of the
locale a user selected during a previous visit:
<h1>Formatting with a locale set by setLocale</h1>
<c:if test="${!empty cookie.preferredLocale}">
<fmt:setLocale value="${cookie.preferredLocale.value}" />
</c:if>
<jsp:useBean id="now" class="java.util.Date" />
Date: <fmt:formatDate value="${now}" dateStyle="full" />
Number: <fmt:formatNumber value="${now.time}" />
Here I first use the JSTL <c:if>
action to test if a cookie named preferredLocale
is received with the request. If so, the <fmt:setLocale>
action overrides the locale for the current page by setting the locale
configuration variable in the page scope. If you want to, you can set the
locale for another scope using the scope
attribute. Finally, the date and the number are formatted according to the
rules for the locale specified by the cookie, or the default locale, if the
cookie isn't present.
In addition to formatting actions, JSTL includes actions for interpreting
(parsing) dates and numbers in a locale-sensitive way: <fmt:parseDate>
and <fmt:parseNumber>
. These actions
support pretty much the same attributes as their formatting counterparts, and
can be used to convert dates and numeric input into their native form before
they are further processed. I'll show you an example of this later in this
article.
If you've developed Web applications using Java technology for some time, you have no doubt heard about the MVC pattern and, most likely, about the Apache Struts MVC framework. The basic idea behind the MVC pattern is that an application is easier to maintain and evolve if the different parts of the application (Model, View, and Controller) are implemented as separate components. For Java, this typically means using beans as the Model (business logic), JSP pages as the View (the user interface), and a servlet as the Controller (the piece that controls the communication between the View and the Model). The Struts framework provides a generic Controller servlet that delegates the processing of specific types of requests to classes called Action classes, and then uses a JSP page specified by the Action to generate the response.
JSTL is designed to play well in an MVC-based application by exposing a
class that can be used by any Java component, such as a Struts Action class, to
access the configuration variables used by the JSTL actions in the JSP pages
representing the View. The class is called javax.servlet.jsp.jstl.core.Config
and contains constants (static final String
variables) for all configuration variables, and methods for setting, getting,
and removing the variables in different JSP scopes. You can use code like this
in a Struts Action class to set the default locale for the session, based on
profile data, when a user logs in to the application:
import javax.servlet.jsp.jstl.core.Config;
...
public class LoginAction extends Action {
public ActionForward perform(ActionMapping mapping,
ActionForm form, HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
String userName = request.getParameter("userName");
String password = request.getParameter("password");
...
User user = authenticate(userName, password);
if (user != null) {
/*
* Valid user. Set the default locale in the session
* to the user's preferred locale
*/
HttpSession session = request.getSession();
Config.set(session, Config.FMT_LOCALE, user.getLocale());
...
}
}
The Config
class provides
four versions of the set()
method, one for each JSP scope (specified by the first method parameter). Here
I use the one that sets the variable in the session scope. The second parameter
is the name of the configuration, typically set by the corresponding constant.
The third parameter is the variable value. For reading and removing variables,
the class provides similar get()
and remove()
methods, plus a
find()
method for locating a
variable in any scope. In addition to using these methods in a Controller, you
may want to use them in your own custom tag handlers to take advantage of the
JSTL configuration setting mechanism.
While date and number formatting is important when localizing an
application, the text content is, of course, even more so. JSTL is based on
Java, so it leverages the generic i18n support in the Java platform.
When it comes to text, this support is based on what's called a resource
bundle. In its simplest form, a resource bundle is represented by a text
file containing keys and a text value for each key. This example shows a file
with two keys (hello
and goodbye
) and their values:
hello=Hello
goodbye=Goodbye
Multiple locales are supported by creating separate files for each locale,
with a filename that includes the locale name. For instance, if the resource
bundle file above represents the English locale, it may be stored in a file
named labels_en.properties. The resource bundle for the Swedish locale
would then be stored in a file named labels_sv.properties, where sv
is the language code for Swedish. The
fixed part of the file name, labels
in this example, is called the resource bundle base name, and it's
combined with a locale specification (such as a language code) to find the
resource bundle for a specific locale. All resource bundle files include the
same keys; only the text differs depending on the locale. The files must be
located in a directory that's part of the Web container's classpath, typically
the application's WEB-INF/classes directory.
The JSTL action that adds text from a resource bundle to a page is the <fmt:message>
action. The bundle
to use can be specified in a number of ways. One way is to nest the <fmt:message>
actions within the
body of a <fmt:bundle>
action:
<fmt:bundle basename="labels">
Hello: <fmt:message key="hello" />
Goodbye: <fmt:message key="goodbye" />
</fmt:bundle>
In this case, the <fmt:bundle>
action locates the bundle for the locale that is the closest match between the
locale configuration setting (or the locales in the Accept-Language
header, if no default
locale is set) and the available resource bundles for the specified base name.
The nested actions then get the text from this bundle for the specified key.
As with the formatting actions, you can also establish a default bundle,
either for the whole application with a context parameter or with the <fmt:setBundle>
action or the Config
class for a specific JSP scope:
<fmt:setBundle basename="labels"/>
...
Hello: <fmt:message key="hello" />
Goodbye: <fmt:message key="goodbye" />
After a default bundle has been defined, you can use the <fmt:message>
actions standalone
within the scope where the default is established.
There are more options for the i18n and formatting actions than I can
fit into this article. For instance, you can use messages that contain dynamic
values assigned using nested <fmt:param>
actions, and override which bundle to use within a certain context. Another
important area when it comes to i18n is how to handle languages with
non-Western characters. I describe all of this and more in JavaServer
Pages, 2nd Edition.
A somewhat controversial subject is JSTL's inclusion of actions for accessing a database. Some people see this as encouraging bad practices, and argue that all database access should be performed by pure Java components in an MVC-based application instead of by JSP pages. I agree with this point of view for anything but the simplest applications, but there are a lot of applications that qualify as very simple, and where lack of programming skills or time makes a full-blown MVC architecture impractical. Without JSTL support, these applications often end up with Java database access code in scriptlets instead, and that is far worse from both a development and maintenance standpoint. I'll therefore show you how to use JSTL to access a database in JSP pages, but ask you to keep in mind that this approach is not suitable for all types of applications. If your development team includes Java programmers, you should carefully consider encapsulating the database access code in Java classes instead, and use JSP only to display the result.
The JSTL database actions are based on the general Java database API (JDBC)
and use the javax.sql.DataSource
abstraction introduced in JDBC 2.0 to represent the database. A DataSource
provides connections to the
database and can implement a feature called connection pooling. Opening
a physical connection to a database is a very time-consuming operation. With
connection pooling, it only needs to be done once, and the same connection can
then be reused over and over, without risking problems associated with other
connection-sharing approaches.
JSTL supports a number of ways to make a DataSource
available to the database actions. In a Web container with Java Naming and
Directory Interface (JNDI) support, a default DataSource
can be defined as a JNDI resource with a
context parameter in the web.xml file:
<context-param>
<param-name>
javax.servlet.jsp.jstl.sql.dataSource
</param-name>
<param-value>
jdbc/Production
</param-value>
</context-param>
The Web container's JNDI configuration tools must be used to configure a JNDI resource with the specified name; for instance, with a database account username and password, min and max connections in the pool, etc. How this is done varies between containers and is out of scope for this article (I cover, in detail, how to do it for Tomcat 4 in JavaServer Pages, 2nd Edition, though).
An alternative for containers that do not support JNDI is to let an
application (servlet context) lifecycle listener create and configure a DataSource
and set it as the default
using the JSTL Config
class:
import javax.servlet.*;
import javax.servlet.http.*;
import oracle.jdbc.pool.*;
public class AppListener implements ServletContextListener {
private OracleConnectionCacheImpl ds =null;
public void contextInitialized(ServletContextEvent sce){
ServletContext application =sce.getServletContext();
try {
ds = new OracleConnectionCacheImpl();
ds.setURL("jdbc:oracle:thin:@voyager2:1521:Oracle9i");
ds.setMaxLimit(20);
ds.setUser("scott");
ds.setPassword("tiger");
}
catch (Exception e){
application.log("Failed to create data source:"+
e.getMessage());
}
Config.SQL_DATA_SOURCE;
}
...
}
The listener class in this example creates a DataSource
with connection pool capabilities for an
Oracle9i database, and makes it available as the default for the JSTL
actions by using the Config
class to set the corresponding configuration variable.
A third way, only suitable for prototyping or applications that are not so
heavily used as to need connection pooling, is to use the <sql:setDataSource>
action:
<sql:setDataSource
url="jdbc:mysql://dbserver/dbname"
driver="org.gjt.mm.mysql.Driver"
user="scott"
password="tiger" />
This action creates a simple data source, without pooling, for the specified JDBC URL, user and password, using the specified JDBC driver. You may use this action to get started, but I recommend that you use one of the other alternatives for a real site. Besides the lack of connection pooling for a data source created this way, it's not a good idea to include sensitive information like the database URL, username and password in a JSP page, since it may be possible for a Bad Guy to get access to the source of the page. Even though it shouldn't be possible, several bugs have been discovered in Web containers over the years that made this possible (as far as I know, all of them have been plugged in recent versions of the most commonly-used containers).
With a DataSource
available, we can access the database. Here's how you read data from a database
represented by the default DataSource
:
<%@ taglib prefix="sql" uri="http://java.sun.com/jstl/sql" %>
<html>
<body>
<h1>Reading database data</h1>
<sql:query var="emps" sql="SELECT * FROM Employee" />
...
</body>
</html>
First you need to declare the JSTL library that contains the database
actions, using the taglib
directive at the top of this example. The <sql:query>
action executes the SQL SELECT
statement specified by the sql
attribute (or as the body of the action element) and saves the result in the
variable named by the var
attribute.
The database query result is returned as a bean of the type javax.servlet.jsp.jstl.sql.Result
with a
number of read-only properties:
Property |
Java Type |
Description |
|
|
An array with a case-insensitive map per row with keys matching column names and values matching column values. |
|
|
An array with an array per row with column values. |
|
|
An array with column names. |
|
|
The number of rows in the result. |
|
|
|
I showed you how to use the JSTL <c:forEach>
action to display all or just some of the rows in part 1 of this article, so
let's see how you can get just some of the rows and display them all in
this part. Next and Previous links allow the user to ask for a different set.
First, here's how to read a subset of the rows and then display the complete
subset:
<c:set var="noOfRows" value="10" />
<sql:query var="emps"
startRow="${param.start}" maxRows="${noOfRows}">
SELECT * FROM Employee
</sql:query>
<ul>
<c:forEach items="${emps.rows}" var="${emp}">
<li><c:out value="${emp.name}" />
</c:forEach>
</ul>
The startRow
attribute
for the <sql:query>
action is set to an EL expression that reads the value of a request parameter
named start
. You'll soon see
how its value changes when clicking on the Next and Previous links. The first
time the page is accessed, the parameter is not present at all, so the
expression evaluates to 0. This means the query result contains rows starting
with the first matching row (index 0). The maxRows
attribute limits the total number of rows to the value of the noOfRows
variable, set to 10 in this
example. The <c:forEach>
action loops through all rows in the result and generates a list item with one
of the column values for each row.
We must also generate Next and Previous links to let the user grab a new set of rows:
<c:choose>
<c:when test="${param.start > 0}">
<a href="emplist.jsp?start=<c:out
value="${param.start - noOfRows}"/>">Previous Page</a>
</c:when>
<c:otherwise>
Previous Page
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${emps.limitedByMaxRows}">
<a href="emplist.jsp?start=<c:out
value="${param.start + noOfRows}"/>">Next Page</a>
</c:when>
<c:otherwise>
Next Page
</c:otherwise>
</c:choose>
The first <c:choose>
block is identical to the example in part 1; if the start
request parameter is greater than
zero, the current page shows a row subset other than the first, so a Previous
link is added. The link points back to the same page, and includes the start
parameter with a value that is its
current value minus the number of rows displayed per page.
The second <c:choose>
block takes advantage of the query result's limitedByMaxRows
property. If this property is true
,
it means that the current result is truncated to the number of rows displayed
per page. Hence, a Next link is generated with a start
parameter value for the next row subset.
Besides reading data from a database, you can use JSTL to update information, as well. This example shows how to insert a new row in a table:
<c:catch var="error">
<fmt:parseDate var="empDate" value="${param.empDate}"
pattern="yyyy-MM-dd" />
</c:catch>
<c:if test="${error != null}">
<jsp:useBean id="empDate" class="java.util.Date" />
</c:if>
<sql:update>
INSERT INTO Employee (FirstName, LastName, EmpDate)
VALUES(?, ?, ?)
<sql:param value="${param.firstName}" />
<sql:param value="${param.lastName}" />
<sql:dateParam value="${empDate}" type="date" />
</sql:update>
Before inserting the row, this example illustrates how to use the JSTL
parsing actions, as I promised earlier. The page expects all data for the new
row to be sent as request parameters (perhaps entered in an HTML form),
including an employment date. Before the date can be inserted into the database,
it must be converted to its native Java form. That's what the <fmt:parseDate>
action does. The value
attribute contains an EL
expression that gets the value of the empDate
request parameter. The action tries to interpret it as a date written in the format
specified by the pattern
attribute (a four-digit year followed by a two-digit month and a two-digit day,
separated by dashes). If it's successful, it stores the date in its native form
in a variable with the name specified by the var
attribute.
The <c:catch>
action takes care of invalid date strings. If the parameter value can not be
interpreted as a date, the <fmt:parseDate>
throws an exception, which the <c:catch>
action catches and saves in the specified variable. When this happens, the <c:if>
action's test condition
evaluates to true
, so the
variable for the employment date is instead created by the nested <jsp:useBean>
action.
To insert the row, you use the <sql:update>
action. As with the query action, the SQL statement can be specified as the element's
body or by an sql
attribute.
The <sql:update>
action can be used to execute INSERT
,
UPDATE
, and DELETE
statements, as well as statements
that create or remove database objects, such as CREATE TABLE
and DROP
TABLE
. The number of rows affected by the statement can
optionally be captured in a variable named by a var
attribute.
In this example (as in most real-world applications), the column values are
not known until runtime; they come from request parameters. The SQL INSERT
statement therefore includes one
question mark per value as a placeholder and nested parameter actions that set
the value dynamically. The FirstName
and LastName
columns are
text columns, and <sql:param>
actions set their values to the values of the corresponding request parameters.
The EmpDate
column,
however, is a date column, demanding special attention. First of all, you must
use a variable that holds a date in its native form (a java.util.Date
object), so instead of
using the request parameter value, we use the variable created by the <fmt:parseDate>
or <jsp:useBean>
actions. Second, you
must use the <sql:dateParam>
action to set the value. In this example, I'm using only the date part, so I
also set the optional type
attribute to date
. Other
valid values are time
and timestamp
(the default), for columns
that take only the time or both the date and time.
There's one more JSTL database action that I have not described so far: <sql:transaction>
. You can use
this action to group multiple update (or even query) actions when they must all
be executed as part of the same database transaction. The standard example is
transferring an amount of money from one account to another, implemented as one
SQL statement that removes the money from the first account and another
statement that adds it to the other. The JSTL specification and my book include
detailed examples of how to use transactions.
If you encapsulate all database access in Java classes instead of using the
JSTL database action, there's still one part of JSTL that you may find useful.
It's a class named javax.servlet.jsp.jstl.sql.ResultSupport
,
with these two methods:
public static Result toResult(java.sql.ResultSet rs);
public static Result toResult(java.sql.ResultSet rs, int maxRows);
You can use this class to turn a standard JDBC ResultSet
object into a JSTL Result
object before forwarding it to a
JSP page for display. The JSTL actions can easily access the data in a Result
object, as shown earlier.
Another, arguably better, approach is to pass the query result to the JSP page
as a custom data structure, such as a List
of beans that contain the data for each row, but the Result
object is still a good candidate
for prototypes and small applications.
In this installment, I have described the JSTL support for internationalization and database access, including how some of the resources used by these actions can be configured through the web.xml file, JNDI, and servlets, letting other components of an MVC-based application interact with the JSTL actions. It's impossible to discuss everything about these subjects in detail in an article, but I hope that what I've described is enough to get you started.
In the next part, we will look at how classes defined by the JSTL specification can be used to simplify development of your own custom actions.
JSTL 1.0: What JSP Applications Need, Part 3
Previous installments in this series gave you an overview of JSTL--the new specification of commonly needed JSP tag libraries--and showed you how to use the core, internationalization, and database JSTL actions, as well as how to use JSTL effectively in an MVC application. In this final installment, we'll look at how you can leverage the JSTL classes when you develop your own custom actions. To understand what I describe here, you need to be a Java programmer and also know a thing or two about how to develop JSP custom actions in general.
Accepting EL Expressions as Attribute Values
The JSTL specification introduces an Expression Language (EL) that can be used to set JSTL action attributes to values computed at runtime, as you have seen in the previous parts of this series. A common question is "Can I also use the EL for setting attribute values in my custom actions?" The answer is: yes and no.
A JSP 1.2 container doesn't know anything about EL expressions, so they are evaluated by code in the JSTL tag handlers. JSTL 1.0 doesn't define a public API for this evaluation code, so there's no way to let custom tag handlers do the same in a way that works with all JSTL implementations. You can, however, pick one JSTL implementation and code to its API. For instance, if you're willing to be dependent on the JSTL Reference Implementation (RI) developed in the Apache Taglibs project (see the Resource section), you can use this org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager method in your tag handler:
public static Object evaluate(String attributeName,
String expression,
Class expectedType,
Tag tag,
PageContext pageContext);
This method takes an EL expression and evaluates it in the specified PageContext and converts (coerces) the result to the specified expected type, according to the rules defined by the JSTL 1.0 specification. The other parameters are used to include details in a possible error message, such as the name of the custom action and the attribute where the invalid expression is used.
You must call this method in one of the main methods in the tag handler (e.g. doEndTag()), never in the attribute setter method. The reason for this is that the setter method may not be called every time the tag handler is used, as I describe in my article, "JSP 1.2: Great News for the JSP Community, Part 2" (in the Tag handler life cycle and instance reuse section).
Here's a tag handler that accepts an EL expression as the value of its name attribute:
package com.ora.jstl;
import java.io.IOException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;
import org.apache.taglibs.standard.lang.support.ExpressionEvaluatorManager;
public class HelloTag extends TagSupport {
private String nameEL;
public void setName(String name) {
nameEL = name;
}
public int doEndTag() throws JspException {
String name = (String)
ExpressionEvaluatorManager.evaluate("name", nameEL,
String.class, this, pageContext);
if (name == null || name.length() == 0) {
name = "World";
}
try {
pageContext.getOut().write("Hello " + name + "!");
}
catch (IOException e) {}
return EVAL_PAGE;
}
}
Note that to compile and use this tag handler, you must have the JSTL RI ExpressionEvaluatorManager in the classpath; it's available in the standard.jar file that's part of the RI download.
If you can hold your horses a bit and wait for JSP 2.0, you don't have to do anything in your tag handlers to accept EL expression attribute values. JSP 2.0 (currently at the Proposed Final Draft stage, expected to be released Q1 2003) will include a somewhat extended version of the EL and will evaluate EL expressions before calling tag handler attribute setter methods. Hence, EL expressions can be used with any tag handler that is declared in the TLD to accept a runtime value. JSP 2.0 will also accept EL expressions anywhere in the template text.
Developing JSTL-Style Conditional Custom Actions
The JSTL specification group realized that no matter how many custom actions JSTL defines, there will always be a need for application-dependent custom actions. We tried to make it as easy as possible to develop custom actions that integrate nicely with the actions defined by the JSTL specification by including a number of public interfaces and base classes in the specification. The following sections show you some examples of how to use these classes and interfaces, starting with custom conditional actions.
The generic <c:if> and <c:when> actions, using the Boolean value of an EL expression as the condition, work great in many scenarios, but not in all. For instance, say you want to conditionally add some content depending on the time of day. You could create your own bean with Boolean properties suitable for use in an EL expression, but a custom action like this may be more convenient to use:
<xmp:ifAfternoon>
Sorry, we only accept delivery requests before noon.
</xmp:ifAfternoon>
It's very easy to implement such a custom action, thanks to the extendable JSTL classes. Here's the complete tag handler code for the <xmp:ifAfternoon> action:
package com.ora.jstl;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.servlet.jsp.jstl.core.ConditionalTagSupport;
public class IfAfternoonTag extends ConditionalTagSupport {
public boolean condition() {
Calendar now = new GregorianCalendar();
return now.get(Calendar.AM_PM) == Calendar.PM;
}
}
The tag handler class extends the JSTL javax.servlet.jsp.jstl.core.ConditionalTagSupport class, which provides implementations for all standard JSP tag handler methods and calls the condition() method implemented by the subclass to decide what to do. It also includes setter methods for var and scope attributes, so a subclass like the one shown here behaves just like the <c:if> action: if the condition() method returns true, the custom action's body is processed; if it doesn't have a body, the value can be saved as a Boolean in variable and scope specified by the var and scope attributes.
You may expect there to be a similar base class for developing custom actions to be used, the same as a <c:when> action within a <c:choose> block, but there isn't. The reason is this: to ensure that custom actions cannot interfere with the somewhat complex interaction between the <c:choose> action and its nested actions, only <c:when> and <c:otherwise> actions are allowed as direct children of the <c:choose> action. But you can combine your own conditional actions with a <c:choose> like this to get the same multiple-choice effect:
<xmp:ifAfternoon var="isAfternoon" />
<c:choose>
<c:when test="${isAfternoon}">
Good day!
</c:when>
<c:otherwise>
Good morning!
</c:otherwise>
</c:choose>
Simply save the custom conditional result, using the var attribute, and then use this result in the EL expression for a <c:when> action in the block.
Developing JSTL-Style Iteration Custom Actions
Developing a custom iteration action can also be simplified by extending a JSTL base class, and custom actions nested within a JSTL <c:forEach> action body have easy access to iteration status information through a JSTL interface.
Let's look at a custom iteration action first. The JSTL base class you can extend is javax.servlet.jsp.jstl.core.LoopTagSupport. All you really need to implement in the subclass are three methods: prepare(), hasNext(), and next(). This gives you iteration plus support for the same var and varStatus attributes as the JSTL <c:forEach> action. If you want to support the begin, end, and step attributes, the base class provides protected fields and validation methods, but you have to implement the setter methods yourself (since not all subclasses need them, and the details differ, depending on if EL expressions are allowed or not).
To see how you can extend the JSTL base class for your own iteration action, let's develop a custom action that iterates through all days in the current month. The tag handler looks like this:
package com.ora.jstl;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.servlet.jsp.jstl.core.LoopTagSupport;
public class ForEachDayTag extends LoopTagSupport {
private Calendar calendar;
private int previousMonth;
public void prepare() {
calendar = new GregorianCalendar();
calendar.set(Calendar.DAY_OF_MONTH, 1);
// Set to last day in previous month, since next() increments it
calendar.add(Calendar.DAY_OF_MONTH, -1);
previousMonth = calendar.get(Calendar.MONTH);
}
public boolean hasNext() {
int currentMonth = calendar.get(Calendar.MONTH);
int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
int lastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
return currentMonth == previousMonth || currentDay < lastDay;
}
public Object next() {
calendar.set(Calendar.DAY_OF_MONTH,
calendar.get(Calendar.DAY_OF_MONTH) + 1);
return calendar;
}
}
The base class calls prepare() once, followed by a sequence of calls to hasNext() and next() until hasNext() returns false. The subclass code is pretty straight forward. The prepare() method creates a GregorianCalendar instance and sets it to the last day of the previous month. The hasNext() method returns true if the day currently represented by the calendar is either a day in the previous month (i.e., before the first iteration) or a day other than the last day of the current month. The next() method, finally, moves the calendar to the next day and returns the adjusted calendar.
Here's an example of how you can use this custom iterator to generate an HTML table with a cell for each day in the current month:
<table>
<xmp:forEachDay var="curr">
<tr>
<td>
<fmt:formatDate value="${curr.time}"
pattern="EE dd, MMM yyyy" />
</td>
</tr>
</xmp:forEachDay>
</table>
It would be fairly easy to extend this custom action to support the begin, end, and step attributes, and maybe an attribute for setting the month to iterate over. I leave that as an exercise for you to try out on your own.
Using JSTL Iteration Status Info
What if you want to do things only for certain items in the body of an iteration action? The JSTL <c:forEach> action and custom actions extending the LoopTagSupport base class expose information about the current item through a variable named by the varStatus attribute. This variable is an instance of a bean with properties like first, last, index, and more (see the JSTL specification for details). For instance, you can use it like this to get alternating colors for the rows in a table:
<table>
<xmp:forEachDay var="curr" varStatus="stat">
<c:set var="bg" value="white" />
<c:if test="${stat.index % 2 == 0}">
<c:set var="bg" value="blue" />
</c:if>
<tr bgcolor="<c:out value="${bg}" />">
<td>
<fmt:formatDate value="${curr.time}"
pattern="EE dd, MMM yyyy" />
</td>
</tr>
</xmp:forEachDay>
</table>
Sometimes it's impossible to use an EL expression testing the status bean properties (or the current item itself) to figure out if special processing is needed or not. With the calendar iterator, for instance, you can't use an EL expression to find out what day in the week the current item represents. This is where a custom action specifically intended for use within an iterator action body can come in handy.
A custom action can use the knowledge that a JSTL iterator action implements the javax.servlet.jsp.jstl.core.LoopTag interface to get access to the current item and the iteration status iformation. Here's the tag handler code for a custom action that processes its body only if the current item represents a Sunday:
package com.ora.jstl;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.jstl.core.ConditionalTagSupport;
import javax.servlet.jsp.jstl.core.LoopTag;
public class IfSundayTag extends ConditionalTagSupport {
public boolean condition() throws JspTagException {
LoopTag parent =
(LoopTag) findAncestorWithClass(this, LoopTag.class);
if (parent == null) {
throw new JspTagException("ifSunday must be used in loop");
}
Calendar current = (Calendar) parent.getCurrent();
return current.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY;
}
}
The LoopTag interface declares two methods: getCurrent() returns the current iteration item as an Object and getLoopStatus() returns an instance of LoopStatus (the same type as for the object exposed as the varStatus variable). The interface is implemented by the LoopTagSupport base class, so all tag handlers that extend this class get the correct behavior for free.
In this example tag handler, the parent that implements the LoopTag interface (our ForEachDayTag tag handler) is located using the findAncestorWithClass() method and the current item is retrieved by calling the parent's getCurrent() method. If the current item represents a Sunday, the condition() method returns true. With this custom action, it's easy to do whatever you want with Sundays:
<table>
<xmp:forEachDay var="curr">
<c:set var="bg" value="white" />
<xmp:ifSunday>
<c:set var="bg" value="red" />
</xmp:ifSunday>
<tr bgcolor="<c:out value="${bg}" />">
<td>
<fmt:formatDate value="${curr.time}"
pattern="EE dd, MMM yyyy" />
</td>
</tr>
</xmp:forEachDay>
</table>
A custom action that needs to do something only for the first or last iteration, or perhaps only for every second or third iteration, can use the getLoopStatus() method to get the information it needs.
Using JSTL Classes to Produce Localized Text
There's one more JSTL class that you may find useful when you develop custom actions: the javax.servlet.jsp.jstl.fmt.LocaleSupport class. This class provides methods for getting localized messages from a ResourceBundle, using the same algorithms as the JSTL i18n actions for determining the appropriate locale (as I described in part 2 of this article series).
The class provides the following methods:
public static String getLocalizedMessage(PageContext pc,
String key);
public static String getLocalizedMessage(PageContext pc, String key,
String basename);
public static String getLocalizedMessage(PageContext pc, String key,
Object[] args);
public static String getLocalizedMessage(PageContext pc, String key,
Object[] args, String basename);
The first two methods get a simple localized message for the specified key. The second method uses the specified basename to locate the correct ResourceBundle, while the first one uses the bundle selected for the current localization context. The second pair of methods are for parameterized messages, using the args parameter to set the message parameters.
Conclusion
If you've read all parts of this article series, you have a glimpse of what JSTL 1.0 has to offer, whether you're a page author or a programmer. I've covered all features except the JSTL XML processing tag library; it works pretty much the same as the other libraries and if you know XML and XPath, I'm sure you can figure out how to use it on your own. If you don't know XML and XPath, that's where you need to start, and I'm afraid that's out of scope for this article.
While you can get an idea about the possibilities from reading an article, the only way to really learn how to use a technology is to do just that: use it! The Resouce section gives you some pointers to where you can find out more about JSTL and where to ask questions. I hope you'll find JSTL both fun and useful.
Resources