|
|
||||||||||||
by Felipe Leme | ||||||||||||
| ||||||
In order to create a library of custom tags (a taglib), we need to follow at least two steps:
The TLD defines properties for the taglib as a whole (such as its URI and description) and for its individual tags (such as name and attributes). It can also define validation rules that dictates how the tags must be used: if a JSP page using the taglib does not follow the rules, it will not be compiled.
Some of these rules are well known to taglib developers:
<body-content> element.<required> element).<rtexpvalue> element).Besides these "popular" rules, the JSP specification allows two other types of compile-time validation:
In this article we explain how to use these two validation techniques.
In order to explain how these validations work, let's first create a set of custom tags to be used throughout the article.
Our first tag is called today and it displays the current date, formatted according to a given locale. The locale can be set by two different ways:
localeCode, which is a String object that represents the locale's language and country (like en_US or pt_BR).java.util.Locale object through the attribute locale.Here is a JSP page example using this tag:
<%@ taglib prefix="helper"
uri="http://felipeal.net/tags/helper.tld"%>
<%@ page import="java.util.Locale" %>
Today in Brazil: <br>
<br>
<helper:today localeCode="pt_BR"/><br>
<br>
Today in the US:<br>
<br>
<helper:today locale="<%=Locale.US%>"/><br>
<br>
And its output:
Today in Brazil:
Terça-feira, 27 de Janeiro de 2004
Today in the US:
Tuesday, January 27, 2004
The other tag is called pageGuard and it is used to guarantee that a page can only be accessed by logged users with permission to do so (similar to the page-guard tag described in Core J2EE Patterns). As this is a site-specific tag, it belongs to a taglib called mySite. Also, it has only one attribute, role, which is a string representing a role the logged user must have to access the page (how a logged user is represented is beyond the scope of this article). The idea here is that all HTML and JSP code in every page of our site is surrounded by this tag, as shown above:
<%@ taglib prefix="mySite" %>
uri="http://felipeal.net/tags/mySite.tld"
<mySite:pageGuard role="admin"/>
<html>
<!-- HTML and JSP code goes here -->
</html>
Here are the TLD fragments that define these two custom tags:
<tag>
<name>today</name>
<tag-class>article.tags.TodayTag</tag-class>
<body-content>EMPTY</body-content>
<description>
Display current time according to a locale
</description>
<attribute>
<name>localeCode</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>locale</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
<tag>
<name>pageGuard</name>
<tag-class>article.tags.PageGuardTag</tag-class>
<body-content>EMPTY</body-content>
<description>
Checks for authorization to access
a page's content
</description>
<attribute>
<name>role</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
Let's go back to our first tag, today. That tag requires a Locale object, which must be set either by the locale or localeCode attributes. But what would happen if both attributes are set -- or none at all? In the former case, the behavior would depend on the tag handler's implementation; in the latter, it would probably raise a NullPointerException.
What should we do to force usage of only one of these attributes? We cannot explicitly define this kind of validation in the TLD, but we can do it implicitly through a Tag Extra Info (TEI), which is defined by the tei-class element in the TLD:
<tag>
<name>today</name>
<tag-class>article.tags.TodayTag</tag-class>
<tei-class>article.tags.TodayTEI</tei-class>
<body-content>EMPTY</body-content>
<-- other attrs. omitted as they didn't change -->
</tag>
To create a TEI, we must extend javax.servlet.jsp.tagext.TagExtraInfo and override the method boolean isValid(TagData data), where TagData represents the translation-time information about the tag attributes and their values. In our case, we can get a Enumeration with all of the attributes currently set in the tag and return true only if just one of the attributes (locale or localeCode) is being used:
public boolean isValid( TagData tagData ) {
Enumeration enum = tagData.getAttributes();
// flag indicating if the locale was set
// (through localeCode or locale)
boolean hasLocale = false;
while ( enum.hasMoreElements() ) {
String attribute =
(String) enum.nextElement();
if ( attribute.equals("locale") ||
attribute.equals("localeCode") ) {
if ( ! hasLocale ) {
// found the first attribute
hasLocale = true;
} else {
// found the 2nd attr.: invalid usage
System.err.println(
">>>>>>>> Date tag has both locale" +
"and localeCode attributes" );
return false;
} // if ! hasLocale
} // if equals
} // while
// if no attribute was found, returns false
if ( ! hasLocale ) {
System.err.println(
">>>>>>>> Date tag must have either " +
"locale or localeCode attribute" );
return false;
}
// otherwise, the tag is valid
return true;
}
If we try to use the tag without setting locale or localeCode, for instance, we get the following message (in a server running Tomcat 4.1.29):
HTTP Status 500 -
type Exception report
message
description The server encountered an internal
error () that prevented it from fulfilling
this request.
exception
org.apache.jasper.JasperException:
/testToday.jsp(15,0) jsp.error.invalid.attributes
at org.apache.jasper.compiler.DefaultErrorHandler.
jspError(DefaultErrorHandler.java:94)
at org.apache.jasper.compiler.ErrorDispatcher.
dispatch(ErrorDispatcher.java:428)
...
...
This message means that the page was not compiled due to a jsp.error.invalid.attributes error. It is not a helpful message, though, as it does not inform us which attributes were invalid. This happens because the isValid() method only checks if the tag attributes are invalid, not why they are invalid. Fortunately, that situation has changed in JSP 2.0, which defines a method called ValidationMessage[] validate(TagData data). So our new method would be:
public ValidationMessage[] validate(
TagData tagData ) {
Enumeration enum = tagData.getAttributes();
boolean hasLocale = false;
while ( enum.hasMoreElements() ) {
String attr = (String) enum.nextElement();
Object value = tagData.getAttribute( attr );
System.out.println(
"attr(" + tagData.getId() + "): " +
attr + " value: " + value );
if ( attr.equals("locale") ||
attr.equals("localeCode") ) {
if ( hasLocale ) {
return validationError(
tagData.getId(),
"Date tag has both locale and " +
"localeCode attributes" );
} else {
hasLocale = true;
} // if hasLocale
} // if equals
} // while
if ( ! hasLocale ) {
return validationError(
tagData.getId(),
"Date tag must have either locale " +
"or localeCode attribute" );
}
return null;
}
And the output on a server running Tomcat 5.0.16:
HTTP Status 500 -
type Exception report
message
description The server encountered an internal
error () that prevented it from fulfilling
this request.
exception
org.apache.jasper.JasperException:
/testToday.jsp(16,0)
Validation error messages from TagExtraInfo
for helper:today
Date tag must have either locale or
localeCode attribute
Now let's go back to the pageGuard tag. This tag is crucial to our site security mechanism, and therefore it must be used in all JSP pages. So how can we assure the tag is included in all of our pages? One way would be to define a Tag Library Validator (TLV) for the mySite taglib.
If a TLV is defined for a taglib, every time a JSP page using that taglib is compiled, the JSP engine "asks" the TLV to validate the page, by calling its ValidationMessage[] validate(String prefix, String uri, PageData page) method. The parameters prefix and uri indicate how the taglib is mapped (i.e., how the taglib directive was used in the page) and page represents the XML view of the JSP being validated. If the page is not valid, that method should return a list describing what makes it invalid; otherwise it should return null or an empty list.
With these objects in hand, we can parse the XML view and do our validation, as shown below:
package net.felipeal.view.taglib;
import javax.servlet.jsp.tagext.*;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class MandatoryPageGuardTLV
extends TagLibraryValidator {
public ValidationMessage[] validate(
String prefix, String uri,
PageData pageData) {
// create a handler that will parse the XML
MyHandler handler = new MyHandler( prefix );
// parse the page...
try {
SAXParserFactory factory =
SAXParserFactory.newInstance();
factory.setValidating( true );
SAXParser parser = factory.newSAXParser();
parser.parse( pageData.getInputStream(),
handler );
} catch (Exception e) {
getValidationMessages(
e.getClass().getName() +
" parsing document: " + e.getMessage() );
}
// ... and return the error (if any)
return getValidationMessages(
handler.getErrorMessage() );
}
// MyHandler is a XML parser that will check if
// the pageGuard tag is present in the page
private class MyHandler extends DefaultHandler {
private String guardTag = null;
private boolean foundGuard = false;
private String errorMessage = null;
public MyHandler(String prefix) {
this.guardTag = prefix + ":pageGuard";
}
public void startElement(
String uri, String localName,
String qName, Attributes attributes )
throws SAXException {
// found the guard tag
if ( qName.startsWith(this.guardTag) ) {
this.foundGuard = true;
} //if
} // startElement
String getErrorMessage() {
return this.errorMessage;
}
public void endDocument() throws SAXException {
if (!this.foundGuard) {
this.errorMessage = this.guardTag +
" is mandatory";
} // if
} // endDocument
} // private class
// helper method
public static ValidationMessage[]
getValidationMessages( String msg ) {
if ( msg == null ) {
return null;
}
ValidationMessage[] array =
new ValidationMessage[] {
new ValidationMessage( null, msg )
};
return array;
}
} // public class
It is also necessary to change the TLD to include a validator:
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>mySite</short-name>
<uri>http://felipeal.net/taglib/mySite</uri>
<validator>
<validator-class>
article.tags.MandatoryPageGuardTLV
</validator-class>
</validator>
<tag>
<name>pageGuard</name>
<-- remainder omitted as it did not change -->
Again, here is a JSP page that breaks the rule:
<%@ taglib prefix="mySite"
uri="http://felipeal.net/taglib/mySite" %>
<%-- pageGuard tag should be here
<mySite:pageGuard role="admin"/>
--%>
HTML code goes here...
And its output in Tomcat 4.1.29:
HTTP Status 500 -
type Exception report
message
description The server encountered an internal
error () that prevented it from fulfilling
this request.
exception
org.apache.jasper.JasperException:
jsp.error.tlv.invalid.page
null: mySite:pageGuard is mandatory
at org.apache.jasper.compiler.DefaultErrorHandler.
jspError(DefaultErrorHandler.java:105)
at org.apache.jasper.compiler.ErrorDispatcher.
dispatch(ErrorDispatcher.java:430)
...
...
It is also possible to pass initialization parameters to the validator. If you take a closer look at the code above, you see that the guard tag's name is hardcoded to pageGuard. Using initialization parameters, we could eliminate this restriction, making our TLV a reusable component. The new TLD definition would looks like this:
<validator>
<validator-class>
article.tags.MandatoryPageTagTLV
</validator-class>
<init-param>
<param-name>tagName</param-name>
<param-value>myPageGuard</param-value>
</init-param>
</validator>
And the new code:
public class MandatoryPageGuardTLV
extends TagLibraryValidator {
private String tagName = null;
private static final String
DEFAULT_TAGNAME = "pageGuard";
public void setInitParameters(Map parameters) {
this.tagName =
(String) parameters.get("tagName");
if ( this.tagName == null ) {
this.tagName = DEFAULT_TAGNAME;
}
}
public ValidationMessage[] validate(
String prefix, String uri, PageData pageData) {
// create the handler that will parse the XML
MyHandler handler =
new MyHandler( prefix + ":" + this.tagName );
// remainder of method omitted as
// it did not change
}
// remainder of class omitted as
// it did not change
private class MyHandler extends DefaultHandler {
public MyHandler( String guardTag ) {
this.guardTag = guardTag;
}
}
// remainder of class omitted as
// it did not change
Finally, it is important to mention that we can validate only the pages that use the taglib. If the page author does not use the taglib in a page, there is nothing we can do. This situation also changed in JSP 2.0, where you can set a prelude to a group of JSP pages. The prelude is automatically included at the beginning of every page in the group, and hence you can set the taglib directive there.
The JSP Standard Tag Library (JSTL) (see the java.net article "Practical JSTL," parts 1 and 2, for more details) is well known for its taglibs and EL support. What most people do not know is that JSTL also provides two TLVs that can be reused in their own taglibs:
These TLVs are very useful to enforce page authors to write MVC-compliant pages, as they can restrict what can and cannot be used in a JSP 1.2 page. (In JSP 2.0, some of these restrictions can be set at the web-descriptor level, without the need for a TLV.)
ScriptFreeTLV takes four Boolean parameters: allowScriptlets, allowDeclarations, allowExpressions, and allowRTExpressions. If you want to forbid any JSP element (except JSP actions) in your pages, you must set all parameters to false, as shown below:
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>tlv1</short-name>
<uri>http://felipeal.net/tags/jsp_free</uri>
<validator>
<validator-class>
javax.servlet.jsp.jstl.tlv.ScriptFreeTLV
</validator-class>
<init-param>
<param-name>allowScriptlets</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowDeclarations</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowExpressions</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowRTExpressions</param-name>
<param-value>false</param-value>
</init-param>
</validator>
<tag>
<name>Dummy tag</name>
<tag-class>dev.null</tag-class>
</tag>
</taglib>
Notice that it is necessary to define at least one tag element, even if it is a dummy one. The JSP page below disobeys the TLV rules:
<%@ taglib prefix="tlv1"
uri="http://felipeal.net/tags/jsp_free" %>
<%@ taglib prefix="c_rt"
uri="http://java.sun.com/jstl/core_rt" %>
Testing jsp_free
1 RT expression: <c_rt:out value="<%=request%>"/>
2 expressions: <%=%><%=%><br>
3 scriptlets: <%%><%%><%%><br>
4 declarations<%!%><%!%><%!%><%!%><br>
Consequently, it cannot be compiled, and its access results in an internal error:
500 Internal Server Error
Error: Validator
javax.servlet.jsp.jstl.tlv.ScriptFreeTLV reports:
JSP page contains 4 declarations, 3 scriptlets,
2 expressions, 1 request-time attribute value.
Similarly, PermittedTaglibsTLV takes as a parameter (permittedTaglibs) a list of space-delimited URIs representing the permitted taglibs in a page (besides the taglib using the TLV, of course), as shown in the TLD below:
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>tlv2</short-name>
<uri>
http://felipeal.net/tags/taglibs-restricted
</uri>
<validator>
<validator-class>
javax.servlet.jsp.jstl.tlv.PermittedTaglibsTLV
</validator-class>
<init-param>
<param-name>permittedTaglibs</param-name>
<param-value>
http://java.sun.com/jstl/core_rt
http://java.sun.com/jstl/core
</param-value>
</init-param>
</validator>
<tag>
<name>dummy</name>
<tag-class>dev.null</tag-class>
</tag>
</taglib>
So if a JSP page tries to use a taglib other than core, core_rt, or tlv2 (which is the taglib using the TLV):
<%@ taglib prefix="tlv2"
uri="http://felipeal.net/tags/taglibs-restricted"%>
<%@ taglib prefix="tlv1"
uri="http://felipeal.net/tags/jsp_free"%>
Testing taglibs_restriction
it generates an error like this:
Error: Validator
javax.servlet.jsp.jstl.tlv.PermittedTaglibsTLV
reports:
taglib tlv2 (http://felipeal.net/tags/jsp_free)
allows only the following taglibs to be imported:
[http://java.sun.com/jstl/core_rt,
http://java.sun.com/jstl/core]
Taglib validation is a valuable tool to the taglib developer. Still, it is a technology that has not been widely adopted yet (as far as I know, for instance, JSTL is one of the few taglibs out there that uses TLVs). In my opinion, the slow adoption has two main causes:
With this article, I hope this situation is improved a little bit.
Felipe Leme has worked professionally with Java since 1996, and in the last years had became an active enthusiastic of the technology.
View all java.net Articles.
|
|