Professional Documents
Culture Documents
01 Jun 2004
Interested in adding custom tags to your JavaServer Pages (JSP) applications? This
tutorial will show you how to use these tags to write custom actions that are similar to
those built in to JSP technology, such as jsp:useBean, jsp:getProperty, and
jsp:forward. Learn how you can extend the JSP syntax with your very own custom
actions that are specific to your domain's presentation logic.
The ability to add custom tags to JSP applications will allow you to focus your efforts
on a document-centric approach to development. You can keep Java code out of
your JSP pages, which makes those pages easier to maintain. (I know from
experience, when too much Java code is put into a JSP page it can be a code
maintenance nightmare.) This tutorial will get you developing custom tags in no time.
And once you learn the benefits of JSP custom tag development, you may be
surprised that programmers don't use them more often.
In this tutorial, I'll cover the basics of using custom tags. You'll learn how to use
custom tags to create reusable presentation components and avoid introducing Java
scriptlets into your JSP pages.
This tutorial assumes that you are familiar with the Java platform, JavaServer Pages
(JSP) technology, the MVC pattern, the Reflection API, Model 2, and, optionally, the
Struts framework. In addition, you'll need a good background in using custom tag
libraries to get the most out of this tutorial. (See Resources for more information.)
Tag handlers
Before you can create a custom tag, you need to create a tag handler. A tag handler
is a Java object that performs the action of a custom tag. When you use a custom
tag, you import a tag library -- that is, a list of tag/tag handler pairs. You import a
library by declaring it in the Web application deployment descriptor, then importing it
into a JSP page using the taglib directive. If the JSP container encounters the use
At runtime, the servlet generated by the JSP page gets an instance of the tag
handler class corresponding to the tag used in the page. The generated servlet
initializes the tag handler with any attributes that you pass.
The tag handler implements life cycle methods. The generated servlet uses these
methods to inform the tag handler that the custom tag's action should start, stop, or
be repeated. The generated servlet calls these life cycle methods to perform the
functionality of the tag.
Types of tags
You can define two types of tags:
• javax.servlet.jsp.tagext.Tag
• javax.servlet.jsp.tagext.BodyTag
Tags that operate on their body -- that is, on the material between the opening and
closing tags -- must implement the BodyTag interface. In this tutorial, we'll refer to
these tags as body tags. We'll call tags that do not operate on their body simple
tags. Simple tags can implement the Tag interface, though they aren't required to do
so. Keep in mind that a tag that does not operate on its body still has a body;
however, its tag handler cannot read that body.
There's a problem, though: If you want to pass a list of request parameters (a query
string, for instance), you may have to create a Java scriptlet to do so. What a mess!
The following listing (search_results.jsp) illustrates a JSP page that by necessity
The search_results.jsp creates a hashmap and passes that map two properties. In
the next few sections, we are going to create a custom tag that does this work
without Java code. Our tag will define a hashmap as follows:
<map:mapDefine id="deleteParams">
<map:mapEntry id="id" name="cd" property="id"/>
<map:mapEntry id="method" value="delete"/>
</map:mapDefine>
<!-- Pass the map named deleteParams to html:link to generate
the request parameters-->
<html:link action="/deleteCD"
name="deleteParams">delete </html:link> </font></td>
This example will demonstrate several key concepts, including working with nested
tags and defining scriptlet variables. First I'll explain how this simple tag works, then
you'll build on these concepts in later sections, and learn how to write variations of
this tag that work with their bodies and control execution flow.
The tag will instantiate a map. A developer using the tag can specify the type of map
that will be instantiated -- a HashMap, TreeMap, FastHashMap, or FastTreeMap.
The FastHashMap and FastTreeMap are from the Jakarta Commons Collection
library (see Resources for a link). The developer will also be able to specify the
scope -- whether it be page, request, session, or application scope -- into which the
tag should be placed.
In order to build this simple tag, we will have to walk through the following steps:
4. Define attributes in the TLD file that correspond to the attributes defined in
the tag handler Java class.
If you're like me, you like to read the end of the book long before you get to it, so
check the Appendix for the complete listing of our tag handler class to see how this
process is going to end up.
Over the course of the next few sections, we will look at the implementation of the
MapDefineTag and see how we can get to this point.
Method Purpose
int doStartTag() throws JspException Process the start tag
TagSupport
Now, you don't have to implement the Tag interface directly; instead, your
map-defining tag will subclass the TagSupport class. This class implements the Tag
interface with sensible default methods, thus making it easier to develop custom
tags. (See Resources for a link to TagSupport's API documentation.) For example,
the TagSupport class defines get/setParent() and setPageContext(), which
would be nearly the same for all tag handlers The get/setParent() methods
allow tags to be nested. The TagSupport class also defines a pageContext
instance variable (protected PageContext pageContext) that can be used by
subclasses; this variable is set by the setPageContext() method.
Without further ado, let's implement the Tag interface by subclassing TagSupport as
follows:
...
import javax.servlet.jsp.tagext.TagSupport;
...
public class MapDefineTag extends TagSupport {
...
Now that we have defined the tag handler, we'll need to add the mapping from the
handler to the tag in the TLD file. We'll tackle this in the next sections. Later, we'll fill
in the rest of the code for MapDefineTag.
A TLD file has a preamble, where we identify the version of JSP technology and tag
libraries that we are using. This preamble usually looks something like this:
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>map</short-name>
• The root element the TLD file is taglib. A taglib describes a tag
library -- that is, a list of tag/tag handler pairs.
• The tlib-version and short-name elements are required in this
example, because we are using JSP version 1.2.
• The tlib-version element corresponds to the tag libraries version.
• The jsp-version corresponds to the version of JSP technology on
which our tag library depends.
• The short-name element defines a simple name for the tag library that
can be used by IDEs and other development tools.
• The taglib element contains many tag elements, one for each tag in
the tag library.
Because we just created our class, we will go ahead and declare that class in the
TLD file, as follows:
<taglib>
...
<tag>
<name>mapDefine</name>
<tag-class>trivera.tags.map.MapDefineTag</tag-class>
<body-content>JSP</body-content>
...
The tag element is used to map custom tags to their custom tag handlers. The tag
element in the listing above maps the custom tag mapDefine to the handler
trivera.tags.map.MapDefineTag. Thus, whenever the translation engine runs
across mapDefine, it will invoke trivera.tags.map.MapDefineTag.
Now that we have defined the tag in the TLD, we'll next define some attributes for
that tag in the tag handler class.
Attribute Description
id The name of the new scriptlet variable.
scope The scope where we will put the new scriptlet
variable.
type The type the new scriptlet variable will be
(HashMap, FastHashMap, TreeMap, or
FastTreeMap.) If type is set to hash, then a
HashMap will be created. If type is set to
fasthash, then a FastHashMap will be
created.
When we use our tag in a JSP page, it will look like this:
...
private String type = FASTTREE;
private String id;
private String scope;
The translation engine will set the properties of our tag with either hardcoded
configuration data or runtime expressions. We'll discuss this in more detail in Step 4:
Define attributes in the TLD file.
In Step 5: Implementing the doStartTag() method, we'll use these attributes in our
tag handler's doStartTag() method.
<tag>
<name>mapDefine</name>
<tag-class>trivera.tags.map.MapDefineTag</tag-class>
<body-content>JSP</body-content>
...
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
<description>The id attribute</description>
</attribute>
<attribute>
<name>scope</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
<description>The scope attribute</description>
</attribute>
<attribute>
<name>type</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
<description>
Specifies the type of map valid values are fasttree, fasthash, hash, tree
</description>
</attribute>
</tag>
The name element specifies the name of the attribute. The required element
specifies whether the attribute is required (the default value is false). The
rtexprvalue element indicates whether the attribute is hardcoded based on the
value at translation time or if it allows runtime scriptlet expressions.
Remember, the MapDefineTag class must define a JavaBean property for each
attribute described above; we took care of this in Step 3: Create attributes in the tag
handler Java class.
/** Based on the type attribute, determines which type of Map to create */
if (type.equalsIgnoreCase(FASTTREE)) {
map = new FastTreeMap();
} else if (type.equalsIgnoreCase(HASH)) {
map = new HashMap();
} else if (type.equalsIgnoreCase(TREE)) {
map = new TreeMap();
} else if (type.equalsIgnoreCase(FASTHASH)) {
map = new FastHashMap();
}
Next, the id and scope attributes are used to set the hashmap into a given scope
with a given name:
...
if (scope == null){
pageContext.setAttribute(id, map);
}else if("page".equalsIgnoreCase(scope)){
pageContext.setAttribute(id, map);
}else if("request".equalsIgnoreCase(scope)){
pageContext.getRequest().setAttribute(id, map);
}else if("session".equalsIgnoreCase(scope)){
pageContext.getSession().setAttribute(id, map);
}else if("application".equalsIgnoreCase(scope)){
pageContext.getServletContext().setAttribute(id, map);
return EVAL_BODY_INCLUDE;
}
If the scope attribute is null, then the map will be put into page scope. Otherwise,
the parameter will be put into the scope corresponding to the scope name passed
via the scope attribute.
So far, we have a pretty simple tag that has three attributes: id, scope, and type.
We've put the map into a scope with a given name. One thing we haven't done yet,
however, is declare a scriptlet variable. In order to create to do so, we need to add
one more entry to the TLD, and that's what we'll do in the next section.
<variable>
<name-from-attribute>id</name-from-attribute>
<variable-class>java.util.Map</variable-class>
<scope>AT_BEGIN</scope>
</variable>
The above code snippet is placed in the TLD file after the body-content element
and before the attribute elements. Under the variable element, we have
declared three sub-elements: name-from-attribute, variable-class, and
scope. name-from-attribute specifies that the value of the id attribute is the
name of the scriptlet variable that the translation engine will define.
variable-class is the class type of the variable that the translation will define.
scope specifies when the variable will be available: it can be nested inside of the
body of the tag (NESTED), after the end of the tag (AT_END), or from the beginning of
the tag (AT_BEGIN). For scope, we opted for AT_BEGIN, which means that the
variable will be available from the start of the tag to the end of the current JSP page.
Now you have a sense of how a simple custom tag is put together. In the next
section, we'll look at our tag's life cycle methods to understand what happens as our
JSP page is actually run.
Diagram
The diagrams did not make much sense to me, but the generated code did, so we'll
take a look at that code in this section. (The diagrams make sense to me now, but I
found the code much more revealing; you might want to examine that diagram again
when we're done with this section.)
As you may recall, JSP pages are really servlets in disguise. Your JSP files are
translated into servlets before they are used. Here is a small JSP page, called
testMapDefine.jsp, that we will use to demonstrate and test MapDefineTag, the tag
we developed in the previous section:
<map:mapDefine id="employee">
<br />
The employee is <%=employee%> <br />
<%
employee.put("firstName", "Kiley");
employee.put("lastName", "Hightower");
employee.put("age", new Integer(33));
employee.put("salary", new Float(22.22));
%>
The employee is <%=employee%> <br />
</map:mapDefine>
</body>
</html>
Notice that this page imports a custom tag with the URI of map. It does this because
we declared the TLD in the web.xml file as follows:
<web-app>
...
<taglib>
<taglib-uri>map</taglib-uri>
<taglib-location>/WEB-INF/tlds/map.tld</taglib-location>
</taglib>
...
There are other ways to import tags. I find this method the most useful, as it allows
you to assign a short name for the URI, and short names are easy to remember.
Notice that the taglib is described in the context of its TLD file. Our map.tld file,
located in the WEB-INF directory, looks like this:
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>map</short-name>
<tag>
<name>mapDefine</name>
<tag-class>trivera.tags.map.MapDefineTag</tag-class>
<body-content>JSP</body-content>
...
When the JSP translator runs into the use of the custom tag mapDefine, it will look
up the TLD file based on the taglib directive and the TLD file specified in the
web.xml file. It then adds code like the following to the generated servlet:
employee.put("firstName", "Kiley");
employee.put("lastName", "Hightower");
employee.put("age", new Integer(33));
employee.put("salary", new Float(22.22));
Notice that the generated servlet has a local variable called employee, based on
the id attribute of the tag being set to employee. Thus, the translation engine
defines a local variable called employee at translation time, not at runtime. This is a
concept that confuses a lot of novices.
The MapDefineTag defines a getMap() method that returns the newly created
map. The nested MapEntryTag in doEndTag() uses MapDefineTag's getMap()
method to add a value to the map, as follows:
...
/* Instantiate a new String, Integer or Float based on the type. */
if (type.equals("java.lang.String")) {
objectValue = value;
} else if (type.equals("java.lang.Integer")) {
Integer intValue = Integer.valueOf(value);
objectValue = intValue;
} else if (type.equals("java.lang.Float")) {
Float floatValue = Float.valueOf(value);
objectValue = floatValue;
}
Based on the above code, and assuming that we add the necessary entries to the
TLD file, we could use our tag like this:
<map:mapDefine id="employee">
The above listing would define an employee map with three entries apiece for
firstName, lastName, age, and salary, of types String, String, Integer,
and Float, respectively.
String test="Jenny";
public String getTest() {
return test;
}
Our rewritten tag can use the bean property as an entry into the map, like this:
<map:mapDefine id="employee2">
Notice that the firstName entry is now defined with the bean's test property.
To accomplish this, we need to add Reflection in our custom tag, like so (follow the
comments in the code to see how things were changed):
if (type.equals("java.lang.String")) {
objectValue = value;
} else if (type.equals("java.lang.Integer")) {
Integer intValue = Integer.valueOf(value);
objectValue = intValue;
} else if (type.equals("java.lang.Float")) {
Float floatValue = Float.valueOf(value);
objectValue = floatValue;
}
/* If it is not a simple entry,
then use reflection to get the property from the bean */
}else {
if (scope == null){
bean = pageContext.findAttribute(name);
}else if("page".equalsIgnoreCase(scope)){
bean = pageContext.getAttribute(name);
}else if("request".equalsIgnoreCase(scope)){
bean = pageContext.getRequest().getAttribute(name);
}else if("session".equalsIgnoreCase(scope)){
bean = pageContext.getSession().getAttribute(name);
}else if("application".equalsIgnoreCase(scope)){
bean = pageContext.getServletContext().getAttribute(name);
}
/* If the property attribute is null,
then just use the bean as the entry*/
if (property==null){
objectValue = bean;
mapDef.getMap().put(id,bean);
try{
String propertyMethod = "get" +
property.substring(0,1).toUpperCase() +
property.substring(1, property.length());
mapDef.getMap().put(id,objectValue);
return EVAL_PAGE;
}
This seems like a lot of work just to achieve some functionality that a lot of tags need
to do anyway. Fortunately for us, there is a library that makes this type of
development easier. See how this library -- Struts -- can help in the next section.
Instead of using Reflection to achieve the results we saw on the last section, you
if (value !=null){
if (type.equals("java.lang.String")) {
objectValue = value;
} else if (type.equals("java.lang.Integer")) {
Integer intValue = Integer.valueOf(value);
objectValue = intValue;
} else if (type.equals("java.lang.Float")) {
Float floatValue = Float.valueOf(value);
objectValue = floatValue;
}
}else {
/** THIS USED TO BE 30 LINES OF CODE */
objectValue = RequestUtils.lookup(pageContext, name, property, scope);
}
mapDef.getMap().put(id,objectValue);
return EVAL_PAGE;
}
Introduction
You can write tag handlers that manipulate their body content. Remember, a tag's
body content is the data that appears in the JSP page between the opening and
closing tag of the custom tag. Tags that manipulate their bodies are called body
tags. Body tag handlers are more complex to write than handlers for simple tags.
Note: Remember, simple tags can have bodies too. The only difference is that
simple tags can not read or manipulate their bodies.
To write a body tag handler, you need to implement the BodyTag interface.
BodyTag implements all the methods implemented by Tag (see Step 1: Create a
tag handler that implements the Tag interface for the details), along with two extra
methods that deal with body content:
Method Purpose
void setBodyContent(BodyContent b) Setter method for the bodyContent property.
void doInitBody() Prepares for evaluation of body. Called once per
tag invocation, after a new BodyContent is
obtained and set via setBodyContent(). Not
invoked if there is no body content requested
because doStartTag() has returned
EVAL_BODY_BUFFERED.
Just as the Tag interface has the TagSupport class, the BodyTag interface has the
BodyTagSupport class. Thus, your body tag handlers only need to override the
methods that they will use. The BodyTagSupport class subclasses TagSupport
and implements the BodyTag interface. This makes it easier to write body tag
handlers. BodyTagSupport defines get/setBodyContent() and a protected
bodyContent instance variable.
BodyContent is a buffer that holds the runtime results of the evaluation of the
body. BodyContent subclasses JspWriter and acts as the implicit out for the
body of the tag. Thus, the JSP container creates an instance of BodyContent, and
as it processes the body content of the tag, it writes to this instance instead to the
root JspWriter. Thus, when you use the implicit object out inside of a tag, you are
really using a BodyContent object, not the JspWriter. (The JspWriter is the
implicit out for the page.)
You can get the evaluated body as a String from BodyContent. BodyContents
are created at runtime by the container calling the pushBody() and popBody()
methods of the pageContext for the page. (pushBody() is called only if
doStartTag() returns EVAL_BODY_BUFFERED.) Thus, BodyContents are in a
nested structure of JspWriters and BodyContents. (The outer out can be
another BodyContent object, because a BodyContent is a JspWriter.) The
BodyContent is given to the body tag handler through the setBodyContent()
method. The body tag handler is passed a BodyContent instance (via
setBodyContent()) and can decide what to do with it. It may process it further,
discard it, send it to the browser, etc.
Enough background; let's look at the code! We'll look at a simple body tag example
in the next few sections.
Keeping with our hashmap theme, we will write a tag that parses a string that looks
like this:
{
firstName=Jennifer,
lastName=Wirth,
age=25
}
Essentially, our tag will read this kind of string and convert it to a Map
(java.util.Map). We will call our new tag map. Here is an example of our new tag
in action:
<map:map id="employee">
{
firstName=Jennifer,
lastName=Jones,
age=25
}
</map:map>
<br />
The employee map is <%=employee%> <br />
</html>
The code above creates a map called employee with three entries: firstName,
lastName, and age.
this.pageContext.setAttribute(id,map);
return SKIP_BODY;
}
Simple enough! Now that we have covered tags that work on their bodies, let's cover
tags that implement execution flow in detail.
Overview
At times, you may want to conditionally invoke the body of a method based on some
application-specific display logic. This can be done by returning a value from
doStartTag(): SKIP_BODY to skip the body of the tag, or EVAL_BODY to evaluate
it.
Keeping with our map theme, in the next few sections we'll create a custom tag
handler that iterates over a collection of maps and prints out their values.
<map:map id="employee1">
{
firstName=Jennifer,
lastName=Wirth,
age=33
}
</map:map>
<map:map id="employee2">
{
firstName=Kiley,
lastName=McKeon,
age=27
}
</map:map>
Next, let's create a collection called list and add the two maps (employee1 and
employee2) defined above to it.
list.add(employee1);
list.add(employee2);
%>
Now, here is how we would use a custom tag to iterate over our collection:
<map:printMaps name="list">
<table border="1">
<tr>
<td>First Name</td>
<td> ${firstName} </td>
<tr>
<td> Last Name </td>
<td> ${lastName} </td>
<tr>
<td>Age</td>
<td> ${age} </td>
</table>
</map:printMaps>
The map:printMaps tag iterates over the list collection. On each iteration, it
uses its body, searching for keys into the map for the current iteration by searching
for substrings that start with ${key}. It parses the key out of the ${} string and
replaces it with the value of the that key from the map -- that is, with
map.get(key).
if (iter.hasNext() == false) {
return SKIP_BODY;
} else {
If the string from the tokenizer starts with ${, doAfterBody(), it then extracts the
key from the string, and uses the key to look up the value in the map. It converts the
value from the map into a string by calling the toString() method of the value
object and appending the results to the buffer. The following listing illustrates how
this process works.
Once the doAfterBody() method constructs the output buffer, it outputs it to the
previous out, like so:
try {
this.getPreviousOut().print(buffer.toString());
} catch (IOException e) {
throw new JspException(e);
}
Thus, each iteration processes the body and outputs it to the previous out. If the
previous out is the root JspWriter, then it will be written to the browser.
Summary
With the completion of this tutorial, you now have a deeper understanding of custom
tags. If you completed this tutorial, and you are new to custom tags, then you should
be ready to start using this powerful tool on your current project. You will no longer
accept mediocre Java scriptlets strewn about your JSP pages in an unmaintainable
and unreusable manner. And even if you're not planning on creating your own
custom tags, this tutorial will help you understand custom tags that you might
encounter in code written by other developers.
Section 8. Appendix
package trivera.tags.map;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;
import org.apache.commons.collections.FastHashMap;
import org.apache.commons.collections.FastTreeMap;
/**
*
* <p>
* Created to demonstrate the creation of custom tag handlers.
* Created by Rick Hightower from Trivera Technologies.
* </p>
* @jsp.tag name="mapDefine"
* body-content="JSP"
*
* @jsp.variable name-from-attribute="id" class="java.util.Map"
* scope="AT_BEGIN"
*/
public class MapDefineTag extends TagSupport {
/* (non-Javadoc)
* @see javax.servlet.jsp.tagext.Tag#doStartTag()
*/
public int doStartTag() throws JspException {
if (type.equalsIgnoreCase(FASTTREE)) {
map = new FastTreeMap();
} else if (type.equalsIgnoreCase(HASH)) {
map = new HashMap();
} else if (type.equalsIgnoreCase(TREE)) {
map = new TreeMap();
} else if (type.equalsIgnoreCase(FASTHASH)) {
map = new FastHashMap();
}
if (scope == null){
pageContext.setAttribute(id, map);
}else if("page".equalsIgnoreCase(scope)){
pageContext.setAttribute(id, map);
}else if("request".equalsIgnoreCase(scope)){
pageContext.getRequest().setAttribute(id, map);
}else if("session".equalsIgnoreCase(scope)){
pageContext.getSession().setAttribute(id, map);
}else if("application".equalsIgnoreCase(scope)){
pageContext.getServletContext().setAttribute(id, map);
}
return EVAL_BODY_INCLUDE;
}
/**
* @param string
*/
public void setType(String string) {
type = string;
}
/**
* @param string
*/
public void setId(String string) {
id = string;
}
/**
* @param string
*/
public void setScope(String string) {
scope = string;
}
Resources
Learn
• Check out the J2EE API documentation for Tag and TagSupport.
• If you are looking for an application server to run these examples on, try IBM
WebSphere Express.
• Need to get up to speed on JSP custom tags, servlets, filters and more? Try this
online course from IBM: Servlet and JSP Development using WebSphere
Studio V5.
• For more on this topic, read "Take control of your JSP pages with custom tags,"
Jeff K. Wilson (developerWorks, January 2002).
• For more on taglibs, check out "JSP best practices: Intro to taglibs," Brett
McLaughlin (developerWorks, July 2003).
• For more on Struts, check out the Struts home page at the Apache Foundation.
Get products and technologies
• You can find the Common Collection Library at the Commons Collections Web
page.
Discuss
• Participate in the discussion forum for this content.