The J2EE Front Controller design pattern uses a single controller to funnel all client requests through a central point. Among the many advantages this pattern brings to application functionality is that services such as security, internationalization, and logging are concentrated in the controller. This permits the consistent application of these functions across all requests. When the behavior of these services needs modification, changes potentially affecting the entire application need to be made only to a relatively small and isolated area of the program.
As discussed in Chapter 1, the Struts controller has several responsibilities. Chief among these are:
· Intercepting client requests.
· Mapping each request to a specific business operation.
· Collecting results from the business operation and making them available to the client.
· Determining the view to display to the client based on the current state and result of the business operation.
In the Struts framework, several components are responsible for the controller duties. Figure 5-1 is a simple class diagram of the components in the Struts framework that share some portion of the controller responsibility.
There also are secondary helper components that assist those in Figure 5-1 in fulfilling their responsibilities, but for now, let's focus on the ones in Figure 5-1.
The org.apache.struts.action.ActionServlet class acts as an interceptor for a Struts application. All requests from the client tier must pass through the ActionServlet before proceeding anywhere else in the application.
When an instance of the ActionServlet receives an HttpRequest, through either the doGet( ) or doPost( ) method, the process( ) method is called to handle the request. The process( ) method of ActionServlet is shown in Example 5-1.
protected void process(HttpServletRequest request,HttpServletResponse response)
throws IOException, ServletException {
RequestUtils.selectApplication( request, getServletContext( ) );
getApplicationConfig( request ).getProcessor( ).process( request, response );
}
The process( ) method might not look complicated, but the methods invoked within it are. First, the static selectApplication( ) method in the org.apache.struts.util.RequestUtils class is called and passed the current request and the ServletContext for the web application. The job of the selectApplication( ) method is to select an application module to handle the current request by matching the path returned from the request.getServletPath( ) to the prefix of each configured application module.
The selectApplication( ) method stores the appropriate ApplicationConfig and MessageResources objects into the request. This makes it easier for the rest of the framework to know which application and application components should be used for the request.
Prior to Struts 1.1, the ActionServlet class contained much of the code to process each user request. Starting with 1.1, however, most of that functionality has been moved to the org.apache.struts.action.RequestProcessor class, which is discussed in a moment. This new controller component was added to help relieve the ActionServlet class of most of the controller burden.
Although the framework still allows you to extend the ActionServlet class, the benefit is not as great as with earlier versions because most of the functionality lies in the new RequestProcessor class. If you still want to use your own version, just create a class that extends ActionServlet and configure the framework to use this class instead of the ActionServlet itself. Example 5-2 shows a Java servlet that extends the Struts ActionServlet and overrides the init( ) method.
package com.oreilly.struts.storefront.framework;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import org.apache.struts.action.ActionServlet;
import com.oreilly.struts.storefront.service.IStorefrontService;
import com.oreilly.struts.storefront.service.StorefrontServiceImpl;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.framework.exceptions.DatastoreException;
/**
* Extend the Struts ActionServlet to perform your own special
* initialization.
*/
public class ExtendedActionServlet extends ActionServlet {
public void init( ) throws ServletException {
// Make sure to always call the super's init( ) first
super.init( );
// Initialize the persistence service
try{
// Create an instance of the service interface
IStorefrontService serviceImpl = new StorefrontServiceImpl( );
// Store the service into the application scope
getServletContext( ).setAttribute( IConstants.SERVICE_INTERFACE_KEY,
serviceImpl );
}catch( DatastoreException ex ){
// If there's a problem initializing the service, disable the web app
ex.printStackTrace( );
throw new UnavailableException( ex.getMessage( ) );
}
}
}
Overriding the init( ) method is just an example; you can override any method you need to. If you do override init( ), make sure that you call the super.init( ) method so that the default initialization occurs.
|
To configure the framework to use your ActionServlet subclass instead of the default in the Struts framework, you will need to modify the web.xml file as follows:
<servlet>
<servlet-name>storefront</servlet-name>
<servlet-class>
com.oreilly.struts.storefront.framework.ExtendedActionServlet
</servlet-class>
</servlet>
Depending on the initialization parameters configured in the web.xml file, the servlet container will load the Struts ActionServlet either when the container is first started or when the first request arrives for the servlet. In either case (as with any other Java servlet) the init( ) method is guaranteed to be called and must finish before any request is processed by the servlet. The Struts framework performs all of the compulsory initialization when init( ) is called. Let's take a look at what goes on during that initialization process. Understanding these details will make debugging and extending your applications much easier.
The following steps occur when the init( ) method of the Struts ActionServlet is invoked by the container:
1. Initialize the framework's internal message bundle. These messages are used to output informational, warning, and error messages to the log files. The org.apache.struts.action.ActionResources bundle is used to obtain the internal messages.
2. Load from the web.xml file the initialization parameters that control various behaviors of the ActionServlet class. These parameters include config, debug, detail, and convertNull. For information on how these and other servlet parameters affect the behavior of an application, refer to Section 4.5.3 in Chapter 4.
3. Load and initialize the servlet name and servlet mapping information from the web.xml file. These values will be used throughout the framework (mostly by tag libraries) to output correct URL destinations when submitting HTML forms. During this initialization, the DTDs used by the framework also are registered. The DTDs are used to validate the configuration file in the next step.
4. Load and initialize the Struts configuration data for the default application, which is specified by the config initialization parameter. The default Struts configuration file is parsed and an ApplicationConfig object is created and stored in the ServletContext. The ApplicationConfig object for the default application is stored in the ServletContext with a key value of org.apache.struts.action.APPLICATION.
5. Each message resource that is specified in the Struts configuration file for the default application is loaded, initialized, and stored in the ServletContext at the appropriate location, based on the key attribute specified in each message-resources element. If no key is specified, the message resource is stored at the key value org.apache.struts.action.MESSAGE. Only one message resource can be stored as the default because the keys have to be unique.
6. Each data source declared in the Struts configuration file is loaded and initialized. If no data-sources elements are specified, this step is skipped.
7. Load and initialize each plug-in specified in the Struts configuration file. The init( ) method will be called on each and every plug-in specified.
8. Once the default application has been properly initialized, the servlet init( ) method will determine if any application modules are specified and, if so, will repeat Steps 4 through 7 for each one.
Figure 5-2 uses a sequence diagram to illustrate the eight major steps that occur during the initialization of the ActionServlet.
|
The second step in Example 5-1 is to call the process( ) method of the org.apache.struts.action.RequestProcessor class. It's called by the ActionServlet instance and passed the current request and response objects.
The RequestProcessor class was added to the framework to allow developers to customize the request-handling behavior for an application. Although this type of customization was possible in previous versions by extending the ActionServlet class, it was necessary to introduce this new class to allow each application module to have its own customized request handler. The RequestProcessor class contains many methods that can be overridden if you need to modify the default functionality.
As shown in Example 5-1, once the correct application module has been selected, the process( ) method of the RequestProcessor is called to handle the request. The behavior of the process( ) method in the RequestProcessor class is similar to its behavior in earlier versions of the ActionServlet class. Example 5-3 shows the implementation of the process( ) method in the RequestProcessor class.
public void process(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// Wrap multipart requests with a special wrapper
request = processMultipart(request);
// Identify the path component we will use to select a mapping
String path = processPath(request, response);
if (path == null) {
return;
}
if (log.isInfoEnabled( )) {
log.info("Processing a '" + request.getMethod( ) +
"' for path '" + path + "'");
}
// Select a Locale for the current user if requested
processLocale(request, response);
// Set the content type and no-caching headers if requested
processContent(request, response);
processNoCache(request, response);
// General-purpose preprocessing hook
if (!processPreprocess(request, response)) {
return;
}
// Identify the mapping for this request
ActionMapping mapping = processMapping(request, response, path);
if (mapping == null) {
return;
}
// Check for any role required to perform this action
if (!processRoles(request, response, mapping)) {
return;
}
// Process any ActionForm bean related to this request
ActionForm form = processActionForm(request, response, mapping);
processPopulate(request, response, form, mapping);
if (!processValidate(request, response, form, mapping)) {
return;
}
// Process a forward or include specified by this mapping
if (!processForward(request, response, mapping)) {
return;
}
if (!processInclude(request, response, mapping)) {
return;
}
// Create or acquire the Action instance to process this request
Action action = processActionCreate(request, response, mapping);
if (action == null) {
return;
}
// Call the Action instance itself
ActionForward forward =
processActionPerform(request, response, action, form, mapping);
// Process the returned ActionForward instance
processActionForward(request, response, forward);
}
As Example 5-3 shows, there's quite a lot going on in the process( ) method of the RequestProcessor. Let's go through the method step by step:
1. The processMultipart( ) method is called. If the HttpServletRequest method is a POST and the contentType of the request starts with multipart/form-data, the standard request object is wrapped with a special version from the Struts framework that deals exclusively with multipart requests. If the request method is a GET or the contentType is not multipart, the original request is returned. Unless your application supports uploading files, you don't need to worry about multipart functionality in Struts.
2. The processPath( ) method is called to determine the path component from the URI for the request. Among other things, this information is used to select the appropriate Struts Action to invoke.
3. The processLocale( ) method is called to determine the locale of the user making the request and to store a Locale object into the user's HttpSession object. The locale isn't always obtained into the user's session—it depends on the locale attribute in the controller configuration element. See Chapter 4 for more details on the attributes of the controller element.
4. Determine the content type and optional encoding of the request by calling the processContent( ) method. The content type may be configured in the configuration settings and also overridden by the JSPs. The default content type is text/html.
5. The processNoCache( ) method is called to determine whether the noCache attribute is set to true. If it is, add the proper header parameters in the response object to prevent the pages from being cached in the browser. The header parameters include Pragma, Cache-Control, and Expires.
6. The processPreprocess( ) method is called next. It's a general-purpose preprocessing hook that, by default, just returns true. However, subclasses can override this method and perform conditional logic to decide whether to continue processing the request. Because this method gets called before an Action is invoked, this is a good place to validate whether the user contains a valid session. If this method returns true, processing of the request will continue. If it returns false, processing will stop. It's up to you to programmatically redirect or forward the request—the controller will assume that you are handling the request and will not send a response to the client.
7. Determine the ActionMapping for the request using the path information by calling the processMapping( ) method. If a mapping can't be found using the path information, an error response will be returned to the client.
8. Check to see if any security roles are configured for the Action by calling the processRoles( ) method. If there are roles configured, the isUserInRole( ) method is called on the request. If the user doesn't contain the necessary role, processing will end here and an appropriate error message will be returned to the client.
9. Call the processActionForm( ) method to determine whether an ActionForm is configured for the ActionMapping. If an ActionForm has been configured for the mapping, an attempt will be made to find an existing instance in the appropriate scope. Once an ActionForm is either found or created, it is stored within the proper scope using a key that is configured in the name attribute for the Action element.
10. The processPopulate( ) method is called next, and if an ActionForm is configured for the mapping, its properties are populated from the request parameter values. Before the properties are populated from the request, however, the reset( ) method is called on the ActionForm.
11. The processValidate( ) method is called, and if an ActionForm has been configured and the validate attribute is set to true for the action element, the validate( ) method is called. If the validate( ) method detects errors, it will store an ActionErrors object into the request scope, and the request automatically will be forwarded to the resource specified by the input attribute for the action mapping. If no errors were detected from the validate( ) method or there was no ActionForm for the action mapping, processing of the request continues. You can configure the controller element to interpret the input attributes as defined forwards. See Chapter 4 for more information on this feature.
12. Determine if a forward or an include attribute is configured for the action mapping. If so, call the forward( ) or include( ) method on the RequestDispatcher, depending on which one is configured. The processing of the request ends at this point if either one of these is configured. Otherwise, continue processing the request.
13. Call the processActionCreate( ) method to create or acquire an Action instance to process the request. An Action cache will be checked to see if the Action instance already has been created. If so, that instance will be used to process the request. Otherwise, a new instance will be created and stored into the cache.
14. Call the processActionPerform( ) method, which in turn calls the execute( ) method on the Action instance. The execute( ) call is wrapped with a try/catch block so that exceptions can be handled by the RequestProcessor.
15. Call the processActionForward( ) method and pass it the ActionForward object returned from the execute( ) method. The processActionForward( ) method determines whether a redirect or a forward should occur by checking with the ActionForward object, which in turn depends on the redirect attribute in the forward element.
It's easy to create your own custom RequestProcessor class. Let's look at an example of how and why you might do this. Suppose your application needs to allow the user to change her locale at any time during the session. The default behavior of the processLocale( ) method in RequestProcessor is to set the user's Locale only if it hasn't already been stored in the session, which typically happens during the first request.
|
Example 5-4 shows a customized RequestProcessor class that checks the request for a locale each time and updates the user's session if it has changed from the previous one. This allows the user to change her locale preference at any point during the application.
package com.oreilly.struts.framework;
import javax.servlet.http.*;
import java.util.Locale;
import org.apache.struts.action.Action;
import org.apache.struts.action.RequestProcessor;
/**
* A customized RequestProcessor that checks the user's preferred locale
* from the request each time. If a Locale is not in the session or
* the one in the session doesn't match the request, the Locale in the
* request is set in the session.
*/
public class CustomRequestProcessor extends RequestProcessor {
protected void processLocale(HttpServletRequest request,
HttpServletResponse response) {
// Are we configured to select the Locale automatically?
if (!appConfig.getControllerConfig().getLocale( )){
// The locale is configured not to be stored, so just return
return;
}
// Get the Locale (if any) that is stored in the user's session
HttpSession session = request.getSession( );
Locale sessionLocale = (Locale)session.getAttribute(Action.LOCALE_KEY);
// Get the user's preferred locale from the request
Locale requestLocale = request.getLocale( );
// If the Locale was never added to the session or it has changed, set it
if (sessionLocale == null || (sessionLocale != requestLocale) ){
if (log.isDebugEnabled( )) {
log.debug(" Setting user locale '" + requestLocale + "'");
}
// Set the new Locale into the user's session
session.setAttribute( Action.LOCALE_KEY, requestLocale );
}
}
}
To configure the CustomizedRequestProcessor for your application, you will need to add a controller element to the Struts configuration file and include the processorClass attribute as shown here:
<controller
contentType="text/html;charset=UTF-8"
debug="3"
locale="true"
nocache="true"
processorClass="com.oreilly.struts.framework.CustomRequestProcessor"/>
You need to specify the fully qualified class name of the CustomizedRequestProcessor, as shown in this fragment. Although not every application has a reason to create a custom request processor, having one available in your application can act as a placeholder for future customizations. Therefore, it's a good idea to create one for your application and specify it in the configuration file. It doesn't have to override anything when you create it; you can add to it as the need arises. For more information on the controller element, see Section 4.7.2 in Chapter 4.
The org.apache.struts.action.Action class is the heart of the framework. It's the bridge between a client request and a business operation. Each Action class typically is designed to perform a single business operation on behalf of a client. A single business operation doesn't mean the Action can perform only one task. Rather, the task that it performs should be cohesive and centered around a single functional unit. In other words, the tasks performed by the Action should be related to one business operation. For instance, you shouldn't create an Action that performs shopping-cart functionality as well as handling login and logout responsibilities. These areas of the application are not closely related and shouldn't be combined.
|
Once the correct Action instance is determined, the processActionPerform( ) method is invoked. The processActionPerform( ) method of the RequestProcessor class is shown in Example 5-5.
protected ActionForward processActionPerform(HttpServletRequest request,
HttpServletResponse response,
Action action,
ActionForm form,
ActionMapping mapping)
throws IOException, ServletException {
try {
return (action.execute(mapping, form, request, response));
}catch (Exception e){
return (processException(request, response, e, form, mapping));
}
}
The processActionPerform( ) method is responsible for calling the execute( ) method on the Action instance. In earlier versions of the Struts framework, the Action class contained only a perform( ) method. The perform( ) method has been deprecated in favor of the execute( ) method in Struts 1.1. This new method is necessary because the perform( ) method declares that it throws only IOExceptions and ServletExceptions. Due to the added declarative exception-handling functionality, the framework needs to catch all instances of java.lang.Exception from the Action class.
Instead of changing the method signature for the perform( ) method and breaking backward compatibility, the execute( ) method was added. The execute( ) method invokes the perform() method, but eventually the perform( ) method will go away. You should use the execute( ) method in place of the perform( ) method in all of your Action classes.
|
You will need to extend the Action class and provide an implementation of the execute( ) method. Example 5-6 shows the LoginAction from the Storefront application.
package com.oreilly.struts.storefront.security;
import java.util.Locale;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.BaseException;
import com.oreilly.struts.storefront.framework.UserContainer;
import com.oreilly.struts.storefront.framework.StorefrontBaseAction;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.service.IStorefrontService;
/**
* Implements the logic to authenticate a user for the Storefront application.
*/
public class LoginAction extends StorefrontBaseAction {
/**
* Called by the controller when the user attempts to log in to the
* Storefront application.
*/
public ActionForward execute( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response )
throws Exception{
// The email and password should have already 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);
// Create a single container object to store user data
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);
// Store the UserView in the container and store the container in the session
existingContainer.setUserView(userView);
session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer);
// Return a Success forward
return mapping.findForward(IConstants.SUCCESS_KEY);
}
}
When the execute( ) method in LoginAction is called, the email and password values are retrieved and passed to the authenticate( ) method. If no exception is thrown by the authenticate( ) business operation, a new HttpSession is created and a JavaBean that contains user information is stored into the user's session.
|
The UserView class contains simple properties such as firstName and lastName that can be used by the presentation. These types of presentation JavaBeans are commonly referred to as value objects (VOs), but are more formally called data transfer objects (DTOs) because they are used to transfer data from one layer to another. In the UserView class shown in Example 5-7, the data is transferred from the security service to the presentation layer.
package com.oreilly.struts.storefront.customer.view;
import com.oreilly.struts.storefront.framework.view.BaseView;
/**
* Mutable data representing a user of the system.
*/
public class UserView extends BaseView {
private String lastName;
private String firstName;
private String emailAddress;
private String creditStatus;
public UserView( ){
super( );
}
public String getFirstName( ) {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName( ) {
return lastName;
}
public String getEmailAddress( ) {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public void setCreditStatus(String creditStatus) {
this.creditStatus = creditStatus;
}
public String getCreditStatus( ) {
return creditStatus;
}
}
Data transfer objects are discussed in Chapter 6.
|
Because Action instances are expected to be thread-safe, only a single instance of each Action class is created for an application. All client requests share the same instance and are able to invoke the execute( ) method at the same time.
The RequestProcessor contains a HashMap, the keys of which are the names of all the Action classes that are specified in the configuration file; the value for each key is the single instance of that Action. During the processActionCreate( ) method of the RequestProcessor class, the framework checks the HashMap to see whether an instance already has been created. If it has, this instance is returned. Otherwise, a new instance of the Action class is created, stored into the HashMap, and returned. The section of the code that creates a new Action instance is synchronized to ensure that only one thread will create an instance. Once a thread creates an instance and inserts it into the HashMap, all future threads will use the instance from the cache.
As you saw in the discussion of the Action class, the execute( ) method returns an ActionForward object. The ActionForward class represents a logical abstraction of a web resource. This resource typically is a JSP page or a Java servlet.
The ActionForward is a wrapper around the resource, so there's less coupling of the application to the physical resource. The physical resource is specified only in the configuration file (as the name, path, and redirect attributes of the forward element), not in the code itself. The RequestDispatcher may perform either a forward or redirect for an ActionForward, depending on the value of the redirect attribute.
To return an ActionForward from an Action, you can either create one dynamically in the Action class or, more commonly, use the action mapping to locate one that has been preconfigured in the configuration file. The following code fragment illustrates how you can use the action mapping to locate an ActionForward based on its logical name:
return mapping.findForward( "Success" );
Here, an argument of "Success" is passed to the findForward( ) method of an ActionMapping instance. The argument in the findFoward( ) method must match either one of the names specified in the global-forwards section or one specific to the action from which it's being called. The following fragment shows forward elements defined for the /signin action mapping:
<action
input="/security/signin.jsp"
name="loginForm"
path="/signin"
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>
The findForward( ) method in the ActionMapping class first calls the findForwardConfig( ) method to see if a forward element with the corresponding name is specified at the action level. If not, the global-forwards section is checked. When an ActionForward that matches is found, it's returned to the RequestProcessor from the execute( ) method. Here's the findForward( ) method from the ActionMapping class:
public ActionForward findForward(String name) {
ForwardConfig config = findForwardConfig(name);
if (config == null) {
config = getApplicationConfig( ).findForwardConfig(name);
}
return ((ActionForward) config);
}
|
A single Action instance is created for each Action class in the framework. Every client request will share the same instance, just as every client request shares the same ActionServlet instance. Thus, as with servlets, you must ensure that your Action classes operate properly in a multithreaded environment.
To be thread-safe, it's important that your Action classes do not use instance variables to hold client-specific state. You may use instance variables to hold state information; it just shouldn't be specific to one client or request. For example, you can create an instance variable of type org.apache.commons.logging.Log to hold onto a logger, as the Struts RequestProcessor class does. The log instance can be used by all requests because the logger is thread-safe and does not hold state for a specific client or request.
For client-specific state, however, you should declare the variables inside the execute( ) method. These local variables are allocated in a different memory space than instance variables. Each thread that enters the execute( ) method has its own stack for local variables, so there's no chance of overriding the state of other threads.
Some developers get confused about what logic belongs in an Action class. The Action class is not the proper place to put your application's business logic. If you look back to Figure 3-6, you can see that the Action class is still part of the controller; it's just been separated out from the ActionServlet and RequestProcessor for the sake of convenience.
Business logic belongs in the model domain. Components that implement this logic may be EJBs, CORBA objects, or even services written on top of a data source and a connection pool. The point is that the business domain should be unaware of the type of presentation tier that's accessing it. This allows your model components to be more easily reused by other applications. Example 5-8 illustrates the GetItemDetailAction from the Storefront application, which calls the model to retrieve the detail information for an item in the catalog.
package com.oreilly.struts.storefront.catalog;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import com.oreilly.struts.storefront.framework.exceptions.BaseException;
import com.oreilly.struts.storefront.framework.UserContainer;
import com.oreilly.struts.storefront.framework.StorefrontBaseAction;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.service.IStorefrontService;
/**
* An action that gets an ItemView based on an id parameter in the request and
* then inserts the item into an ActionForm and forwards to whatever
* path is defined as Success for this action mapping.
*/
public class GetItemDetailAction extends StorefrontBaseAction {
public ActionForward execute( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response )
throws Exception {
// Get the primary key of the item from the request
String itemId = request.getParameter( IConstants.ID_KEY );
// Call the storefront service and ask it for an ItemView for the item
IStorefrontService serviceImpl = getStorefrontService( );
ItemDetailView itemDetailView = serviceImpl.getItemDetailView( itemId );
// Set the returned ItemView into the Dynamic Action Form
// The parameter name 'view' is what is defined in the struts-config
((DynaActionForm)form).set("view", itemDetailView);
// Return the ActionForward that is defined for the success condition
return mapping.findForward( IConstants.SUCCESS_KEY );
}
}
The GetItemDetailAction class in Example 5-8 delegates to the Storefront service the real work of getting the item information. This a good approach because the Action doesn't know the internals of the Storefront service or the getItemDetailView( ) method. It can be a local object that performs JDBC calls, a session bean performing a remote call to an application server, or some other implementation. If the model implementation changes (which it will when we discuss EJB in Chapter 13), the Action will be protected from that change. Because the Storefront service is unaware of the type of client using it, clients other than Struts can use it. Decoupling the Action classes from the business objects is explored further in the next chapter.
The Struts framework includes five out-of-the-box Action classes that you can integrate into your applications easily, saving yourself development time. Some of these are more useful than others, but all of them deserve some attention. The classes are contained within the org.apache.struts.actions package.
There are many situations where you just need to forward from one JSP page to another, without really needing to go through an Action class. However, calling a JSP directly should be avoided, for several reasons. The controller is responsible for selecting the correct application module to handle the request and storing the ApplicationConfig and MessageResources for that application module in the request. If this step is bypassed, functionality such as selecting the correct messages from the resource bundle may not work properly.
Another reason that calling a JSP directly is not a good idea is that it violates the component responsibilities of MVC. The controller is supposed to process all requests and select a view for the client. If your application were allowed to call the page directly, the controller would not be able to fulfill its obligations to the MVC contract.
To solve these problems and to prevent you from having to create an Action class that performs only a simple forward, you can use the provided ForwardAction. This Action simply performs a forward to a URI that is configured in the parameter attribute. In the Struts configuration file, you specify an action element using the ForwardAction as the type attribute:
<action
input="/index.jsp"
name="loginForm"
path="/viewsignin"
parameter="/security/signin.jsp"
scope="request"
type="org.apache.struts.actions.ForwardAction"
validate="false"/>
</action>
When the /viewsignin action is selected, the perform( ) method of the ForwardAction class gets called. When you use the ForwardAction in an action element, the parameter attribute (instead of an actual forward element) is used to specify where to forward to. Other than this difference, you call the ForwardAction in the same way as any other Action.
The ForwardAction class comes in handy when you need to integrate your Struts application with other servlets or JSP pages while still taking advantage of the controller functionality. The ForwardAction class is one of the most valuable of the pre-built Action classes included with the framework.
The IncludeAction class is similar in some respects to the ForwardAction class. It originally was created to make it easier to integrate existing servlet-based components into Struts-based web applications. If your application is using the include( ) method of a RequestDispatcher, you can implement the same behavior using the IncludeAction.
You specify the IncludeAction in an action element in the same manner that you do for ForwardAction, except that you use IncludeAction in the type attribute:
<action
input="/subscription.jsp"
name="subscriptionForm"
path="/saveSubscription"
parameter="/path/to/processing/servlet"
scope="request"
type="org.apache.struts.actions.IncludeAction"/>
You must include the parameter attribute and specify a path to the servlet you want to include.
The purpose of the DispatchAction class is to allow multiple operations that normally would be scattered throughout multiple Action classes to reside in a single class. The idea is that there is related functionality for a service, and instead of being spread over multiple Action classes, it should be kept together in the same class. For example, an application that contains a typical shopping-cart service usually needs the ability to add items to the cart, view the items in the cart, and update the items and quantities in the cart. One design is to create three separate Action classes (e.g., AddItemAction, ViewShoppingCartAction, and UpdateShoppingCartAction).
Although this solution is a valid approach, all three Action classes probably would perform similar functionality before carrying out their assigned business operations. By combining them, you would be making it easier to maintain the application—if you exchanged the current shopping-cart implementation for an alternate version, all of the code would be located in a single class.
To use the DispatchAction class, create a class that extends it and add a method for every function you need to perform on the service. Your class should not contain the typical execute( ) method, as other Action classes do. The execute( ) method is implemented by the abstract DispatchAction class.
You must include one method in your DispatchAction for every Action you want to invoke for this DispatchAction. Example 5-9 will help illustrate this. One thing should be noted about this example, however. Instead of extending the Struts DispatchAction, it actually extends a Storefront version called StorefrontDispatchAction. This was done to allow for utility-type behavior to exist as a superclass without modifying the Struts version. It's a fairly common practice.
package com.oreilly.struts.storefront.order;
import java.io.IOException;
import java.text.Format;
import java.text.NumberFormat;
import java.util.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.struts.actions.DispatchAction;
import com.oreilly.struts.storefront.service.IStorefrontService;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.framework.UserContainer;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.framework.ShoppingCartItem;
import com.oreilly.struts.storefront.framework.ShoppingCart;
import com.oreilly.struts.storefront.framework.StorefrontDispatchAction;
/**
* Implements all of the functionality for the shopping cart.
*/
public class ShoppingCartActions extends StorefrontDispatchAction {
/**
* This method just forwards to the success state, which should represent
* the shoppingcart.jsp page.
*/
public ActionForward view(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
// Call to ensure that the user container has been created
UserContainer userContainer = getUserContainer(request);
return mapping.findForward(IConstants.SUCCESS_KEY);
}
/**
* This method updates the items and quantities for the shopping cart from the
* request.
*/
public ActionForward update(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
updateItems(request);
updateQuantities(request);
return mapping.findForward(IConstants.SUCCESS_KEY);
}
/**
* This method adds an item to the shopping cart based on the id and qty
* parameters from the request.
*/
public ActionForward addItem(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
UserContainer userContainer = getUserContainer(request);
// Get the id for the product to be added
String itemId = request.getParameter( IConstants.ID_KEY );
String qtyParameter = request.getParameter( IConstants.QTY_KEY );
int quantity;
if(qtyParameter != null) {
Locale userLocale = userContainer.getLocale( );
Format nbrFormat = NumberFormat.getNumberInstance(userLocale);
try {
Object obj = nbrFormat.parseObject(qtyParameter);
quantity = ((Number)obj).intValue( );
}
catch(Exception ex) {
// Set the default quantity
quantity = 1;
}
}
// Call the Storefront service and ask it for an ItemView for the item
IStorefrontService serviceImpl = getStorefrontService( );
ItemDetailView itemDetailView = serviceImpl.getItemDetailView( itemId );
// Add the item to the cart and return
userContainer.getCart( ).addItem(
new ShoppingCartItem(itemDetailView, quantity));
return mapping.findForward(IConstants.SUCCESS_KEY);
}
/**
* Update the items in the shopping cart. Currently, only deletes occur
* during this operation.
*/
private void updateItems(HttpServletRequest request) {
// Multiple checkboxes with the name "deleteCartItem" are on the
// form. The ones that were checked are passed in the request.
String[] deleteIds = request.getParameterValues("deleteCartItem");
// Build a list of item ids to delete
if(deleteIds != null && deleteIds.length > 0) {
int size = deleteIds.length;
List itemIds = new ArrayList( );
for(int i = 0;i < size;i++) {
itemIds.add(deleteIds[i]);
}
// Get the ShoppingCart from the UserContainer and delete the items
UserContainer userContainer = getUserContainer(request);
userContainer.getCart( ).removeItems(itemIds);
}
}
/**
* Update the quantities for the items in the shopping cart.
*/
private void updateQuantities(HttpServletRequest request) {
Enumeration enum = request.getParameterNames( );
// Iterate through the parameters and look for ones that begin with
// "qty_". The qty fields in the page were all named "qty_" + itemId.
// Strip off the id of each item and the corresponding qty value.
while(enum.hasMoreElements( )) {
String paramName = (String)enum.nextElement( );
if(paramName.startsWith("qty_")) {
String id = paramName.substring(4, paramName.length( ));
String qtyStr = request.getParameter(paramName);
if(id != null && qtyStr != null) {
ShoppingCart cart = getUserContainer(request).getCart( );
cart.updateQuantity(id, Integer.parseInt(qtyStr));
}
}
}
}
}
The com.oreilly.struts.storefront.order.ShoppingCartActions class contains the methods addItem( ), update( ), and view( ). Each of these methods would normally be put into a separate Action class. With the DispatchAction class, they can be kept together in the same one.
|
To use your specialized DispatchAction class, you need to configure each action element that uses it a little differently than the other mappings. Example 5-10 illustrates how the ShoppingCartActions class from Example 5-9 is declared in the configuration file.
<action path="/cart"
input="/order/shoppingcart.jsp"
parameter="method"
scope="request"
type="com.oreilly.struts.storefront.order.ShoppingCartActions"
validate="false">
<forward name="Success" path="/order/shoppingcart.jsp" redirect="true"/>
</action>
The /cart action mapping shown in Example 5-10 specifies the parameter attribute and sets the value to be the literal string "method". The value specified here becomes very important to the DispatchAction instance when invoked by a client. The DispatchAction uses this attribute value to determine which method in your specialized DispatchAction to invoke. Instead of just calling the /cart action mapping, an additional request parameter is passed; the key is the value specified for the parameter attribute from the mapping. The value of this request parameter must be the name of the method to invoke. To invoke the addItem( ) method on the Storefront application, you might call it like this:
http://localhost:8080/storefront/action/cart?method=addItem&id=2
The request parameter named method has a value of "addItem". This is used by the DispatchAction to determine which method to invoke. You must have a method in your DispatchAction subclass that matches the parameter value. The method name must match exactly, and the method must include the parameters normally found in the execute( ) method. The following fragment highlights the method signature for the addItem( ) method from Example 5-9:
public ActionForward addItem( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response )
throws Exception;
DispatchAction uses reflection to locate a method that matches the same name as the request parameter value and contains the same number and type of arguments. Once found, the method will be invoked and the ActionForward object will be returned, just as with any other Action class.
|
LookupDispatchAction, as you might guess, is a subclass of the DispatchAction class. From a high level, it performs a similar task as the DispatchAction.
Like DispatchAction, the LookupDispatchAction class allows you to specify a class with multiple methods, where one of the methods is invoked based on the value of a special request parameter specified in the configuration file. That's about where the similarity ends. While DispatchAction uses the value of the request parameter to determine which method to invoke, LookupDispatchAction uses the value of the request parameter to perform a reverse lookup from the resource bundle using the parameter value and match it to a method in the class.
An example will help you understand this better. First, create a class that extends LookupDispatchAction and implements the getKeyMethodMap( ) method. This method returns a java.util.Map containing a set of key/value pairs.
The keys of this Map should match those from the resource bundle. The value that is associated with each key in the Map should be the name of the method in your LookupDispatchAction subclass. This value will be invoked when a request parameter equal to the message from the resource bundle for the key is included.
The following fragment shows an example of the getKeyMethodMap( ) method for ProcessCheckoutAction in the Storefront application:
protected Map getKeyMethodMap( ) {
Map map = new HashMap( );
map.put("button.checkout", "checkout" );
map.put("button.saveorder", "saveorder" );
return map;
}
For the purposes of this discussion, let's suppose we have the following resources in the message resource bundle:
button.checkout=Checkout
button.saveorder=Save Order
and that we have specified the following action element in the Struts configuration file:
<action path="/processcheckout"
input="/checkout.jsp"
name="checkoutForm"
parameter="action"
scope="request"
type="com.oreilly.struts.storefront.order.ProcessCheckoutAction">
<forward name="Success" path="/order/ordercomplete.jsp"/>
</action>
Then create a JSP that performs a POST using the processcheckout action. A URL parameter of action="Checkout" will be sent in the request header. Example 5-11 shows the JSP that calls the processcheckout action.
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:html>
<head>
<title>Virtual Shopping with Struts</title>
<html:base/>
<script language=javascript src="include/scripts.js"></script>
<link rel="stylesheet" href="../stylesheets/format_win_nav_main.css" type="text/css">
</head>
<body topmargin="0" leftmargin="0" bgcolor="#FFFFFF">
<!-- Header Page Information -->
<%@ include file="../include/head.inc"%>
<!-- Nav Bar -->
<%@ include file="../include/menubar.inc"%>
<br>
Display order summary and take credit card information here
<html:form action="/processcheckout">
<html:submit property="action">
<bean:message key="button.checkout"/>
</html:submit>
</html:form>
<br><br>
<%@ include file="../include/copyright.inc"%>
</body>
</html:html>
The key to understanding how all of this works is that the submit button in Example 5-11 will have a name of "action" and its value will be the value returned from the <bean:message> tag. This is more evident when you see the HTML source generated from this JSP page. The following fragment shows the source generated inside the <html:form> tag:
<form
name="checkoutForm"
method="POST"
action="/storefront/action/processcheckout">
<input type="submit" name="action" value="Checkout" alt="Checkout">
</form>
You can see in this HTML source that when the checkoutForm is posted, the action="Checkout" URL parameter will be included. ProcessCheckoutAction will take the value "Checkout" and find a message resource key that has this value. In the instance, the key will be button.checkout, which, according to the getKeyMethodMap( ) method shown earlier, maps to the method checkout( ).
Whew! That's a long way to go just to determine which method to invoke. The intent of this class is to make it easier when you have an HTML form with multiple submit buttons with the same name. One submit button may be a Checkout action and another might be a Save Order action. Both buttons would have the same name (for example, "action"), but the value of each button would be different. This may not be an Action class that you will use often, but in certain situations, it can save you scarce development time.
The SwitchAction class is new to the framework. It was added to support switching from one application module to another and then forwarding control to a resource within the application.
There are two required request parameters. The prefix request parameter specifies the application prefix, beginning with a "/", of the application module to which control should be switched. If you need to switch to the default application, use a zero-length string ("â?ť). The appropriate ApplicationConfig object will be stored in the request, just as it is when a new request arrives at the ActionServlet.
The second required request parameter is the page parameter. This parameter should specify the application-relative URI, beginning with a "/", to which control should be forwarded once the correct application module is selected. This Action is very straightforward. You'll need it only if you use more than one Struts application module.