站内搜索: 请输入搜索关键词
当前页面: 在线文档首页 > The J2EE 1.4 Tutorial

Programming Simple Tag Handlers - The J2EE 1.4 Tutorial

Programming Simple Tag Handlers

The classes and interfaces used to implement simple tag handlers are contained in the javax.servlet.jsp.tagext package. Simple tag handlers implement the SimpleTag interface. Interfaces can be used to take an existing Java object and make it a tag handler. For most newly created handlers, you would use the SimpleTagSupport classes as a base class.

The heart of a simple tag handler is a single method--doTag--which gets invoked when the end element of the tag is encountered. Note that the default implementation of the doTag method of SimpleTagSupport does nothing.

A tag handler has access to an API that allows it to communicate with the JSP page. The entry point to the API is the JSP context object (javax.servlet.jsp.JspContext). The JspContext object provides access to implicit objects. PageContext extends JspContext with servlet-specific behavior. Through these objects, a tag handler can retrieve all the other implicit objects (request, session, and application) that are accessible from a JSP page. If the tag is nested, a tag handler also has access to the handler (called the parent) that is associated with the enclosing tag.

Including Tag Handlers in Web Applications

Tag handlers can be made available to a web application in two basic ways. The classes implementing the tag handlers can be stored in an unpacked form in the WEB-INF/classes/ subdirectory of the web application. Alternatively, if the library is distributed as a JAR, it is stored in the WEB-INF/lib/ directory of the web application.

How Is a Simple Tag Handler Invoked?

The SimpleTag interface defines the basic protocol between a simple tag handler and a JSP page's servlet. The JSP page's servlet invokes the setJspContext, setParent, and attribute setting methods before calling doStartTag.

ATag t = new ATag();
t.setJSPContext(...);
t.setParent(...);
t.setAttribute1(value1);
t.setAttribute2(value2);
...
t.setJspBody(new JspFragment(...))
t.doTag(); 

The following sections describe the methods that you need to develop for each type of tag introduced in Types of Tags.

Tag Handlers for Basic Tags

The handler for a basic tag without a body must implement the doTag method of the SimpleTag interface. The doTag method is invoked when the end element of the tag is encountered.

The basic tag discussed in the first section, <tt:basic />, would be implemented by the following tag handler:

public HelloWorldSimpleTag extends SimpleTagSupport {
  public void doTag() throws JspException, IOException {
    getJspContext().getOut().write("Hello, world.");
  }
} 

Tag Handlers for Tags with Attributes

Defining Attributes in a Tag Handler

For each tag attribute, you must define a set method in the tag handler that conforms to the JavaBeans architecture conventions. For example, consider the tag handler for the JSTL c:if tag:

<c:if test="${Clear}"> 

This tag handler contains the following method:

public void setTest(boolean test) {
  this.test = test;
} 

Attribute Validation

The documentation for a tag library should describe valid values for tag attributes. When a JSP page is translated, a web container will enforce any constraints contained in the TLD element for each attribute.

The attributes passed to a tag can also be validated at translation time using the validate method of a class derived from TagExtraInfo. This class is also used to provide information about variables defined by the tag (see TagExtraInfo Class).

The validate method is passed the attribute information in a TagData object, which contains attribute-value tuples for each of the tag's attributes. Because the validation occurs at translation time, the value of an attribute that is computed at request time will be set to TagData.REQUEST_TIME_VALUE.

The tag <tt:twa attr1="value1"/> has the following TLD attribute element:

<attribute>
  <name>attr1</name>
  <required>true</required>
  <rtexprvalue>true</rtexprvalue>
</attribute> 

This declaration indicates that the value of attr1 can be determined at runtime.

The following validate method checks whether the value of attr1 is a valid Boolean value. Note that because the value of attr1 can be computed at runtime, validate must check whether the tag user has chosen to provide a runtime value.

public class TwaTEI extends TagExtraInfo {
  public ValidationMessage[] validate(TagData data) {
    Object o = data.getAttribute("attr1");
    if (o != null && o != TagData.REQUEST_TIME_VALUE) {
      if (((String)o).toLowerCase().equals("true") || 
        ((String)o).toLowerCase().equals("false") ) 
        return null;
      else
        return new ValidationMessage(data.getId(),
          "Invalid boolean value.");
    }
    else
      return null;
  }
} 

Setting Dynamic Attributes

Simple tag handlers that support dynamic attributes must declare that they do so in the tag element of the TLD (see Declaring Tag Handlers). In addition, your tag handler must implement the setDynamicAttribute method of the DynamicAttributes interface. For each attribute specified in the tag invocation that does not have a corresponding attribute element in the TLD, the web container calls setDynamicAttribute, passing in the namespace of the attribute (or null if in the default namespace), the name of the attribute, and the value of the attribute. You must implement the setDynamicAttribute method to remember the names and values of the dynamic attributes so that they can be used later when doTag is executed. If the setDynamicAttribute method throws an exception, the doTag method is not invoked for the tag, and the exception must be treated in the same manner as if it came from an attribute setter method.

The following implementation of setDynamicAttribute saves the attribute names and values in lists. Then, in the doTag method, the names and values are echoed to the response in an HTML list.

private ArrayList keys = new ArrayList();
private ArrayList values = new ArrayList();

public void setDynamicAttribute(String uri, 
  String localName, Object value ) throws JspException {
  keys.add( localName );
  values.add( value );
}

public void doTag() throws JspException, IOException {
  JspWriter out = getJspContext().getOut();
  for( int i = 0; i < keys.size(); i++ ) {
      String key = (String)keys.get( i );
      Object value = values.get( i );
      out.println( "<li>" + key + " = " + value + "</li>" );
  }
} 

Tag Handlers for Tags with Bodies

A simple tag handler for a tag with a body is implemented differently depending on whether or not the tag handler needs to manipulate the body. A tag handler manipulates the body when it reads or modifies the contents of the body.

Tag Handler Does Not Manipulate the Body

If a tag handler needs simply to evaluate the body, it gets the body using the getJspBody method of SimpleTag and then evaluates the body using the invoke method.

The following tag handler accepts a test parameter and evaluates the body of the tag if the test evaluates to true. The body of the tag is encapsulated in a JSP fragment. If the test is true, the handler retrieves the fragment using the getJspBody method. The invoke method directs all output to a supplied writer or, if the writer is null, to the JspWriter returned by the getOut method of the JspContext associated with the tag handler.

public class IfSimpleTag extends SimpleTagSupport {
  private boolean test;
  public void setTest(boolean test) {
    this.test = test;
  }
  public void doTag() throws JspException, IOException {
    if(test){
      getJspBody().invoke(null);
    }
  }
} 
Tag Handler Manipulates the Body

If the tag handler needs to manipulate the body, the tag handler must capture the body in a StringWriter. The invoke method directs all output to a supplied writer. Then the modified body is written to the JspWriter returned by the getOut method of the JspContext. Thus, a tag that converts its body to uppercase could be written as follows:

public class SimpleWriter extends SimpleTagSupport {
  public void doTag() throws JspException, IOException {
    StringWriter sw = new StringWriter();
    jspBody.invoke(sw);
    jspContext().
      getOut().println(sw.toString().toUpperCase());
  }
} 

Tag Handlers for Tags That Define Variables

Similar communication mechanisms exist for communication between JSP page and tag handlers as for JSP pages and tag files.

To emulate IN parameters, use tag attributes. A tag attribute is communicated between the calling page and the tag handler when the tag is invoked. No further communication occurs between the calling page and the tag handler.

To emulate OUT or nested parameters, use variables with availability AT_BEGIN, AT_END, or NESTED. The variable is not initialized by the calling page but instead is set by the tag handler.

For AT_BEGIN availability, the variable is available in the calling page from the start tag until the scope of any enclosing tag. If there's no enclosing tag, then the variable is available to the end of the page. For AT_END availability, the variable is available in the calling page after the end tag until the scope of any enclosing tag. If there's no enclosing tag, then the variable is available to the end of the page. For nested parameters, the variable is available in the calling page between the start tag and the end tag.

When you develop a tag handler you are responsible for creating and setting the object referenced by the variable into a context that is accessible from the page. You do this by using the JspContext().setAttribute(name, value) or JspContext.setAttribute(name, value, scope) method. You retrieve the page context using the getJspContext method of SimpleTag.

Typically, an attribute passed to the custom tag specifies the name of the variable and the value of the variable is dependent on another attribute. For example, the iterator tag introduced in Chapter 12 retrieves the name of the variable from the var attribute and determines the value of the variable from a computation performed on the group attribute.

public void doTag() throws JspException, IOException {
  if (iterator == null)
    return;
  while (iterator.hasNext()) {
    getJspContext().setAttribute(var, iterator.next());
    getJspBody().invoke(null);
  }
}
public void setVar(String var) {
  this.var = var;
}
public void setGroup(Collection group) {
  this.group = group;
  if(group.size() > 0)
    iterator = group.iterator();
} 

The scope that a variable can have is summarized in Table 15-13. The scope constrains the accessibility and lifetime of the object.

Table 15-13 Scope of Objects 
Name
Accessible From
Lifetime
page
Current page
Until the response has been sent back to the user or the request is passed to a new page
request
Current page and any included or forwarded pages
Until the response has been sent back to the user
session
Current request and any subsequent request from the same browser (subject to session lifetime)
The life of the user's session
application
Current and any future request in the same web application
The life of the application

TagExtraInfo Class

In Declaring Tag Variables for Tag Handlers we discussed how to provide information about tag variables in the tag library descriptor. Here we describe another approach: defining a tag extra info class. You define a tag extra info class by extending the class javax.servlet.jsp.tagext.TagExtraInfo. A TagExtraInfo must implement the getVariableInfo method to return an array of VariableInfo objects containing the following information:

  • Variable name
  • Variable class
  • Whether the variable refers to a new object
  • The availability of the variable

The web container passes a parameter of type javax.servlet.jsp.tagext.TagData to the getVariableInfo method, which contains attribute-value tuples for each of the tag's attributes. These attributes can be used to provide the VariableInfo object with an EL variable's name and class.

The following example demonstrates how to provide information about the variable created by the iterator tag in a tag extra info class. Because the name (var) and class (type) of the variable are passed in as tag attributes, they can be retrieved using the data.getAttributeString method and can be used to fill in the VariableInfo constructor. To allow the variable var to be used only within the tag body, you set the scope of the object to NESTED.

package iterator;
public class IteratorTEI extends TagExtraInfo {
  public VariableInfo[] getVariableInfo(TagData data) {
    String type = data.getAttributeString("type");
    if (type == null)
      type = "java.lang.Object";
    return new VariableInfo[] {
      new VariableInfo(data.getAttributeString("var"),
      type,
      true,
      VariableInfo.NESTED)
    };
  }
} 

The fully qualified name of the tag extra info class defined for an EL variable must be declared in the TLD in the tei-class subelement of the tag element. Thus, the tei-class element for IteratorTei would be as follows:

<tei-class>
  iterator.IteratorTEI
</tei-class> 

Cooperating Tags

Tags cooperate by sharing objects. JSP technology supports two styles of object sharing.

The first style requires that a shared object be named and stored in the page context (one of the implicit objects accessible to JSP pages as well as tag handlers). To access objects created and named by another tag, a tag handler uses the pageContext.getAttribute(name, scope) method.

In the second style of object sharing, an object created by the enclosing tag handler of a group of nested tags is available to all inner tag handlers. This form of object sharing has the advantage that it uses a private namespace for the objects, thus reducing the potential for naming conflicts.

To access an object created by an enclosing tag, a tag handler must first obtain its enclosing tag by using the static method SimpleTagSupport.findAncestorWithClass(from, class) or the SimpleTagSupport.getParent method. The former method should be used when a specific nesting of tag handlers cannot be guaranteed. After the ancestor has been retrieved, a tag handler can access any statically or dynamically created objects. Statically created objects are members of the parent. Private objects can also be created dynamically. Such privately named objects would have to be managed by the tag handler; one approach would be to use a Map to store name-object pairs.

The following example illustrates a tag handler that supports both the named approach and the private object approach to sharing objects. In the example, the handler for a query tag checks whether an attribute named connectionId has been set. If the connectionId attribute has been set, the handler retrieves the connection object from the page context. Otherwise, the tag handler first retrieves the tag handler for the enclosing tag and then retrieves the connection object from that handler.

public class QueryTag extends SimpleTagSupport {
  public int doTag() throws JspException {
    String cid = getConnectionId();
    Connection connection;
    if (cid != null) {
    // there is a connection id, use it
      connection =(Connection)pageContext.
        getAttribute(cid);
    } else {
      ConnectionTag ancestorTag =
        (ConnectionTag)findAncestorWithClass(this,
          ConnectionTag.class);
      if (ancestorTag == null) {
        throw new JspTagException("A query without
          a connection attribute must be nested
          within a connection tag.");
      }
      connection = ancestorTag.getConnection();
      ...
    }
  }
} 

The query tag implemented by this tag handler can be used in either of the following ways:

<tt:connection cid="con01" ... > 
  ... 
</tt:connection>
<tt:query id="balances" connectionId="con01"> 
  SELECT account, balance FROM acct_table 
    where customer_number = ?
  <tt:param value="${requestScope.custNumber}" />
</tt:query>

<tt:connection ... >
  <tt:query cid="balances"> 
    SELECT account, balance FROM acct_table 
    where customer_number = ?
    <tt:param value="${requestScope.custNumber}" />
  </tt:query>
</tt:connection> 

The TLD for the tag handler use the following declaration to indicate that the connectionId attribute is optional:

<tag>
  ...
  <attribute>
    <name>connectionId</name>
    <required>false</required>
  </attribute>
</tag> 

Examples

The simple tags described in this section demonstrate solutions to two recurring problems in developing JSP applications: minimizing the amount of Java programming in JSP pages and ensuring a common look and feel across applications. In doing so, they illustrate many of the styles of tags discussed in the first part of the chapter.

An Iteration Tag

Constructing page content that is dependent on dynamically generated data often requires the use of flow control scripting statements. By moving the flow control logic to tag handlers, flow control tags reduce the amount of scripting needed in JSP pages. Iteration is a very common flow control function and is easily handled by a custom tag.

The discussion on using tag libraries in Chapter 12 introduced a tag library containing an iterator tag. The tag retrieves objects from a collection stored in a JavaBeans component and assigns them to an EL variable. The body of the tag retrieves information from the variable. As long as elements remain in the collection, the iterator tag causes the body to be reevaluated. The tag in this example is simplified to make it easy to demonstrate how to program a custom tag. web applications requiring such functionality should use the JSTL forEach tag, which is discussed in Iterator Tags.

JSP Page

The index.jsp page invokes the iterator tag to iterate through a collection of department names. Each item in the collection is assigned to the departmentName variable.

<%@ taglib uri="/tlt" prefix="tlt" %>
<html>
  <head>
  <title>Departments</title>
  </head>
  <body bgcolor="white">
  <jsp:useBean id="myorg" class="myorg.Organization"/>
  <table border=2 cellspacing=3 cellpadding=3>
    <tr>
      <td><b>Departments</b></td>
    </tr>
  <tlt:iterator var="departmentName" type="java.lang.String"
      group="${myorg.departmentNames}">
    <tr>
      <td><a href="list.jsp?deptName=${departmentName}">
        ${departmentName}</a></td>
    </tr>
  </tlt:iterator>
  </table>
  </body>
</html> 
Tag Handler

The collection is set in the tag handler via the group attribute. The tag handler retrieves an element from the group and passes the element back to the page in the EL variable whose name is determined by the var attribute. The variable is accessed in the calling page using the JSP expression language. After the variable is set, the tag body is evaluated with the invoke method.

public void doTag() throws JspException, IOException {
  if (iterator == null)
    return;
  while (iterator.hasNext()) {
    getJspContext().setAttribute(var, iterator.next());
    getJspBody().invoke(null);
  }
}
public void setVar(String var) {
  this.var = var;
}
public void setGroup(Collection group) {
  this.group = group;
  if(group.size() > 0)
    iterator = group.iterator();
} 

A Template Tag Library

A template provides a way to separate the common elements that are part of each screen from the elements that change with each screen of an application. Putting all the common elements together into one file makes it easier to maintain and enforce a consistent look and feel in all the screens. It also makes development of individual screens easier because the designer can focus on portions of a screen that are specific to that screen while the template takes care of the common portions.

The template is a JSP page that has placeholders for the parts that need to change with each screen. Each of these placeholders is referred to as a parameter of the template. For example, a simple template might include a title parameter for the top of the generated screen and a body parameter to refer to a JSP page for the custom content of the screen.

The template uses a set of nested tags--definition, screen, and parameter--to define a table of screen definitions and uses an insert tag to insert parameters from a screen definition into a specific application screen.

JSP Pages

The template for the Duke's Bookstore example, template.jsp, is shown next. This page includes a JSP page that creates the screen definition and then uses the insert tag to insert parameters from the definition into the application screen.

<%@ taglib uri="/tutorial-template" prefix="tt" %>
<%@ page errorPage="/template/errorinclude.jsp" %>
<%@ include file="/template/screendefinitions.jsp" %>
<html>
<head>
<title>
<tt:insert definition="bookstore" parameter="title"/>
</title>
</head>
<body  bgcolor="#FFFFFF">
  <tt:insert definition="bookstore" parameter="banner"/>
<tt:insert definition="bookstore" parameter="body"/>
<center><em>Copyright &copy; 2004 Sun Microsystems, Inc. </
em></center>
</body>
</html> 

The screendefinitions.jsp page creates a definition for the screen specified by the request attribute javax.servlet.forward.servlet_path:

<tt:definition name="bookstore"
screen="${requestScope
  ['javax.servlet.forward.servlet_path']}">
  <tt:screen id="/bookstore">
  <tt:parameter name="title" value="Duke's Bookstore"
    direct="true"/>
  <tt:parameter name="banner" value="/template/banner.jsp"
    direct="false"/>
  <tt:parameter name="body" value="/bookstore.jsp"
    direct="false"/>
  </tt:screen>
  <tt:screen id="/bookcatalog">
  <tt:parameter name="title" direct="true">
      <jsp:attribute name="value" >
        <fmt:message key="TitleBookCatalog"/>
      </jsp:attribute>
    </tt:parameter>
    <tt:parameter name="banner" value="/template/banner.jsp"
    direct="false"/>
    <tt:parameter name="body" value="/bookcatalog.jsp"
    direct="false"/>
  </tt:screen>
  ...
</tt:definition> 

The template is instantiated by the Dispatcher servlet. Dispatcher first gets the requested screen. Dispatcher performs business logic and updates model objects based on the requested screen. For example, if the requested screen is
/bookcatalog, Dispatcher determines whether a book is being added to the cart based on the value of the Add request parameter. It sets the price of the book if it's on sale, and then adds the book to the cart. Finally, the servlet dispatches the request to template.jsp:

public class Dispatcher extends HttpServlet {
  public void doGet(HttpServletRequest request,
    HttpServletResponse response) {
    String bookId = null;
    BookDetails book = null;
    String clear = null;
    BookDBAO bookDBAO =
      (BookDBAO)getServletContext().
        getAttribute("bookDBAO");
    HttpSession session = request.getSession();
    String selectedScreen = request.getServletPath();
    ShoppingCart cart = (ShoppingCart)session.
      getAttribute("cart");
    if (cart == null) {
      cart = new ShoppingCart();
      session.setAttribute("cart", cart);
    }
    if (selectedScreen.equals("/bookcatalog")) {
      bookId = request.getParameter("Add");
      if (!bookId.equals("")) {
        try {
          book = bookDBAO.getBookDetails(bookId);
          if ( book.getOnSale() ) {
            double sale = book.getPrice() * .85;
            Float salePrice = new Float(sale);
            book.setPrice(salePrice.floatValue());
          }
          cart.add(bookId, book);
        } catch (BookNotFoundException ex) {
          // not possible
        }
      }
    } else if (selectedScreen.equals("/bookshowcart")) {
      bookId =request.getParameter("Remove");
      if (bookId != null) {
        cart.remove(bookId);
      } 
      clear = request.getParameter("Clear");
      if (clear != null && clear.equals("clear")) {
        cart.clear();
      }
    } else if (selectedScreen.equals("/bookreceipt")) {
    // Update the inventory
      try {
        bookDBAO.buyBooks(cart);
      } catch (OrderException ex) {
        request.setAttribute("selectedScreen",
          "/bookOrderError");
      }
    }
    try {
      request.
        getRequestDispatcher(
        "/template/template.jsp").
        forward(request, response);
    } catch(Exception ex) {
      ex.printStackTrace();
    }
  }

  public void doPost(HttpServletRequest request,
    HttpServletResponse response) {     
    request.setAttribute("selectedScreen",
      request.getServletPath());
    try {
      request.
        getRequestDispatcher(
        "/template/template.jsp").
        forward(request, response);
    } catch(Exception ex) {
      ex.printStackTrace();
    }
  }
} 
Tag Handlers

The template tag library contains four tag handlers--DefinitionTag, ScreenTag, ParameterTag, and InsertTag--that demonstrate the use of cooperating tags. DefinitionTag, ScreenTag, and ParameterTag constitute a set of nested tag handlers that share private objects. DefinitionTag creates a public object named bookstore that is used by InsertTag.

In doTag, DefinitionTag creates a private object named screens that contains a hash table of screen definitions. A screen definition consists of a screen identifier and a set of parameters associated with the screen. These parameters are loaded when the body of the definition tag, which contains nested screen and parameter tags, is invoked. DefinitionTag creates a public object of class Definition, selects a screen definition from the screens object based on the URL passed in the request, and uses this screen definition to initialize a public Definition object.

public int doTag() {
  try {
    screens = new HashMap();
    getJspBody().invoke(null);
    Definition definition = new Definition();
    PageContext context = (PageContext)getJspContext();
    ArrayList params = (ArrayList) screens.get(screenId);
    Iterator ir = null;
    if (params != null) {
      ir = params.iterator();
      while (ir.hasNext())
        definition.setParam((Parameter)ir.next());
    // put the definition in the page context
    context.setAttribute(definitionName, definition, 
      context.APPLICATION_SCOPE);
    }
  } 

The table of screen definitions is filled in by ScreenTag and ParameterTag from text provided as attributes to these tags. Table 15-14 shows the contents of the screen definitions hash table for the Duke's Bookstore application

Table 15-14 Screen Definitions 
Screen ID
Title
Banner
Body
/bookstore
Duke's Bookstore
/banner.jsp
/bookstore.jsp
/bookcatalog
Book Catalog
/banner.jsp
/bookcatalog.jsp
/bookdetails
Book Description
/banner.jsp
/bookdetails.jsp
/bookshowcart
Shopping Cart
/banner.jsp
/bookshowcart.jsp
/bookcashier
Cashier
/banner.jsp
/bookcashier.jsp
/bookreceipt
Receipt
/banner.jsp
/bookreceipt.jsp

.

If the URL passed in the request is /bookstore, the Definition object contains the items from the first row of Table 15-14 (see Table 15-15).

Table 15-15 Definition Object Contents for URL /bookstore
Title
Banner
Body
Duke's Bookstore
/banner.jsp
/bookstore.jsp

The parameters for the URL /bookstore are shown in Table 15-16. The parameters specify that the value of the title parameter, Duke's Bookstore, should be inserted directly into the output stream, but the values of banner and body should be included dynamically.

Table 15-16 Parameters for the URL /bookstore 
Parameter Name
Parameter Value
isDirect
title
Duke's Bookstore
true
banner
/banner.jsp
false
body
/bookstore.jsp
false

InsertTag inserts parameters of the screen definition into the response. The doTag method retrieves the definition object from the page context and then inserts the parameter value. If the parameter is direct, it is directly inserted into the response; otherwise, the request is sent to the parameter, and the response is dynamically included into the overall response.

public void doTag() throws JspTagException {  
  Definition definition = null;
  Parameter parameter = null;
  boolean directInclude = false;
  PageContext context = (PageContext)getJspContext();

    // get the definition from the page context
  definition = (Definition)context.getAttribute(
    definitionName, context.APPLICATION_SCOPE);
    // get the parameter
  if (parameterName != null && definition != null)
    parameter = (Parameter)
      definition.getParam(parameterName);

  if (parameter != null)
    directInclude = parameter.isDirect();

  try {
    // if parameter is direct, print to out
    if (directInclude && parameter  != null)
      context.getOut().print(parameter.getValue());
    // if parameter is indirect, 
        include results of dispatching to page 
    else {
      if ((parameter != null) && 
        (parameter.getValue() !=  null))
      context.include(parameter.getValue());
    }
  } catch (Exception ex) {
      throw new JspTagException(ex.getMessage());
  }
}