Almost every web application has to accept input from users. Some examples of user input are usernames and passwords, credit card information, and billing and shipping address information. HTML provides the necessary components to render the input fields in a browser, including text boxes, radio buttons, checkboxes, and buttons. When you're building these types of pages, you must nest the input components inside HTML form elements. Example 7-1 illustrates a very basic sign-in page, similar to the one used in the Storefront application.
<html>
<head>
<title>Example 7-1. OReilly Struts Book</title>
<link rel="stylesheet" href="stylesheets/main.css" type="text/css">
</head>
<body>
<form method="post" action="/action/signin">
<!-- The table layout for the email and password fields -->
<table BORDER="0" cellspacing="0" cellpadding="0">
<tr>
<td>Email: </td>
<td> </td>
<td>
<input type="text" name="email" size="20" maxlength="20"/>
</td>
</tr>
<tr>
<td>Password:</td>
<td> </td>
<td class="alignformslist">
<input type="text" name="password" size="20" maxlength="25"/>
</td>
</tr>
<!-- The table layout for the signin button -->
<table width="250" border="0">
<tr>
<td>
<input type="submit" name="Submit" value="Signin" class="Buttons">
</td>
</tr>
</table>
</form>
</body>
</html>
When the user presses the Signin button on the HTML form from Example 7-1, the values within the fields are submitted along with the HTTP request. The server application can retrieve the values that were entered, perform input validation on the data, and then pass the data to another component in the application where the actual authentication process occurs. If the input data fails the input validation rules, the application should return to the previous location, redisplay some or all of the values entered, and display an error message indicating that the login attempt failed.
Manually performing all of this functionality, retrieving the values, executing the validation, and displaying error messages on failure can be a daunting task. This type of behavior is performed in many places throughout a web application, and it would be nice to have it taken care of by the framework and to be able to reuse it across applications.
Fortunately, the Struts framework does provide this functionality and will handle these tasks on behalf of your application. The Struts framework relies on the org.apache.struts.action.ActionForm class as the key component for handling these tasks.
The ActionForm class is used to capture input data from an HTML form and transfer it to the Action class. Because users often enter invalid data, web applications need a way to store the input data temporarily so that it can be redisplayed when an error occurs. In this sense, the ActionForm class acts as a buffer to hold the state of the data the user entered while it is being validated. The ActionForm also acts as a "firewall" for your application in that it helps to keep suspect or invalid input out of your business tier until it has been scrutinized by the validation rules. Lastly, when data is returned from the business tier, a particular ActionForm can be populated and used by a JSP page to render the input fields for an HTML form. This allows more consistency for your HTML forms, as they always pull data from the ActionForm, not from different JavaBeans.
When the user-input data does pass input validation, the ActionForm is passed into the execute( ) method of the Action class. From there, the data can be retrieved from the ActionForm and passed on to the business tier.
You don't have to declare an ActionForm for every HTML form in your application. The same ActionForm can be associated with one or more action mappings. This means that they can be shared across multiple HTML forms. For example, if you had a wizard interface where a set of data was entered and posted across multiple pages, you could use a single ActionForm to capture all of this data, a few fields at a time.
ActionForms can have two different levels of scope: request and session. If request scope is used, the ActionForm is available only until the end of the request/response cycle. Once the response has been returned to the client, the ActionForm and the data within it are no longer accessible.
If you need to keep the form data around for longer than a single request, you can configure an ActionForm to have session scope. This might be necessary if your application captures data across multiple pages, like a wizard dialog does. An ActionForm that has been configured with session scope will remain in the session until it's removed or replaced with another object, or until the session times out. The framework doesn't have a built-in facility for automatically cleaning up session-scoped ActionForm objects. As with any other object placed into the HttpSession, it's up to the application to routinely perform cleanup on the resources stored there. This is slightly different from what happens with objects placed into request scope, because once the request is finished, they no longer can be referenced and so can be reclaimed by the garbage collector.
Unless you need to hold the form data across multiple requests, you should use request scope for your ActionForm objects.
|
When the controller receives a request, it attempts to recycle an ActionForm instance from either the request or the session, depending on the scope that the ActionForm has in the action element. If no instance is found, a new instance is created.
Section 3.5.1 in Chapter 3 described the steps the framework takes when an ActionForm is being used by an application. From these steps, it's easy to get a picture of the lifecycle of an ActionForm. Figure 7-4 illustrates the main steps taken by the framework that have some effect on the ActionForm.
Figure 7-4 shows only the steps that are relevant to an ActionForm, not all those that a request goes through during request processing. Notice that when an ActionForm detects one or more validation errors, it performs a forward back to the resource identified in the input attribute. The data that was sent in the request is left in the ActionForm so that it can be used to repopulate the HTML fields.
The ActionForm class provided by the Struts framework is abstract, and you need to create subclasses of it to capture your application-specific form data. Within your subclass, you should define a property for each field that you want to capture from the HTML form. For example, suppose you want to capture the email and password fields from a form, similar to the one in Example 7-1. Example 7-2 illustrates the LoginForm for the Storefront application that can be used to store and validate the email and password fields.
package com.oreilly.struts.storefront.security;
import javax.servlet.http.HttpServletRequest;
import org.apache.struts.action.*;
/**
* Form bean for the user signin page.
*/
public class LoginForm extends ActionForm {
private String email = null;
private String password = null;
public void setEmail(String email) {
this.email = email;
}
public String getEmail( ) {
return (this.email);
}
public String getPassword( ) {
return (this.password);
}
public void setPassword(String password) {
this.password = password;
}
/**
* Validate the properties that have been sent from the HTTP request,
* and return an ActionErrors object that encapsulates any
* validation errors that have been found. If no errors are found, return
* an empty ActionErrors object.
*/
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
ActionErrors errors = new ActionErrors( );
if( getEmail() == null || getEmail().length( ) < 1 ) {
errors.add("email", new ActionError("security.error.email.required"));
}
if( getPassword() == null || getPassword().length( ) < 1 ){
errors.add("password", new ActionError("security.error.password.required"));
}
return errors;
}
public void reset(ActionMapping mapping, HttpServletRequest request) {
/** Because this ActionForm should be request-scoped, do nothing here.
* The fields will be reset when a new instance is created. We could
* have just not overriden the parent reset( ) method, but we did so
* to provide an example of the reset( ) method signature.
*/
}
}
When the form is submitted, an instance of the LoginForm will be created and populated from the request parameters. The framework does this by matching each request parameter name against the corresponding property name in the ActionForm class.
|
The RequestProcessor may call the validate( ) method for every request. Whether it's called depends on two things. First, an ActionForm must be configured for an action mapping. This means that the name attribute for an action element must correspond to the name attribute of one of the form-bean elements in the configuration file.
The second condition that must be met for the RequestProcessor to invoke the validate( ) method is that the validate attribute in the action mapping must have a value of true. The following fragment shows an action element that uses the LoginForm from Example 7-2 and meets both requirements:
<action
path="/signin"
type="com.oreilly.struts.storefront.security.LoginAction"
scope="request"
name="loginForm"
validate="true"
input="/security/signin.jsp">
<forward name="Success" path="/index.jsp" redirect="true"/>
<forward name="Failure" path="/security/signin.jsp" redirect="true"/>
</action>
When the signin action is invoked, the framework will populate an instance of a LoginForm using the values it finds in the request. Because the validate attribute has a value of true, the validate( ) method in the LoginForm will be called. Even if the validate attribute is set to false, the ActionForm still will be populated from the request.
|
The validate( ) method may return an ActionErrors object, depending on whether any validation errors were detected; it also can return null if there are no errors. The framework will check for both null and an empty ActionErrors object. This saves you from having to create an instance of ActionErrors when there are no errors. The ActionError class and its parent class, ActionMessage, are discussed later in this chapter.
The reset( ) method has been a bane for much of the Struts user community at one time or another. Exactly when the reset( ) method is called and what should be done within it almost always are misinterpreted. This doesn't mean that one implementation is more correct than another, but many new Struts developers pick up misconceptions about reset() that they have a hard time shaking.
As Figure 7-4 showed, the reset( ) method is called for each new request, regardless of the scope of the ActionForm, before the ActionForm is populated from the request. The method was originally added to the ActionForm class to facilitate resetting Boolean properties back to their defaults. To understand why they need to be reset, it's helpful to know how the browser and the HTML form-submit operation process checkboxes.
When an HTML form contains checkboxes, only the values for the checkboxes that are checked are sent in the request. Those that are not checked are not included as a request parameter. The reset( ) method was added to allow applications to reset the Boolean properties in the ActionForm to false—because false wasn't included in the request, it was possible for the Boolean values to be stuck in the true state.
The reset( ) method in the base ActionForm contains no default behavior, as no properties are defined in this abstract class. Applications that extend the ActionForm class are allowed to override this method and reset the ActionForm properties to whatever state they want. This may include setting Boolean properties to true or false, setting String values to null or some initialized value, or even instantiating instances of other objects that the ActionForm holds on to. For an ActionForm that has been configured with request scope, the framework will create a new instance for each new request; hence, there's not much need to reset the values back to any default state. ActionForms that are configured with session scope are different, however, and this is where the reset( ) method comes in handy.
Once you have created a class that extends ActionForm, you need to configure the class in the Struts configuration file. The first step is to add a new form-bean element to the form-beans section of the file:
<form-beans>
<form-bean
name="loginForm"
type="com.oreilly.struts.storefront.security.LoginForm"/>
</form-beans>
The value for the type attribute must be a fully qualified Java class name that is a descendant of ActionForm.
Once you have defined your form-bean, you can use it in one or more action elements. It's common to share one ActionForm across several actions. For example, suppose there was an admin application that managed the items in the Storefront application. There would need to be an HTML form for adding new items to the system. This might be the createItem action. There also would need to be a getItemDetail action to show the details of an existing item. These HTML forms would look similar, but they might be submitted to different actions. Still, because they contained the same properties, both forms could use the same ActionForm.
To use an ActionForm in an action element, you need to specify a few attributes for each action mapping that uses the ActionForm. These attributes are name, scope, and validate:
<action
path="/signin"
input="/security/signin.jsp"
name="loginForm"
scope="request"
type="com.oreilly.struts.storefront.security.LoginAction"
validate="true">
<forward name="Success" path="/index.jsp" redirect="true"/>
<forward name="Failure" path="/security/signin.jsp" redirect="true"/>
</action>
For more information on the attributes of the action element, see Section 4.7.2 in Chapter 4.
Once you have configured the ActionForm for a particular Action, you can insert values into it and retrieve values from it within the execute( ) method, as Example 7-3 illustrates.
public ActionForward execute( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response )
throws Exception{
// Get the user's login name and password. They should already have
// been validated by the ActionForm.
String email = ((LoginForm)form).getEmail( );
String password = ((LoginForm)form).getPassword( );
// Log in through the security service.
IStorefrontService serviceImpl = getStorefrontService( );
UserView userView = serviceImpl.authenticate(email, password);
UserContainer existingContainer = null;
HttpSession session = request.getSession(false);
if ( session != null ){
existingContainer = getUserContainer(request);
session.invalidate( );
}else{
existingContainer = new UserContainer( );
}
// Create a new session for the user.
session = request.getSession(true);
existingContainer.setUserView(userView);
session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer);
return mapping.findForward(IConstants.SUCCESS_KEY);
}
|
All request parameters that are sent by the browser are strings. This is true regardless of the type that the value will eventually map to in Java. For example, dates, times, Booleans, and other values all are strings when they are pulled out of the request, and they will be converted into strings when they are written back out to the HTML page. Therefore, all of the ActionForm properties where the input may be invalid should be of type String, so that the data can be displayed back to the user in its original form when an error occurs. For example, say a user types in "12Z" for a property expecting an Integer. There's no way to store "12Z" into an int or Integer property, but you can store it into a String until it can be validated. This value can be used to render the input field with the value, so the user can see his mistake. Even the most inexperienced users have come to expect and look for this functionality.
ActionForms Are Not the ModelMany developers get confused when they learn about the ActionForm class. Although it can hold state for an application, the state that it holds should be limited and constrained to the user input that is received from the client, and the ActionForm should hold it only until it can be validated and transferred to the business tier. You've already seen why it's important to separate the model from the presentation tier in an application. Business objects can be persisted and should contain the business logic for an application. They also should be reusable. This set of criteria does not match up well when compared against ActionForms. For one thing, the ActionForm class is tied to the Struts framework and explicitly to a web container, as it imports javax.servlet packages. It would be very difficult to port ActionForm classes to a different type of framework, such as a Swing application. ActionForms are designed exclusively to capture the HTML data from a client, allow "presentation validation" to occur, and provide a transport vehicle for the data back to the more persistent business tier. They also transport data from the business tier forward to the views. Apart from these uses, you should keep the ActionForms separate from your business components. |
Many applications require wizard-like functionality where data is captured across multiple pages. ActionForm s can be used for this but the scope must be set to session to ensure that the data collected from a previous page will be present throughout the set of wizard pages.
When programming this type of functionality into your application, you must be careful to validate only the fields that are ready to be validated. For instance, if you go from page 1 to page 2, only the properties populated from page 1 should be validated. You also must be careful with the reset( ) method. You don't want to reset fields that already have been entered, but, you might want to reset the properties for the upcoming page. This can be a little tricky, but keeping these thoughts in mind will save time.