The <c:forEach> and <c:forTokens> actions should be sufficient for most of your iteration needs, but you may find that you need a custom action that provides more functionality than those actions or a custom action that simplifies their use. JSTL provides support for two types of custom iteration actions:
Each of those types of custom actions is discussed in the following sections. Collaboration Custom ActionsCollaboration custom actions work in concert with existing iteration actions. Typically, collaboration custom actions provide functionality related to the status of an iteration; for example, a custom action that implements some functionality for the first round of an iteration. The <c:forEach> and <c:forTokens> actions are implemented with tag handlers that implement the LoopTag interface; the methods defined by that interface are listed in Table 4.3. Table 4.3. LoopTag Methods
Collaboration custom actions obtain a reference to the tag handler for their enclosing <c:forEach> or <c:forTokens> action and access the current object in the iteration or the iteration's status by invoking that tag handler's LoopTag methods listed in Table 4.3. In "The <c:forTokens> Action" on page 166 we discussed how you can use <c:forTokens> to create an HTML table from a single string delimited with brackets and commas. That example, which is listed in its entirety in Listing 4.5 on page 168, looked something like this: <table border='1'> <c:forTokens var='tableData' varStatus='status' items='${dataForSimpleTable}' delims='[]'> <c:choose> <c:when test='${status.first}'> ... </c:when> <c:otherwise> ... </c:otherwise> </c:choose> </c:forTokens> </table> In Listing 4.5, represented by the preceding code fragment, we created the table headers during the first round of the iteration and created the rest of the table's data during the subsequent rounds of the iteration. That required us to differentiate between the first round of the iteration and the subsequent rounds, as illustrated by the preceding code fragment. That differentiation required us to use the <c:forTokens> varStatus attribute to access a status object and to use that status object with the <c:choose>, <c:when>, and <c:otherwise> actions. If we implement two custom actions, one that detects the first round of an iteration and another that detects all other rounds, we can significantly reduce that complexity, like this: <table border='1'> <c:forTokens var='tableData' items='${dataForSimpleTable}' delims='[]'> <core-jstl:firstRound> ... </core-jstl:firstRound> <core-jstl:notFirstRound> ... </core-jstl:notFirstRound> </c:forTokens> </table> Listing 4.8 lists a revision of Listing 4.5 on page 168 that uses the custom actions described above. Listing 4.8 Using a Collaborative Custom Action<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Testing Iteration Custom Actions</title> </head> <body> <%@ taglib uri='http://java.sun.com/jstl/core' prefix='c' %> <%@ taglib uri='WEB-INF/core-jstl.tld' prefix='core-jstl'%> <% // This string, which could ultimately come from a data // source such as a flat file, could be stored in // request scope (or any other scope) by a business // component. The page author just needs to know the // name of the corresponding scoped variable and // the delimiters. // The first token delimited with the characters [] // represents table column names. The rest of the tokens // represent table data. String s = "[Name, Age, Sex, Street Address]" + "[Phyllis Keeney, 62, Female,123 Cherry Lane]" + "[George Wilson, 24, Male, 16 Rue Florence]" + "[Lynn Seckinger, 36, Female, 2096 Oak St]" + "[Roy Martin, 44, Male, 29419 112th Ave]" + "[William Bates, 96, Male, 800 Birch St]" + "[Kathy Giuseppi, 13, Female, 1245 Genesee St]"; // Store the string in page scope pageContext.setAttribute("dataForSimpleTable", s); %> <p><table border='1'> <%-- For each token in the string delimited with [] --%> <c:forTokens var='tableData' delims='[]' items='${dataForSimpleTable}'> <%-- This custom action includes its body content if it's the first round of the iteration --%> <core-jstl:firstRound> <tr> <c:forEach var='tableHeader' items='${tableData}'> <th><c:out value='${tableHeader}'/></th> </c:forEach> </tr> </core-jstl:firstRound> <%-- This custom action includes its body content if it's NOT the first round of the iteration --%> <core-jstl:notFirstRound> <tr> <c:forEach var='rowData' items='${tableData}'> <td><c:out value='${rowData}'/></td> </c:forEach> </tr> </core-jstl:notFirstRound> </c:forTokens> </table> </body> </html> The tag handler for the <core-jstl:firstRound> custom action is listed in Listing 4.9. The tag handler shown in Listing 4.9 uses the findAncestorWithClass method from javax.servlet.jsp. tagext .TagSupport to obtain a reference to an enclosing action, such as <c:forEach> or <c:forTokens>, that implements the LoopTag interface. If no ancestor action fits that requirement, the preceding tag handler throws an exception. If the ancestor action is found, the tag handler accesses the iteration status by invoking that ancestor's getLoopStatus method, which returns an object whose type is LoopTagStatus . That status object is subsequently used to determine whether the current round of iteration is the first; if so, the body of the action is evaluated; if not, the action's body is skipped . The tag handler for the <core-jstl:notFirstRound> action is listed in Listing 4.10. The preceding tag handler is almost identical to the tag handler listed in Listing 4.9 except the preceding tag handler evaluates its body content if the current round of iteration is not the first round. Note The common functionality implemented by the two preceding tag handlers could be encapsulated in a base class, thereby reducing the amount of code that needs to be written and making maintenance of those tag handlers easier. In the interest of simplicity, the two preceding tag handlers do not share that common base class. Listing 4.9 WEB-INF/classes/tags/FirstRoundAction.javapackage tags; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.TagSupport; import javax.servlet.jsp.jstl.core.LoopTag; import javax.servlet.jsp.jstl.core.LoopTagStatus; public class FirstRoundAction extends TagSupport { public int doStartTag() throws JspException { Class klass = javax.servlet.jsp.jstl.core.LoopTag.class; LoopTag ancestor = (LoopTag)findAncestorWithClass( this, klass); if(ancestor != null) { LoopTagStatus status = ancestor.getLoopStatus(); if( status.isFirst() ) return EVAL_BODY_INCLUDE; } else { throw new JspException("This tag can only be nested " + "in the body of a tag that implements " + "javax.servlet.jsp.jstl.core.LoopTag"); } return SKIP_BODY; } } The tag library containing the two custom actions used in Listing 4.8 on page 180 are declared in the tag library descriptor (TLD) listed in Listing 4.11. The preceding TLD declares the <core-jstl:firstRound> and <core-jstl:notFirstRound> actions and their associated tag handlers. That TLD is referenced with a taglib directive in Listing 4.8 on page 180. Iteration Custom ActionsIn addition to implementing collaboration custom actions as described in "Collaboration Custom Actions" on page 178, you can also implement custom actions that iterate by implementing the LoopTag interface. The easiest way to do that is to extend the LoopTagSupport class which implements the LoopTag interface and provides a number of protected variables and methods that greatly simplify implementing custom iteration actions. Table 4.4 lists the LoopTagSupport protected variables. Listing 4.10 WEB-INF/classes/tags/NotFirstRoundAction.javapackage tags; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.TagSupport; import javax.servlet.jsp.jstl.core.LoopTag; import javax.servlet.jsp.jstl.core.LoopTagStatus; public class NotFirstRoundAction extends TagSupport { public int doStartTag() throws JspException { Class klass = javax.servlet.jsp.jstl.core.LoopTag.class; LoopTag ancestor = (LoopTag)findAncestorWithClass( this, klass); if(ancestor != null) { LoopTagStatus status = ancestor.getLoopStatus(); if( !status.isFirst() ) return EVAL_BODY_INCLUDE; } else { throw new JspException("This tag can only be nested " + "in the body of a tag that implements " + "javax.servlet.jsp.jstl.core.LoopTag"); } return SKIP_BODY; } } The protected variables listed above give you direct access to the begin , end , step , var ( itemId ), and varStatus ( statusID ) attributes. You can also find out whether the begin , end , and step attributes were specified with the beginSpecified , endSpecified , and stepSpecified variables. The LoopTagSupport class defines three abstract methods that subclasses must implement; those methods are listed in Table 4.5. [5]
The methods listed in Table 4.5 are always called in the order they are listed in Table 4.5. The prepare method is called by the LoopTagSupport.doStartTag method before an iteration starts. Subsequently, the LoopTagSupport superclass calls the hasNext method (possibly more than once) for each round of the iteration. Finally, the next method, which returns the next object in the iteration, is invoked by the LoopTagSupport superclass. Listing 4.11 WEB-INF/core-jstl.tld<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"> <taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>Core JSTL Custom Actions</short-name> <display-name> Core JSTL Custom Actions for the Iteration Actions chapter </display-name> <description> A library containing two custom actions that determine whether a round is or is not the first round in an iteration. </description> <tag> <name>firstRound</name> <tag-class>tags.FirstRoundAction</tag-class> <body-content>JSP</body-content> <description> This action determines whether the current round in an iteration is the first. This action must be nested in an action that implements the javax.servlet.jsp.jstl.core.LoopTag interface. </description> </tag> <tag> <name>notFirstRound</name> <tag-class>tags.NotFirstRoundAction</tag-class> <body-content>JSP</body-content> <description> This action determines whether the current round in an iteration is NOT the first. This action must be nested in an action that implements the javax.servlet.jsp.jstl.core.LoopTag interface. </description> </tag> </taglib> Table 4.4. LoopTagSupport protected Variables
Table 4.5. LoopTagSupport abstract Methods [a]
Typically, the three methods listed in Table 4.5 are the only methods you will need to implement for your iteration custom actions. The LoopTagSupport class also provides a number of convenience methods, which are listed in Table 4.6. Table 4.6. LoopTagSupport Convenience Methods [a]
If you write enough iteration custom actions, you will probably find a use for all of the methods listed in Table 4.6 at one time or another, except for the setVar and setVarStatus methods, which are setter methods for the var and varStatus attributes, respectively. The following two sections show you how to implement to custom actions: one that iterates over integer values, and another that iterates over a data structure. Custom Actions That Iterate Over Integer ValuesRemember from our discussion in "The <c:forEach> Action" on page 154 that the <c:forEach> action can be used in two ways: to iterate over integer values or a data structure. The differentiator that determines how <c:forEach> is used is whether you specify a data structure with the items attribute; if you specify that attribute, <c:forEach> iterates over the data structure. If you don't specify the items attribute, <c:forEach> iterates over the integer values that you specify with the begin and end attributes. If you look at Table 4.5 and Table 4.6, which collectively list all of the LoopTagSupport methods, you will see that the LoopTagSupport class does not implement a setter method for the items attribute. That omission may lead you to believe that custom actions whose tag handlers extend LoopTagSupport are meant to iterate over integer values, but that assumption can get you into trouble. Let's see how. Listing 4.12 lists a simple tag handler, designed to iterate over integer values, that extends LoopTagSupport . Listing 4.12 WEB-INF/classes/tags/SimpleIterationAction.javapackage tags; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.jstl.core.LoopTagSupport; public class SimpleIterationAction extends LoopTagSupport { private int count; public void setBegin(int begin) throws JspTagException { this.begin = begin; } public void setEnd(int end) throws JspTagException { this.end = end; } protected void prepare() throws JspTagException { count = begin; } protected boolean hasNext() throws JspTagException { return count <= end; } protected Object next() throws JspTagException { return new Integer(count++); } } Because LoopTagSupport does not provide setter methods for the begin and end properties, the preceding tag handler implements those methods. That tag handler also implements the abstract methods defined by LoopTagSupport listed in Table 4.5 on page 185. The prepare method sets a private count variable to the value specified for the begin attribute, the hasNext method returns true if the count variable is less than or equal to the value specified for the end attribute, and the next method returns an integer value representing the current count and increments that count. Now that our tag handler is implemented, let's see how you can use that tag handler's associated custom action named <core-jstl:simpleIteration>. Listing 4.13 lists a JSP page that iterates with <c:forEach> and <core-jstl:simpleIteration>. Identical attributes are specified for both actions: the begin attribute's value is set to 5 and the end attribute's value is set to 10 . Listing 4.13 Using an Iteration Custom Action<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>A Broken Custom Iteration Action</title> </head> <body> <%@ taglib uri='http://java.sun.com/jstl/core' prefix='c' %> <%@ taglib uri='WEB-INF/core-jstl.tld' prefix='core-jstl' %> <font size='4'>With <c:forEach></font><p> <c:forEach var='item' begin='5' end='10'> Item: <c:out value='${item}'/><br> </c:forEach> <p> <font size='4'>With the Custom Action</font><p> <core-jstl:simpleIteration var='item' begin='5' end='10'> Item: <c:out value='${item}'/><br> </core-jstl:simpleIteration> </body> </html> The output of the preceding JSP page is shown in Figure 4-9. Figure 4-9. A Broken Custom Iteration Action
As you can see from Figure 4-9, the <c:forEach> action properly iterates over the integer values 5 through 10 inclusive; however, the <core-jstl:simpleIteration> action only iterates once. The tag handler for the <core-jstl:simpleIteration> action couldn't be much simpler, but somewhere along the line, something went wrong. The problem with the tag handler listed in Listing 4.12 is that it set the begin and end attributes stored in its superclass ( LoopTagSupport ). When you set those attributes, LoopTagSupport interprets those values as indexes into an underlying collection. But if you don't explicitly specify a collection, where does the collection come from? The answer is that the values returned from the next method are interpreted by LoopTagSupport as a collection. So then, why did the <core-jstl:simpleIteration> action only iterate once? Here is the answer ”the values returned by the next method of the action's tag handler are 5 6 7 8 9 10 . As mentioned above, those values are interpreted by LoopTagSupport as a collection, and therefore the begin and end attributes ” in this case, 5 and 10 ”are interpreted as indexes into that collection. So LoopTagSupport starts iterating over the sixth item (remember that collection indexes are 0-based), which is the value 10 . Because that value is the last item in the "collection," the iteration stops after that value. So, how can we fix the tag handler for the <core-jstl:simpleIteration> action so that it properly iterates over integer values? The answer is simple: we implement that tag handler so that it does not set the begin and end attributes stored in its superclass ( LoopTagSupport ), and therefore LoopTagSupport will not interpret those values ”because it won't know about them ”as indexes into the values returned by the tag handler's next method. The easiest way to do that is to declare begin and end variables in the tag handler ”that declaration hides the variables of the same name in the LoopTagSupport superclass. Listing 4.14 lists the revised tag handler. With the revised tag handler listed above, the <core-jstl:simpleIteration> action will properly iterate over integer values, as you can see from Figure 4-10, which shows the output from the JSP page listed in Listing 4.13 on page 188. Figure 4-10. A Custom Iteration Action That Iterates Over Integer Values Properly
Listing 4.14 WEB-INF/classes/tags/SimpleIterationAction.java (revised) package tags; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.jstl.core.LoopTagSupport; public class SimpleIterationAction extends LoopTagSupport { private int count, begin, end; public void setBegin(int begin) throws JspTagException { this.begin = begin; } public void setEnd(int end) throws JspTagException { this.end = end; } protected void prepare() throws JspTagException { count = begin; } protected Object next() throws JspTagException { return new Integer(count++); } protected boolean hasNext() throws JspTagException { return count <= end; } } One note of caution before we move on. The preceding discussion is based on the JSTL Reference Implementation. The JSTL specification is rather vague about how the LoopTagSupport class should behave when used as a superclass, as in the preceding example. Because of that vagueness, you may experience different results if you try the preceding example with a JSTL implementation other than the JSTL Reference Implementation. [6]
Finally, the tag library descriptor (TLD) for the tag library containing the <core-jstl:simpleIteration> action is listed in Listing 4.15. Listing 4.15 WEB-INF/core-jstl.tld<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"> <taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>Core JSTL Custom Actions</short-name> <display-name> Core JSTL Custom Actions for the Iteration Action chapter </display-name> <description> A library containing a custom action that iterates over numeric values. </description> <tag> <name>simpleIteration</name> <tag-class>tags.SimpleIterationAction</tag-class> <body-content>JSP</body-content> <description> This action iterates over numeric values. This action leverages JSTL functionality by extending the javax.servlet.jsp.jstl.core.LoopTagSupport class. </description> <attribute> <name>var</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>varStatus</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>begin</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>end</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>step</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag> </taglib> Notice that the preceding TLD must list all of the attributes supported by the <core-jstl:simpleIteration> action, even though setter methods for some of those attributes, namely, the var and varStatus attributes, are implemented by the LoopTagSupport class. Now that we've seen how to properly implement custom actions that iterate over integer values, let's see how to implement a custom action that iterates over a data structure. Custom Actions That Iterate Over Data StructuresThe JSP page shown in Figure 4-11 contains a login form that does not specify an action, so when you activate the Log In button, the JSP page is reloaded. The top picture in Figure 4-11 shows the JSP page just after the form has been filled out, and the bottom picture shows the JSP page after the Log In button has been activated and the page has been reloaded. Figure 4-11. An Iteration Custom Action That Displays Request Parameters
The JSP page shown in Figure 4-11 uses a custom action that iterates over request parameters and displays their values. That custom action comes in handy for debugging, especially for forms that post their data, such as the form contained in the JSP page shown in Figure 4-11. The JSP page shown in Figure 4-11 is listed in Listing 4.16. Listing 4.16 Displaying Request Parameters with a Custom Action<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title> An Iteration Custom Action that Displays Request Parameters </title> </head> <body> <%@ taglib uri='http://java.sun.com/jstl/core' prefix='c' %> <%@ taglib uri='WEB-INF/app-tlds/app.tld' prefix='core-jstl'%> <%-- This form does not specify an action, so this JSP page will be reloaded when the submit button is activated --%> <form method='post'> <table> <tr> <td>First Name:</td> <td><input type='text' name='firstName' value='<c:out value="${param.firstName}"/>'/> </td> </tr> <tr> <td>Last Name:</td> <td><input type='text' name='lastName' value='<c:out value="${param.lastName}"/>'/> </td> </tr> <tr> <td>Email Address:</td> <td><input type='text' name='email' value='<c:out value="${param.email}"/>'/> </td> </tr> </table> <p><input type='submit' value='Log In'/> </form> <%-- If any of the fields in the form were filled in... --%> <c:if test='${not empty param.firstName or not empty param.lastName or not empty param.email}'> Request Parameters:<p> <%-- Show all request parameters with the custom action --%> <core-jstl:requestParams var='item'> <c:out value='${item}'/><br> </core-jstl:requestParams> </c:if> </body> </html> The preceding JSP page tests to see whether any of the form's fields have been filled in; if so, the <core-jstl:requestParams> action iterates over the request parameters, which are displayed by the <c:out> action in the body of the <core-jstl:requestParams> action. The tag handler for the <core-jstl:requestParams> action is listed in Listing 4.17. Listing 4.17 WEB-INF/classes/tags/ShowRequest ParametersAction.javapackage tags; import java.util.*; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.jstl.core.LoopTagSupport; public class ShowRequestParametersAction extends LoopTagSupport { private Iterator entriesIterator; private StringBuffer nextItem = new StringBuffer(); // Prepare for the next round of the iteration. The prepare // method is invoked once by LoopTagSupport.doStartTag() protected void prepare () throws JspTagException { // Get a reference to the map of request parameters Map parameterMap=pageContext.getRequest().getParameterMap(); // Store the iterator from the request parameters map entriesIterator = parameterMap.entrySet().iterator(); } // Determine whether there are any items left to iterate over protected boolean hasNext () throws JspTagException { return entriesIterator.hasNext(); } // Return the next item in the collection protected Object next () throws JspTagException { // Get a reference to the next Map.Entry from the iterator // and get that entry's key and value. Map.Entry entry = (Map.Entry)entriesIterator.next(); String key = (String)entry.getKey(); String[] values = (String[])entry.getValue(); // Clear the nextItem string buffer nextItem.delete(0, nextItem.length()); // Add the map entry's key (which is the name of the request // parameter) to the nextItem string buffer nextItem.append(key + " = "); // Iterate over the map entry's value, which is an array of // strings representing the current request parameter's // values for(int i=0; i < values.length; ++i) { // Append the current value to the nextItem string buffer nextItem.append(values[i]); // If it's not the last value, append a comma to the // nextItem string buffer if(i != values.length-1) nextItem.append(","); } // Create a string from the nextItem string buffer and // return that string return nextItem.toString(); } } The preceding tag handler implements the three abstract methods defined by the LoopTagSupport class: prepare , hasNext , and next . The prepare method obtains a reference to a map of request parameters and their values, accesses an iterator for that map, and stores it in a private variable. The hasNext method uses the Iterator.hasNext method to determine whether any items are left to iterate over. The next method obtains a reference to the next item in the collection with the Iterator.next method and stores a string with the format key=values in a string buffer, where key is the name of the request parameter and values is a comma-separated string representing that parameter's values. Finally, the next method creates a string from that string buffer and returns it. Listing 4.18 lists the tag library descriptor for the tag library that contains the <core-jstl:requestParams> action. The preceding tag library descriptor declares the <core-jstl:requestParams> action and all of its attributes. Listing 4.18 WEB-INF/core-jstl.tld<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"> <taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>Core JSTL Custom Actions</short-name> <display-name> Core JSTL Custom Actions for the Iteration Action chapter </display-name> <description> A library containing a custom action that shows request parameters </description> <tag> <name>requestParams</name> <tag-class>tags.ShowRequestParametersAction</tag-class> <body-content>JSP</body-content> <description> This action prints all request parameters and their values. This action leverages JSTL functionality by extending the javax.servlet.jsp.jstl.core.LoopTagSupport class. </description> <attribute> <name>var</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag> </taglib> |