| [ Team LiB ] |
|
22.1 Developing Cooperating ActionsIt's often necessary to develop custom actions so that they can be combined with other actions, letting them cooperate in some fashion. You have seen examples of this throughout this book. For instance, in Chapter 12, <sql:param> action elements are nested within the body of a <sql:query> action to set the values of placeholders in the SQL statement. Another example of cooperation is how the <c:forEach> action can use the query result produced by the <sql:query> action. In this section, we take a look at the cooperation techniques demonstrated by these two examples: explicit cooperation between a parent element and elements nested in its body and implicit cooperation through objects exposed as scoped variables. 22.1.1 Using Explicit Parent-Child CooperationLet's look at a possible implementation of the <sql:param> tag handler as one example of explicit parent-child cooperation. As you may recall from Chapter 12, this action can be nested within the body of either an <sql:query> or an <sql:update> action: <sql:update sql="UPDATE Employee SET Salary = ? WHERE EmpId = ?">
<sql:param value="${param.newSalary}" />
<sql:param value="${param.empId}" />
</sql:update>
How does the <sql:param> action tell the enclosing <sql:update> action about the parameter it defines? The answer to this question lies in a couple of SimpleTag and Tag interface methods that I didn't cover in Chapter 21, plus a utility method implemented by both the SimpleTagSupport class and the TagSupport class. The interface methods are setParent( ) and getParent( ), implemented like this by the TagSupport class: ...
private Tag parent;
...
public void setParent(Tag t) {
parent = t;
}
public Tag getParent( ) {
return parent;
}
These two methods are standard accessor methods for the parent instance variable. The SimpleTagSupport implementation differs only in that the parent's type is JspTag—the common superclass for Tag and SimpleTag—instead of Tag. For a nested action element, the setParent( ) method is always called on the tag handler with a reference to the enclosing tag handler as its value. This way a nested tag handler always knows its parent. So a tag handler at any nesting level can ask for its parent, using getParent( ), and then ask for the parent's parent, and so on until it reaches a tag handler that doesn't have a parent (getParent( ) returns null). This means it has reached the top level. This is part of the puzzle. However, a tag handler is usually interested only in finding a parent it's been designed to work with. It would be nice to have a method that works its way up the hierarchy until it finds the parent of interest. That's exactly what the findAncestorWithClass( ) method does. Here's the TagSupport implementation: public static final Tag findAncestorWithClass(Tag from, Class klass) {
boolean isInterface = false;
if (from == null ||
klass == null ||
(!Tag.class.isAssignableFrom(klass) &&
!(isInterface = klass.isInterface( )))) {
return null;
}
for (;;) {
Tag tag = from.getParent( );
if (tag == null) {
return null;
}
if ((isInterface && klass.isInstance(tag)) ||
klass.isAssignableFrom(tag.getClass( )))
return tag;
else
from = tag;
}
}
The SimpleTagHandler implementation is similar but also deals with details needed in order to allow simple tag handlers to be nested within the body of a classic tag handler. It's used exactly the same in both classic and simple tag handlers, though, so don't worry about these details. First of all, note that the findAncestorWithClass( ) method is a static method. Consequently, even tag handlers that implement the SimpleTag or Tag interface explicitly, instead of extending the support classes, can use it. The method takes two arguments: the tag handler instance to start searching from and the class or interface of the parent. After making sure all parameters are valid, it starts working its way up the hierarchy of nested tag handlers. It stops when it finds a tag handler of the specified class or interface and returns it. If the specified parent type isn't found, the method returns null. This is all that's needed to let a nested action communicate with its parent—the parent accessor methods and the method that walks the action hierarchy to find the parent of interest. Example 22-1 shows how a <sql:param> tag handler class (loosely based on the JSTL reference implementation) can use this mechanism to find the enclosing tag handler instance. Example 22-1. An <sql:param> tag handler class...
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
public class SQLParamTag extends SimpleTagSupport {
private Object value;
public void setValue(String value) {
this.value = value;
}
public void doTag( ) throws JspException {
SQLExecutionTag parent = (SQLExecutionTag)
findAncestorWithClass(this, SQLExecutionTag.class);
if (parent == null) {
throw new JspTagException("The param action is not " +
"enclosed by a supported action type");
}
parent.addSQLParameter(value);
}
}
The class has one instance variable, value, and the corresponding setter method. The most interesting method is the doTag( ) method. This method first uses the findAncestorWithClass( ) method to try to locate the enclosing <sql:query> or <sql:update> tag handler instance. Note that an interface, SQLExecutionTag, is used as the method argument instead of a specific class. This makes it possible to let the <sql:param> action find both types of actions it cooperates with; all that's required is that the parent tag handlers implement the SQLExecutionTag interface: package javax.servlet.jsp.jstl.sql;
public interface SQLExecutionTag {
void addSQLParameter(Object value);
}
The interface defines one method: addSQLParameter( ). This is the method the nested SQLParamTag tag handler uses to communicate with its parent. For each nested <sql:param> action, the addSQLParameter( ) method gets called when the parent's body is processed. The value for each <sql:param> action is accumulated in the parent tag handler, ready to be used when the parent's doTag( ) method is called. Example 22-2 shows how the addSQLParameter( ) method can be implemented by the <sql:query> and <sql:update> tag handler classes. Example 22-2. An <sql:param> parent tag handler class...
public class SQLQueryTag extends SimpleTagSupport,
implements SQLExecutionTag {
private List params;
...
public void addSQLParameter(Object value) {
if (params == null) {
params = new ArrayList( );
}
params.add(value);
}
...
In addSQLParameter( ), the parameter value is saved in an ArrayList. Since I choose a simple tag handler implementation here, I don't have to worry about tag handler reuse issues. If I instead had implemented it as a classic tag handler, I would also need to reset the parameter list, e.g., in the doStartTag( ) method. 22.1.2 Using Implicit Cooperation Through VariablesMany JSTL actions cooperate implicitly through JSP scoped variables; one action exposes the result of its processing as a variable in one of the JSP scopes and another action uses the variable as its input. This type of cooperation is simple yet powerful. All that is required is that the tag handler that exposes the data saves it in one of the JSP scopes, for instance using the PageContext.setAttribute( ) method: public class VariableProducerTag extends SimpleTagSupport {
private String var;
private int scope = PageContext.PAGE_SCOPE;
public void setVar(String var) {
this.var = var;
}
public void setScope(String scope) {
if ("page".equals(scopeName)) {
scope = PageContext.PAGE_SCOPE;
}
else if ("request".equals(scopeName)) {
scope = PageContext.REQUEST_SCOPE;
}
else if ("session".equals(scopeName)) {
scope = PageContext.SESSION_SCOPE;
}
else if ("application".equals(scopeName)) {
scope = PageContext.APPLICATION_SCOPE;
}
}
public void doTag( ) {
// Perform the main task for the action
...
getJspContext( ).setAttribute(var, result, scope);
JspFragment body = getJspBody( );
if (body != null) {
body.invoke(null);
}
}
}
Here an attribute named var lets the page author specify the name of the variable. Even though this isn't strictly a requirement (cooperating tags could be designed to use a predefined, hardcoded variable name), it's the most flexible approach. The attribute name can be anything, but var is the name used by all JSTL actions. I suggest that you follow the same convention to help page authors understand how to use your custom actions. Another convention is to support a scope attribute, so the page author can decide how widely the variable should be made available. You must also decide where in the page you want the variable to be available to other actions. If it should be available to actions nested in the body of your custom action, you need to save the variable before invoking the body fragment in a simple tag handler, or in the doStartTag( ) or doInitBody( ) method for a classic tag handler. For a classic tag handler, you may also need to replace the variable with a new one in the doAfterBody( ) method. This is the typical behavior of an iteration action, in which the doStartTag( ) or doInitBody( ) method saves the initial value, and the doAfterBody( ) method replaces it with a new value for each iteration, for instance the current element of a collection the action iterates over. If you implement the tag handler as a simple tag handler, just replace the value before invoking the body fragment again, as described in Chapter 21. If it's important that the variable is available for nested actions only and not available outside the body of your action, you can remove it before exiting the doTag( ) method in a simple tag handler, or remove it in the doEndTag( ) method for a classic tag handler: pageContext.removeAttribute(var); This is what all JSTL actions do for nested availability variables. Another JSTL convention for this type of nested variable is to always make it available in the page scope, without giving the page author the option to set another scope. In some cases, the variable should not be available until after the end tag, perhaps because the value depends on the evaluation of the body, or the custom action doesn't support a body. For a classic tag handler, you can then save the variable in the doEndTag( ) method. With a simple tag handler, you simply set it after invoking the body fragment in the doTag( ) method. In the examples in this section, the tag handlers expose only one variable, but there's no limitation on the number of variables that can be exposed. If more than one variable is exposed, the recommendation is to use the var and scope attribute names for the primary variable and names starting with var and scope for the others. Other tag handlers can find the scoped variable using the JspContext.findAttribute( ) method: public class VariableConsumerTag extends SimpleTagSupport {
private String items;
public void setItems(String items) {
this.items = items;
}
public void doTag( ) throws JspException {
Collection c = getJspContext( ).findAttribute(items);
if (c == null) {
throw new JspTagException("Collection named " + items +
" could not be found");
}
// Perform the main task for the action
...
}
}
The findAttribute( ) method looks for the specified attribute (variable) in all scopes, in the order page, request, session, and application, and returns the first one it finds, or null if it can't find one. A better approach, starting with JSP 2.0, is to delegate the variable lookup to the EL. In other words, declare the attribute to be of the type you support instead of a variable name represented by a String: public class VariableConsumerTag extends SimpleTagSupport {
private Collection items;
public void setItems(Collection items) {
this.items = items;
}
public void doTag( ) throws JspException {
if (items == null) {
throw new JspTagException("The 'items' attribute is null");
}
// Perform the main task for the action
...
}
}
When an EL expression is used as the attribute value, the EL machinery looks up the variable saved by the variable producer tag handler: <xmp:varProducer var="someList" />
<xmp:varConsumer items="${someList}" />
22.1.2.1 Creating a scripting variableBesides making a variable available through the standard JSP scopes, a custom action can additionally make it available as a scripting variable in the same way as the standard <jsp:useBean> action. Note that creating a scripting variable isn't a requirement; actions can cooperate nicely through variables in the JSP scopes. As I described in Chapter 16, there are almost no reasons for using scripting elements any longer, since the JSTL actions and the EL provide convenient solutions for the type of problems that typically required scripting elements in previous versions of JSP. In the rare event that the data exposed by a custom action needs to be accessed through a scripting variable, the page author can use the <jsp:useBean> to declare a scripting variable and assign it a reference to the object: <xmp:varProducer var="someList" scope="session" /> <jsp:useBean id="someList" class="java.util.Collection" scope="session" /> <%= foo.size( ) %> You may therefore want to think twice about if you really need to expose the data through a scripting variable, and if so, why the <jsp:useBean> action isn't good enough for your needs. Not that it's extremely hard to let a custom action create a scripting variable, but it does create overhead in terms of extra code generation and potential problems due to the complex interaction with other code generated by the container when it converts the JSP page to a servlet. That said, the basic requirements for a custom action that creates a scripting variable are the same as for an action exposing a variable through the JSP scopes: the tag handler needs to save the variable using the JspContext.setAttribute( ) method in the doTag( ) method for a simple tag handler, or the doStartTag( ), doInitBody( ), doAfterBody( ), or doEndTag( ) method for a classic tag handler, depending on where it needs to be available to other actions. On top of this, you must also tell the container about the variable, so it can generate the code for declaring the scripting variable and assign it the value that your tag handler saves.
The easiest way to tell the container what it needs to know is by declaring the variable in the TLD. As you may remember from Chapter 21, a <variable> element can be nested in the body of a <tag> element in the TLD, as shown in Example 22-3. Example 22-3. Variable declaration using the TLD<tag>
<name>varProducer</name>
<tag-class>com.xmp.VariableProducerTag</tag-class>
<variable>
<name-from-attribute>id</name-from-attribute>
<variable-class>java.util.Collection</variable-class>
<declare>true</declare>
<scope>AT_END</scope>
<description>This variable contains ...</description>
</variable>
...
</tag>
In this example, the varProducer custom action introduces a scripting variable with the name specified by the page author in the id attribute (defined by the <name-from-attribute> element) of type java.util.Collection (defined by the <variable-class> element). The variable name specified by the page author through the id attribute must be unique within the page. Because it's used as a scripting variable name, it must also follow the variable name rules for the scripting language. For Java, this means it must start with a letter, followed by a combination of letters and digits and must not contain special characters, such as a dot or a plus sign. In most cases, letting the page author decide the variable name is the preferred design, but a hardcoded variable name can be specified if it's more appropriate for a specific action. To do so, replace the <name-from-attribute> element with the <name-given> element: <variable>
<name-given>foo</name-given>
...
</variable>
With this declaration in place, the container uses the hardcoded name foo as the variable name. Note that same rules apply to the hardcoded name as for a name picked by the page author: it must be unique and a valid Java variable name. The <declare> element can be true or false. If it's true, the container creates a scripting variable declaration for this variable. If you specify false instead, the container assumes that the variable has already been declared by another action or a scripting element and just reassigns it the value saved in a JSP scope by the tag handler for this action. The <scope> element tells the container where the variable should be available; it has nothing to do with the JSP scopes we have seen so far (page, request, session, and application). Instead, it defines where the new scripting variable is available to JSP scripting elements. A value of AT_BEGIN means that the variable is available from the action's start tag and stays available after the action's end tag. AT_END means it isn't available until after the action's end tag. A variable with scope NESTED is available only in the action's body, between the start and the end tag. The AT_BEGIN and NESTED values don't make sense for a simple tag handler, since the body of an action element implemented by a simple tag handler cannot contain scripting elements. To understand how all this works, let's look at the code the container generates from a JSP page that contains the varProducer action, declared as in Example 22-3: <%@ taglib prefix="xmp" uri="xmptaglib" %> <xmp:varProducer id="someList" /> <%= someList.size( ) %> The <xmp:varProducer> action creates a Collection object and saves it in the page scope with the name specified by the var attribute, someList in this case. Because of the <variable> declaration for this action in the TLD, the container declares a scripting variable with the same name and assigns it the value saved by the tag handler. The JSP scripting expression calls the size( ) method of the Collection referenced by the scripting variable and writes the value to the response. This JSP page fragment results in code similar to that shown in Example 22-4 in the generated servlet, assuming the custom action is implemented as a classic tag handler. Example 22-4. Code generated for JSP actions// Code for <xmp:varProducer> com.xmp.VariableProducerTag _jspx_th_xmp_varProducer_1 = new com.xmp.VariableProducerTag ( ); _jspx_th_xmp_varProducer_1.setPageContext(pageContext); _jspx_th_xmp_varProducer_1.setParent(null); _jspx_th_xmp_varProducer_1.setId("myVariable"); try { _jspx_th_xmp_varProducer_1.doStartTag( ); if (_jspx_th_xmp_varProducer_1.doEndTag( ) == Tag.SKIP_PAGE) return; } finally { _jspx_th_xmp_varProducer_1.release( ); } java.util.Collection someList = null; someList = (String) pageContext.findAttribute("someList"); ... // Code for <%= someList.size( ) %> out.print( someList.size( ) ); First, a tag handler instance is created and initialized with the standard properties (pageContext and parent) plus the property corresponding to the id attribute. Next, the doStartTag( ) and doEndTag( ) methods are called. Then comes the code that makes the object created by the action available as a scripting variable. Note how a variable with the name specified by the id attribute (someList) is declared, using the type specified by the <variable-class> element in the TLD. Also note that the variable is declared after the call to the doEndTag( ) method. This is because the <scope> element in the TLD is set to AT_END. If the scope is specified as AT_BEGIN instead, the declaration is added before the doStartTag() call, and the assignment code is added right after the call. In this case, the tag handler must save the variable in a JSP scope in the doStartTag( ) method. If the tag handler implements IterationTag, assignment code is also added so that the variable gets reassigned for every evaluation of the body and after the call to doAfterBody( ). This allows the tag handler to modify the variable value in the doAfterBody( ) method, so each evaluation of the body has a new value. Finally, if the scope is set to NESTED, both the declaration and the value assignment code is inserted in the code block representing the action body. The tag handler must therefore make the variable available in either the doStartTag( ) method or the doInitBody( ) method, and can also modify the value in the doAfterBody( ) method. For a simple tag handler, there's only on method: doTag( ). Hence, there's only one place where the variable can be created, so the AT_BEGIN and NESTED values are not useful for a simple tag handler, as I mentioned earlier. The variable is assigned the value of the object saved by the tag handler in one of the standard JSP scopes, using the findAttribute( ) method. As you may recall, this method searches through the scopes in the order page, request, session, and application, until it finds the specified object. With the value assigned to the Java variable, it's available to the JSP expression. 22.1.2.2 Using a TagExtraInfo subclass to declare a variableIn most cases, the TLD can declare all information about a variable created by a custom action. Here are the exceptions:
To deal with these cases, you have to implement a TagExtraInfo subclass instead of declaring the variables in the TLD. The TagExtraInfo class contains two methods a subclass can override to inform the container about scripting variables and to perform validation. I describe the variable information method here and the validation method later in this chapter. The TagExtraInfo class also provides a number of property access methods that can be used by the subclass to get information about the custom action attributes specified by the page author. Let's assume that the <xmp:varProducer> action lets the page author specify whether the variable should be declared using an attribute named declare, which accepts true and false values. Let's also assume that the variable type can be specified by a type attribute. Example 22-5 shows a TagExtraInfo subclass that handles these requirements. Example 22-5. TagExtraInfo subclass for <xmp:varProducer>package com.xmp;
import javax.servlet.jsp.tagext.*;
public class VariableProducerTEI extends TagExtraInfo {
public VariableInfo[] getVariableInfo(TagData data) {
String name = data.getAttributeString("id");
String declare = data.getAttributeString("declare");
String type = data.getAttributeString("type");
VariableInfo[] vi = new VariableInfo[1];
vi[0] =
new VariableInfo(name, type,
("true".equals(declare) ? true : false),
VariableInfo.AT_END)
}
return vi;
}
}
When the JSP container converts the JSP page to a servlet it calls the getVariableInfo( ) method. The method returns an array of VariableInfo objects, one per scripting variable exposed by the tag handler. The VariableInfo class is a simple bean with four properties, all of them initialized to the values passed as arguments to the constructor: varName, className, declare, and scope. These values have the same meaning as the corresponding <variable> subelements in the TLD: the variable name, the variable class name, whether to declare the variable or not (true or false), and where the variable should be visible (AT_BEGIN, AT_END, or NESTED). The VariableProducerTEI class sets the varName and className properties of the VariableInfo bean to the values of the var and type attributes specified by the page author in the JSP page. The declare property is set to true or false depending on the value of the declare attribute. To get the attribute value specified by the page author, another simple class named TagData is used. An instance of this class is passed as the argument to the getVariableInfo( ) method as shown in Example 22-5. The TagData instance is created by the JSP container and contains information about all action attributes specified by the page author in the JSP page. It has two methods of interest. First, the getAttributeString( ) method simply returns the specified attribute as a String. Some attributes values, however, may be specified by a JSP expression instead of a string literal, a so-called request-time attribute value. Since such a value isn't known during the translation phase, the TagData class also provides the getAttribute( ) method to indicate if an attribute value is a literal string, a request-time attribute value, or not set at all. The getAttribute( ) method returns an Object. If the attribute is specified as a request-time attribute value, the special REQUEST_TIME_VALUE object is returned. Otherwise a String is returned or null if the attribute isn't set. The final piece of the puzzle is to tell the container to actually use the TagExtraInfo subclass for your custom action. You do so with the <tei-class> element in the TLD: <tag>
<name>varProducer</name>
<tag-class>com.xmp.VariableProducerTag</tag-class>
<tei-class>com.xmp.VariableProducerTEI</tei-class>
...
</tag>
Note that you can't use both a <tei-class> element and a <variable> element for the same tag handler. 22.1.3 Supporting Undeclared AttributesSay you need to generate an HTML table with product information in a number of pages. The information to be shown for each product is subject to change, so you decide to create a custom action that does all the dirty work, allowing you to make the changes in one place when needed. However, the HTML <table> element supports a number of attributes affecting the table's look; there are 23 different attributes in HTML 4.0.1, to be exact. You could define all 23 attributes for the custom action, in addition to the specific ones needed for the custom action's core functionality, but then you would constantly have to add new attributes as they are added to new versions of the HTML specification. A better approach is to tell the container that the custom action supports dynamic attributes (a more accurate name would have been "undeclared attributes"). The dynamic attributes support is a new feature added in JSP 2.0. When a custom action is marked as supporting dynamic attributes, the page author can use attributes that are not explicitly declared for the tag handler in the custom action element, without the container flagging them as errors. You tell the container that the tag handler can handle dynamic attributes by adding a declaration in the TLD: .. ...
<tag>
<name>prodTable</name>
<tag-class>com.ora.jsp.tags.xmp.ProdTableTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>prods</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<dynamic-attributes>true</dynamic-attributes>
</tag>
...
We'll look at the TLD in more detail later, but notice the <dynamic-attributes> element here. This element, with the value true, is what tells the container that the tag handler is ready to deal with dynamic attributes. For regular attributes, the tag handler implements setter methods that the container calls, but that would defeat the purpose for dynamic attributes. Hence, we need another way for the container to provide the dynamic attribute names and values to the tag handler, and a way for the tag handler to read them. That's accomplished by the javax.servlet.jsp.tagext.DynamicAttributes interface that can be implemented by both classic and simple tag handlers. This interface declares a single method: setDynamicAttribute(String uri, String localName, Object value); The container calls this method for each undeclared attribute, in the order the attributes are encountered in the page. The uri argument holds the XML namespace URI for the attribute if specified, or null otherwise, the localName argument holds the attribute name minus the namespace prefix, and the value argument provides the value. Dynamic attributes implicitly support request-time attribute values (i.e., Java and EL expressions), so the real type of the value depends on the expression used to set it. In most cases, though, dynamic attributes are used for static string values, such as optional HTML element attributes to be pushed through to the elements generated by the tag handler. Example 22-6 shows the interesting part of the tag handler for the fictitious table-generating custom action. Example 22-6. Tag handler that accesses context information (ProdTableTag.java)package com.ora.jsp.tags.xmp;
import java.io.*;
import java.util.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
public class ProdTableTag extends SimpleTagSupport
implements DynamicAttributes {
private List prods;
private Map dynamicAttrs;
public void setProds(List prods) {
this.prods = prods;
}
public void setDynamicAttribute(String uri, String localName, Object value) {
if (dynamicAttrs == null) {
dynamicAttrs = new HashMap( );
}
dynamicAttrs.put(localName, value);
}
public void doTag( ) throws JspException, IOException {
StringBuffer html = new StringBuffer("<table");
if (dynamicAttrs != null) {
Iterator i = dynamicAttrs.keySet().iterator( );
while (i.hasNext( )) {
String name = (String) i.next( );
String value = dynamicAttrs.get(name).toString( );
html.append(" ").append(name).append("=\"").append(value).
append("\"");
}
}
JspWriter out = getJspContext().getOut( );
out.println(html.toString( ));
// Generate rows from product list
...
out.println("</table>");
}
}
Every time the setDynamicAttribute( ) method is called, the attribute name and value are saved in a Map. Later, in the doTag( ) method, all attributes accumulated in the Map are added as attributes of the generated HTML <table> element. I ignore the namespace argument in this example. It is rarely, if ever, used. It could potentially be used for a custom action in a JSP Document (i.e., JSP pages in XML format), where an action element attribute can contain a namespace prefix declared in the document. JSP 2.0, however, doesn't provide a way to get the prefix declared for the namespace URI; hence, it's hard to do much with it. I advise you to consider the namespace argument as preparation for XML support enhancements in a future version of the specification, where additional support features can be fleshed out. |
| [ Team LiB ] |
|