| [ Team LiB ] |
|
22.2 Validating SyntaxIt's easy to make mistakes when using custom actions in a JSP page. Everyone types the wrong attribute name now and then or forgets to specify a mandatory attribute. When custom actions depend on each other, using the cooperation techniques described earlier in this chapter, they typically need to be used in a specific order or nesting structure, and this isn't always obvious from the documentation. As a custom action developer, you have a number of tools at your disposal to help the page author find and correct errors like these. The first tool is the TLD. The TLD contains information about the attributes each action element supports and whether a body is supported or not. The JSP container uses this information to verify that the page author uses the custom action correctly, at least in the most basic sense. For more advanced validation, I'm afraid you have to do a bit of coding yourself. The most powerful validation tool defined by the JSP specification is the TagLibraryValidator class. You can extend this class and bundle the subclass with your tag library to validate all aspects of JSP pages that use your library. A less powerful option, but still useful in some cases, is the TagExtraInfo class. Extensions of this class can validate the use of a single custom action, for instance that optional attributes are used correctly. The next three sections describe these validation alternatives in detail. 22.2.1 Validation Based on the TLDWhen the JSP container converts a JSP page to a servlet, it compares each custom action element to the specification of the action element in the TLD. First, it makes sure that the action name matches the name of an action specified in the TLD corresponding to the action element's prefix. It then looks at the attribute list in the page and compares it to the attribute specification in the TLD. If a required attribute is missing or an attribute is used in the page but not specified in the TLD, it reports it as an error so the page author can correct the mistake. It also reports an error if a body is used for a custom action that is declared to be empty. 22.2.2 Using a TagLibraryValidatorA feature introduced in JSP 1.2 is the tag library validator, represented by the javax.servlet.jsp.tagext.TagLibraryValidator class. The container gives a validator access to an XML representation of the complete page. A validator can therefore verify interactions between custom actions, for instance that a custom action that must be used as a subelement of another action element isn't used anywhere else, or that action elements are used in the appropriate order. It can also analyze the use of custom action attributes, perhaps making sure that mutually exclusive optional attributes aren't used for an action element. The container uses the validator defined for a tag library when it converts a JSP page to a servlet, after performing validation of the page based on the information available in the TLD (attributes and empty bodies). Example 22-7 shows the top part of a validator that verifies that a <xmp:child> custom action element is used only within the body of an <xmp:parent> action element. Example 22-7. Validator class declarationpackage com.ora.jsp.tlv;
import java.util.*;
import javax.servlet.jsp.tagext.*;
import org.jdom.*;
import org.jdom.input.*;
public class OraTLV extends TagLibraryValidator {
private SAXBuilder builder = new SAXBuilder( );
private Namespace jspNamespace =
Namespace.getNamespace("http://java.sun.com/JSP/Page");
The validator class extends the TagLibraryValidator class that is part of the JSP API. In this particular validator, I use JDOM to work with the XML representation of the page. JDOM is a great open source product that lets you work with XML data in a way that's more suitable for Java than the standard DOM format defined by W3C. You can find out more about JDOM at the project web site: http://www.jdom.org/. To use JDOM, you must import the JDOM packages containing classes for parsing and the JDOM tree. If you don't want to use JDOM for some reason, you can, of course, use any XML parser and validation tools you want. For the validator in Example 22-7, I create an instance of the JDOM SAXBuilder class and save it as an instance variable. If the container caches instances of validators, this saves me from having to create this object for every page that's validated. I also create a JDOM Namespace instance for the JSP namespace as an instance variable. More about this later. The validator must override the validate( ) method: public ValidationMessage[] validate(String prefix, String uri,
PageData pd) {
ValidationMessage[] vms = null;
ArrayList msgs = new ArrayList( );
Namespace taglibNamespace = Namespace.getNamespace(uri);
try {
Document doc = builder.build(pd.getInputStream( ));
Element root = doc.getRootElement( );
validateElement(root, taglibNamespace, msgs);
}
catch (Exception e) {
vms = new ValidationMessage[1];
vms[0] = new ValidationMessage(null, e.getMessage( ));
}
if (msgs.size( ) != 0) {
vms = new ValidationMessage[msgs.size( )];
msgs.toArray(vms);
}
return vms;
}
The container invokes the validate( ) method with an instance of the PageData class Through the PageData instance, the validator can get the XML representation of the page by calling the getInputStream( ) method. The XML representation is formally called the page's XML View. It's is almost the same as a JSP Document (a JSP page written from scratch with the JSP XML syntax, described in Chapter 17). What's different is that all include directives have been processed, and all template text is wrapped with <jsp:text> elements if the page being validated is written using the classic JSP syntax. The JSP elements in the XML View also have a special jsp:id attribute that I'll get back to later. The prefix argument is the tag library prefix declared for this library, for instance xmp in this example, and the uri argument is the URI for the library, as it appear in the XML View. In a JSP Document, different prefixes can be declared for different parts of a page (using standard XML namespace declarations). In this case, the container calls the validator with the first prefix declared for the tag library. Since the prefix is not accurate in all cases, I create a Namespace from the uri attribute value and use it to identify elements from the tag library, as you will soon see. In this example, the validator( ) method gets the XML View for the page and uses JDOM to parse it. It then calls the validateElement( ) method with the document's root element, the namespace for this tag library, and an ArrayList used to collect error messages. If the validateElement( ) method finds any errors, the message list is converted to an array of ValidationMessage instances, used as the return value. As you will see later, a ValidationMessage instance contains the error message itself and information about where the error was found in the JSP source file. The fact that the validate( ) method returns an array of ValidationMessage instances means that all errors found in the page can be presented in one shot, allowing the page author to fix them all at once instead of one by one. The validateElement( ) method is a dispatcher to methods that validate specific elements: private void validateElement(Element e, Namespace ns, ArrayList msgs) {
if (ns.equals(e.getNamespace( ))) {
if (e.getName( ).equals("child")) {
validateChild(e, ns, msgs);
}
}
if (e.hasChildren( )) {
List kids = e.getChildren( );
Iterator i = kids.iterator( );
while(i.hasNext( )) {
validateElement((Element) i.next( ), ns, msgs);
}
}
}
It's a recursive method that is called for all elements in the document tree. First, it checks if the namespace for the current element matches the namespace for this tag library, i.e., if it's a custom action defined in this tag library. It then checks if it's an element that needs to be validated, and if so, calls the appropriate method. In this example, I validate only elements of type child, but this method can easily be extended to validate other elements as well. For all types of elements that have child nodes, the validateElement( ) method calls itself with each child node. That's how the method recursively scans the whole tree. The real validation code in this example is found in the validateChild( ) method: private void validateChild(Element e, Namespace ns, ArrayList msgs) {
Element parent = findParent(e, ns, "parent");
if (parent == null) {
String id = e.getAttributeValue("id", jspNamespace);
ValidationMessage vm = new ValidationMessage(id,
e.getQualifiedName( ) +
" must only be used with 'parent'");
msgs.add(vm);
}
}
The validateChild( ) method uses the private findParent( ) method to see if the current child element has a parent element of type parent. If it doesn't, it means that the child element is used incorrectly. In this case, a ValidationMessage instance is created to report the error and added to the list of error messages. A ValidationMessage contains two pieces of information: the error message itself and a unique ID for the element that the message refers to. The unique ID is assigned by the container and is passed to the validator as an element attribute named id in the JSP namespace, in other words, typically an attribute named jsp:id. Therefore, the first thing the validateParam( ) method does if it finds an error is to try to get this attribute so it can include it in the ValidationMessage. This is where the Namespace instance variable mentioned earlier is used. The container maintains a map between the ID and the location (line and column) of the element in the JSP source file. With this information, it can generate user-friendly error messages that include the location of the error. Figure 22-1 shows how Tomcat reports the errors reported by the sample validator when faces with the page in Example 22-8. Example 22-8. A page using the <xmp:child> action incorrectly (validation.jsp)<%@ page contentType="text/plain" %> <%@ taglib prefix="xmp" uri="xmplib" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%-- Correct usage. --%> <xmp:parent> <xmp:child/> </xmp:parent> <%-- Incorrect usage. The validator finds and reports these errors. --%> <xmp:child/> <c:if test="true"> <xmp:child/> </c:if> Figure 22-1. Validator error messages![]() Finally, the findParent( ) method that locates parent elements of a certain type looks like this: private Element findParent(Element e, Namespace ns, String name) {
if (e.getName( ).equals(name) &&
ns.equals(e.getNamespace( ))) {
return e;
}
Element parent = e.getParent( );
if (parent != null) {
return findParent(parent, ns, name);
}
return null;
}
It simply calls itself recursively until it either finds an element of the specified type or reaches the top of the document tree. If it finds a matching element, it returns it. Otherwise it returns null. A validator is associated with a tag library through the <validator> element in the TLD: <?xml version="1.0" encoding="ISO-8859-1" ?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<description>
A tag library that illustrates the use of a TagLibraryValidator,
containing two dummy custom actions and a validator. The "child"
action must be nested within the body of a "parent" action element.
</description>
<tlib-version>1.0</tlib-version>
<short-name>xmp</short-name>
<uri>xmplib</uri>
<validator>
<validator-class>com.ora.jsp.tlv.OraTLV</validator-class>
</validator>
<tag>
<name>child</name>
<tag-class>com.ora.jsp.tags.xmp.ChildTag</tag-class>
<body-content>empty</body-content>
</tag>
<tag>
<name>parent</name>
<tag-class>com.ora.jsp.tags.xmp.ParentTag</tag-class>
<body-content>scriptless</body-content>
</tag>
</taglib>
The <validator-class> element specifies the validator class name. Optional <init-param> elements can be nested within the <validator> element to configure a generic validator for a specific tag library: <validator>
<validator-class>com.ora.jsp.tlv.OraTLV</validator-class>
<init-param>
<param-name>logErrors</param-name>
<param-value>true</param-name>
</init-param>
<init-param>
<param-name>logFormat</param-name>
<param-value>detailed</param-name>
</init-param>
</validator>
The validator can read its parameters with the getParameters( ) method inherited from the TagLibraryValidator base class: Map params = getInitParameters( );
String myInitParam = (String) params.get("myInitParam");
22.2.3 Using a TagExtraInfo Class for ValidationThe TagLibraryValidator is the most powerful validation mechanism, but it comes at the price of complexity. You need to be pretty well versed in XML to validate the use of your tag library. I recommend that you give it a shot and make it your first choice, but it may be overkill for a small library with modest validation needs. If that's the case, you can develop TagExtraInfo subclasses for the individual custom actions that need validation. A TagExtraInfo subclass can validate the use of the action element attributes. Optional attributes may be mutually exclusive—if one is used, the other must not be used—or using one optional attribute may require another optional attribute be used as well. A TagExtraInfo subclass can verify rules like this, but it can't verify that a custom action is used correctly in the JSP page relative to other actions. After the JSP container has checked everything it can on its own and has used the TagLibraryValidator classes for all libraries used in the page, it looks for TagExtraInfo declarations for each custom action element used in the page: <tag>
<name>myOptionalAttributesAction</name>
<tag-class>com.foo.MyOptionalAttributesTag</tag-class>
<tei-class>com.foo.MyOptionalAttributesTEI</tei-class>
...
</tag>
If it finds a <tei-class> element for an action, the container creates a TagData instance with the attribute values specified in the action element and calls the TagExtraInfo validate( ) method: public ValidationMessage[] validate(TagData data) {
ValidationMessage[] vms = null;
List errors = new ArrayList( );
// Mutually exclusive attributes: can't mix attr1 and attr2
if (data.getAttribute("attr1") != null &&
data.getAttribute("attr2" != null) {
errors.add(new ValidationMessage(null,
"'attr1' and 'attr2' are mutually exclusive"));
}
// Dependent optional attributes: attr3 requires attr4
if (data.getAttribute("attr3") != null &&
data.getAttribute("attr4" == null) {
errors.add(new ValidationMessage(null, "'attr3' requires 'attr4'"));
}
if (errors.size( ) != 0) {
vms = new ValidationMessage[errors.size( )];
errors.toArray(vms);
}
return vms;
}
A TagExtraInfo subclass uses the TagData argument to verify that all attribute dependencies are okay, as in this example, and returns a ValidationMessage[] to report the errors, just as a TagLibraryValidator. The TagExtraInfo instance doesn't have access to the jsp:id attribute, though. A smart container can still produce a friendly error message including the element location, since it knows for which action element it called the validate( ) method. |
| [ Team LiB ] |
|