Chapter 5. Struts Controller
|
5.1 The Controller Mechanism
The J2EE Front Controller design pattern uses a single controller to funnel all client
As discussed in Chapter 1, the Struts controller has several responsibilities. Chief among these are:
In the Struts framework, several components are responsible for the controller
Figure 5-1. Struts controller components
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. 5.1.1 The ActionServlet Class
The
org.apache.struts.action.ActionServlet
class acts as an interceptor for a Struts application. All requests from the client
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. Example 5-1. The ActionServlet process( ) method
protected void process(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
RequestUtils.selectModule(request, getServletContext( ));
getRequestProcessor(getModuleConfig(request)).process(request, response);
}
The
process()
method might not look complicated, but the
The selectModule () 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. 5.1.1.1 Extending the ActionServlet class
Prior to Struts 1.1, the
ActionServlet
class contained much of the code to process each
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
Example 5-2. The Struts ActionServlet can be extended to perform custom initialization
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
<servlet> <servlet-name>storefront</servlet-name> <servlet-class> com.oreilly.struts.storefront.framework.ExtendedActionServlet </servlet-class> </servlet> 5.1.1.2 Struts initialization process
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
The following steps occur when the init() method of the Struts ActionServlet is invoked by the container:
Figure 5-2 uses a sequence diagram to
Figure 5-2. Sequence diagram for the ActionServlet init( ) method
5.1.2 The RequestProcessor ClassThe 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. Example 5-3. The process( ) method processes all requests
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.isDebugEnabled( )) {
log.debug("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
processForwardConfig(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:
5.1.2.1 Extending the RequestProcessor classIt'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. Example 5-4. Customized RequestProcessor that overrides the default Locale processing
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.6.4 in Chapter 4. 5.1.3 The Action Class
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
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. Example 5-5. The processActionPerform( ) method
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 IOException s and ServletException s. 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.
{% if main.adsdop %}{% include 'adsenceinline.tpl' %}{% endif %} 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. Example 5-6. The LoginAction class 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
Example 5-7. The UserView DTO
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.
5.1.3.1 The Action class cacheBecause 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
5.1.4 The ActionForward classAs 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 = getModuleConfig( ).findForwardConfig(name);
}
return ((ActionForward) config);
}
5.1.5 Creating Multithreaded Action Classes
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
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
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. 5.1.6 Business Logic and the Action Class
Some developers get
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. Example 5-8. The Action class should delegate the business logic to a model component
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. 5.1.7 Using the Prebuilt Struts ActionsThe 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. 5.1.7.1 The org.apache.struts.actions.ForwardAction classThere 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
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
5.1.7.2 The org.apache.struts.actions.IncludeAction classThe 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. 5.1.7.3 The org.apache.struts.actions.DispatchAction class
The purpose of the
DispatchAction
class is to allow multiple operations that normally would be
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
Example 5-9. The shopping-cart functionality is put into a single DispatchAction
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. Example 5-10. Specifying the parameter attribute when using a DispatchAction subclass
<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.
5.1.7.4 The org.apache.struts.actions.LookupDispatchAction classLookupDispatchAction , 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. Example 5-11. The checkout.jsp file that calls the ProcessCheckoutAction when posted
<%@ 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>
<%@ 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
5.1.7.5 The org.apache.struts.actions.SwitchAction classThe 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
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. |