|
Articles Index
Use and extend the open source Struts JSP tag library
By Thor Kristmundsson; Reprinted from JavaWorld
December 2000
One great benefit of JavaServer Pages (JSP) and J2EE is that they enable the developer
community to speak a common language. The downside of those technologies
is that they sometimes require a lot of mindless coding more suitable for
a machine than a human being. That is particularly true in today's big
e-commerce projects with their huge teams and tight deadlines. In such
situations everyone reverts to simple and safe procedures -- the most
advanced form of reuse being cut and paste. There is simply no time for
experimentation. Fortunately, JSP allows more advanced forms of reuse to
be imported from third parties in the form of custom tag libraries. This
article focuses on one such library, the open source Struts tag library,
and presents extensions that ease your coding effort.
The Struts package, which is part of the open source Jakarta project,
provides a well-thought-out MVC (model-view-controller) framework that
many projects would do well in adapting at the start of their Website
development. The custom tags represent the view part of that framework,
but it is perfectly feasible to use the tags without the model and controller
part.
Most of the tags defined by Struts help associate bean properties with
form fields. That greatly reduces the complexity of writing forms that
remember all user choices between requests. That is particularly true
for complex tags such as the standard select tag, in which
plain JSP code to restore the user's choice can get quite bulky.
Even with Struts's custom tags there remain two time-consuming and tedious
tasks you must do for the page bean: you must write properties that hold the
user input between requests and implement user input validation and error
message display. This article presents techniques to deal with those tasks.
The use of automatic properties alleviates the creation of all
page bean properties that primarily serve to remember user input when the
page has to be redisplayed, either because it had errors or because the user
wanted to review a previous step in a multipage transaction. Validation
with regular expressions greatly simplifies the validation of user input
and the display of appropriate error messages. The amount of code needed to
implement those techniques is surprisingly small and can be found in the code
listings in this article.
The following image shows a page that uses both techniques:
Figure 1. Webpage using both automatic properties and validation with regular expressions (40 KB) |
Listing 1 shows the JSP behind this form. Some table and font
formatting has been removed for clarity, so the page would look
a bit jagged and less colorful but otherwise just like the above figure.
Listing 1. transfer.jsp: The transfer form
<%@ taglib uri="/WEB-INF/struts.tld" prefix="struts" %>
<jsp:useBean id="transfer" scope="session" class="com.bank.PageBean"/>
<%
org.apache.struts.util.BeanUtils.populate(transfer, request);
if(request.getParameter("marker") == null)
// initialize a pseudo-property
transfer.set("days", java.util.Arrays.asList(
new String[] {"1", "2", "3", "4", "31"}));
else
if(transfer.validate(request))
%><jsp:forward page="transferConfirm.jsp"/><%
%>
<html>
<head>
<title>MyBank - Transfer</title>
</head>
<body>
<H2> Money Transfer </H2>
<struts:form name="transfer" type="com.bank.PageBean"
action="transfer.jsp" method="post">
<struts:hidden property="marker"/>
Receiver:
<struts:text property="receiver" pattern="@personName"
errorMessage="@personName"/>
<%= transfer.getError("receiver") %><br>
Account:
<struts:text property="destAccount" pattern="/^[0-9]{7,9}$/"
errorMessage="Account can have 7 and 9 digits"/>
<%= transfer.getError("destAccount") %><br>
Amount:
<struts:text property="amount" pattern="@amount"
errorMessage="Amount can have 8 digits plus two decimals" />
<%= transfer.getError("amount") %><br>
Date:
<struts:select property="day">
<struts:options property="days"/>
</struts:select><br>
<struts:submit value="submit"/>
</struts:form>
</body>
</html>
The page bean, specified in the useBean tag and
in the Struts form tag, is completely generic. It
knows nothing about that particular form, neither the names of
the fields nor the constraints on their contents. All that information
comes from the JSP page. In many cases, though, the basic bean would
be extended to, for example, communicate with the back end, perhaps
through JDBC, CORBA, or EJBs.
Let's take a short walk through the JSP code in Listing 1.
First the taglib tag at the top says where to
find a file describing the syntax of any tags prefixed with
"struts:". Then the useBean tag creates a scripting
variable named "transfer" of class PageBean and
associates it with any previously used bean of the same name,
class, and scope. If no such bean exists, then it is created.
Next you see a scriptlet that represents the controller code
for that page. Having the controller code on the JSP page goes
against the separation of code and content but helps keep this
example short. The form on the page thus uses the page itself
as a target, so this same page will be called when the form
is submitted.
The scriptlet starts by populating the bean properties with
the request data (if any). Then the presence of the marker
parameter is used to detect if that is the initial page request or a
subsequent form submit request. If that is the initial request, the
days property for the options tag is initialized.
If, on the other hand, that is a subsequent submission of the form, then
a validation function is called and, if it succeeds, control is forwarded
to the next page in the transaction.
The Struts tags are next. The form tag specifies that
the transfer bean is where all the enclosed field tags
should look for properties. Each of the three text tags
specifies which transfer property should be used for
initialization, to which regular expression pattern the user's input
should be compared, and what should be displayed if the input doesn't
match the pattern. A pattern or an error message starting with "@"
denotes a predefined string. If the users input fails to match the
pattern, the expressions
<%= transfer.getError(fieldname) %>
will display the error messages when the page is redisplayed for correction. Finally, the select tag lets the user select any item in the list specified in the options tag. The initial selection is the value of the day property, which also receives the value that the user selects.
Let's now examine the changes you need to make to the Struts
library for that to work.
Automatic Properties
Automatic properties are to JSP what dynamic variables are to
a programming language. In a programming language that supports
them, you can use dynamic variables without declaring them first.
Similarly, you can use automatic properties on a JSP page without
defining them with set and get methods.
Automatic properties are not really properties in the Java sense,
but they serve the same purpose as properties in JSP pages. They
reduce the need for the tiresome triad of a getter, setter, and
member variable to code a property that corresponds to a form field.
Instead, those entries are created automatically based on the request.
The Struts tags, with the modifications made here, will then treat
that data as properties. If initialization is required before the
page is first sent to the user, you can initialize in a constructor,
in the bean's initialization block or, as in the example above,
directly in the JSP. That last approach, however, mixes code and
content, and it isn't generally recommended.
Automatic properties function only within constructs that belong
to Struts. Thus the getProperty and setProperty
tags can't see them. Instead, you must use JSP expressions
(for getProperty) and statements (for setProperty).
In particular, instead of using the setProperty tag
with property="*" to map request parameters to properties
by name, you call Struts's populate function to do it.
To specify the functionality that an automatic bean must provide,
let's jot down the interface shown in Listing 2. In short, an
AutoBean interface implementation must be able to
associate a value with a property name. Being able to get and set
objects, and not only strings, is convenient because some Struts
tags require collections as attributes such as the options
tag for the days in the example above.
Listing 2. AutoBean.java: The AutoBean interface
package com.bank;
public interface AutoBean {
public void set(String property, Object value);
public String get(String property);
public Object getObject(String property);
}
To make Struts work with automatic properties, you must perform
some minor surgery on the BeanUtils class. The change
is nondestructive in that it makes no difference for beans that
don't implement the AutoBean interface. Essentially,
you implant code to get your property if the bean has no
real property with the given name. Similarly, you implant your code
in the populate function to use your set
function when there is no real property setter for a given name.
Listing 3 shows those changes:
Listing 3. BeanUtils.java: Implanting AutoBeans functionality in Struts
// new function for autobeans
public static Object getAutoPropertyValue(Object bean, String name)
throws Exception {
if(bean instanceof com.bank.AutoBean)
return ((com.bank.AutoBean) bean).getObject(name);
throw new NoSuchMethodException("Unknown property '" + name + "'");
}
public static Object getPropertyValue(Object bean, String name)
throws Exception {
// Retrieve the property getter method for the specified property
PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
if (descriptor == null)
// throw new NoSuchMethodException("Unknown property '" + name + "'");
return getAutoPropertyValue(bean, name);
...
public static void populate(Object bean, Hashtable properties)
throws Exception {
...
if (setter == null) {
if(bean instanceof com.bank.AutoBean)
((com.bank.AutoBean) bean).set(name, value);
continue;
}
...
An implementation of the AutoBean interface
is presented in the last section of this article. Let's
now turn to the issue of validating user input.
Validating User Input with Regular Expressions
One of the mindless chores facing the JSP programmer is
the writing and maintenance of trivial user input validation.
Why not use the power of regular expressions to deal with that?
And why not specify that right in the tag instead of having to
write customized code around each and every input? That will
eliminate the great majority of validation chores. For some
tricky inputs, particularly when the validity depends on a combination
of fields, you still must write validation code.
The open source Jakarta PerlUtil class makes working
with regular expressions deceptively simple. The static method
PerlUtil.match(pattern, string) does the trick.
By introducing attributes for a pattern and an error message at the appropriate point in the inheritance hierarchy, you endow all field tags with those attributes at once. When you are done, you can write something like this:
<struts:text
property="account"
pattern="/^[0-9]{7,9}$/"
errorMessage="requires 7 to 9 digits"/>
Somewhere in the vicinity, often right next to the field,
you can place a JSP expression to show an eventual error:
<%= bean.getError("account") %>;
To nail down the functionality of validating page beans,
let's write the small interface shown in Listing 4.
Listing 4. Validator.java: The Validator interface
package com.bank;
public interface Validator {
public void setPattern(String field, String pattern);
public void setErrorMessage(String field, String message);
public String getError(String field);
public boolean validate(javax.servlet.http.HttpServletRequest request);
}
When everything is in place the setPattern and
setErrorMessage methods will be called from Struts
when tags using the pattern and errorMessage
attributes are processed at page construction time. Those methods should
simply store the association between the form field names on one hand
and the patterns and the error messages on the other. The validate
method will be called from the controller code after the form is submitted,
and it should match the field values of the incoming request with the
patterns stored earlier. If the match fails, the associated error messages
should be tied to the field. The getError will be called from
the JSP, and it should return that error message or an empty string if the
match succeeded. Finally, the setErrorMessage method allows
you to use the same mechanism to display other error messages on the page.
All that is required is a call to setErrorMessage(name, message)
function in the code in which the error is discovered.
Listing 5 shows the change to the Struts BaseFieldTag
class that enables it to work with automatic validation. Apart from
properties for the new attributes, you implant a call to give the attributes
and their values to the page bean just before the page is constructed in the doStartTag method. Added code is shown in bold font.
Listing 5. BaseFieldTag.java: Implanting validator functionality in Struts
private String pattern;
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
private String errorMessage = null;
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
...
public int doStartTag() throws JspException {
Object pagebean = pageContext.findAttribute(name);
if(pagebean instanceof com.bank.Validator) {
((com.bank.Validator) pagebean).setPattern(property, pattern);
((com.bank.Validator) pagebean).setErrorMessage(property, errorMessage);
}
...
The Page Bean
The PageBean shown in Listing 6 implements both
the interfaces and can serve as a base class for other
PageBeans. In some cases, it may even prove
sufficient by itself.
The implementation of the Validator interface
simply stores the given patterns and messages in maps. As a
bonus, it adopts the convention of interpreting patterns and
messages starting with @ as references to predefined strings.
Using predefined tags can help avoid typing errors on JSP pages
and help build up a repository of tested regular expression patterns.
An exception should be thrown if the predefined string isn't found,
but I've left that out in the interest of brevity.
The validation itself predictably iterates over the fields
(parameters) of the given request, locates any pattern that
may have been associated with each field during the page
construction (by a call to setPattern), and
then calls match to compare the field value
with the pattern. If the field value doesn't match the
pattern, the validation method looks up the error message
for the field and stores it in the errors map
with the field name as key. From there, you can access it
from the JSP by calling the getError(fieldName)
method.
The implementation of the AutoBean interface is
even simpler. The three functions directly access a member
Map to set and get
the pseudoproperties.
Listing 6. PageBean.java: The base class for page beans
package com.bank;
import java.util.*;
import org.apache.oro.text.perl.*;
public class PageBean implements Validator, AutoBean {
// implementing Validator
private Perl5Util perl = new Perl5Util();
private Map patterns = new Hashtable();
private Map messages = new Hashtable();
private Map errors = new Hashtable();
private static Map namedPatterns = new Hashtable();
private static Map namedMessages = new Hashtable();
public void setPattern(String field, String pattern) {
if(pattern == null || pattern.equals(""))
return;
if(pattern.charAt(0) == '@')
pattern = (String) namedPatterns.get(pattern);
patterns.put(field, pattern);
}
public void setErrorMessage(String field, String message) {
if(message == null || message.equals(""))
return;
if(message.charAt(0) == '@')
message = (String) namedMessages.get(message);
messages.put(field, message);
public boolean validate(javax.servlet.http.HttpServletRequest request) {
errors.clear();
Enumeration e = request.getParameterNames();
while(e.hasMoreElements()) {
String field = (String) e.nextElement();
validateField(field, request.getParameter(field));
}
return errors.isEmpty();
}
private boolean validateField(String field, String value) {
String pattern = (String) patterns.get(field);
if(pattern == null)
return true;
if(perl.match(pattern, value))
return true;
errors.put(field, messages.get(field));
return false;
}
public String getError(String name) {
String result = (String) errors.get(name);
if(result == null)
return "";
return result;
}
static {
// Predefined patterns and messages
namedPatterns.put("@amount", "/^[1-9][0-9]{0,8}(,[0-9]{1,2})?$/");
namedPatterns.put("@account", "/^[0-9]{7,9}$/");
namedPatterns.put("@personName", "/^[a-zA-Z ]{0,30}$/");
namedMessages.put("@amount", "Amount can have 8 digits plus 2 decimals");
namedMessages.put("@account", "Account must have between 7 and 9 digits");
namedMessages.put("@personName", "Name can have up to 30 alphabetic characters");
}
// implementing AutoBean
private Map properties = new Hashtable();
public void set(String property, Object value) {
properties.put(property, value);
}
public String get(String property) {
return properties.get(property).toString();
}
public Object getObject(String property) {
return properties.get(property);
}
}
One small task remains to be done. You must change the tag
library definition file, struts.tld, to reflect the new
attributes. Listing 7 shows how you insert the new
attributes into the text tag. If you want to use that with
other tags, such as the password tag or the text area tags,
then you must also change them.
Listing 7. Changes to struts.tld
...
<tag>
<name>text</name>
<tagclass>org.apache.struts.taglib.TextTag</tagclass>
<attribute>
<name>errorMessage</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>pattern</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
...
Conclusion
Hopefully, the techniques presented here will help
diminish the amount of robotic coding concomitant with
JSP development and give you some time to deal with more
challenging problems.
Resources
About the Author
Thor
Kristmundsson is a server-side Java developer and
consultant living in Aalborg, Denmark. He has a decade
of experience in development in various environments from
Prolog to Java. As an ATG senior consulting engineer, Thor
helps European clients create quick and clean Websites in Dynamo.

Reprinted with permission from the December 2000 edition of JavaWorld magazine. Copyright ITworld.com, Inc., an IDG Communications company. Register for editorial e-mail alerts.
|