The next set of possible extension points is within the controller layer. Some of these have been mentioned briefly in previous chapters, but they're repeated here for completeness.
In earlier versions of the Struts framework, it was almost a given that an application needed to extend the ActionServlet class because most of the controller functionality, excluding the Action class behavior, was present in this class. With Struts 1.1, this is no longer true. However, there are still a few good reasons why you might need to extend the ActionServlet class.
As was pointed out in Chapter 5, the initialization routines that are invoked when a Struts application is first launched reside in the ActionServlet class. If you need to modify the way the framework initializes itself, this is the place to do so. To extend the ActionServlet class, just create a subclass of org.apache.struts.action.ActionServlet. You can then override the method or methods that you need to function differently. Once this is done, you need to modify the deployment descriptor so that the Struts application will use your custom ActionServlet:
<servlet>
<servlet-name>storefront</servlet-name>
<servlet-class>
com.oreilly.struts.storefront.framework.ExtendedActionServlet
</servlet-class>
</servlet>
Most of the runtime request-processing behavior has been moved to the RequestProcessor class in Struts 1.1. If you need to customize the manner in which your Struts application processes a request, see the next section.
If you need to override functionality within the RequestProcessor class, you must let the framework know that it should use your customized version rather than the default. You can make the framework aware of your specialized RequestProcessor by modifying the configuration file for the Struts application. If your configuration file doesn't already have a controller element within it, you'll need to add one (there are several attributes that can be configured within the controller element—see Chapter 4 for more details):
<controller
contentType="text/html;charset=UTF-8"
debug="3"
locale="true"
nocache="true"
processorClass="com.oreilly.struts.framework.CustomRequestProcessor"/>
The processorClass attribute allows you to specify the fully qualified Java class name of your specialized RequestProcessor. The Struts framework will create an instance of your specialized RequestProcessor at startup and use it to process all of the requests for the application. Because each application module can have its own Struts configuration file, you can specify a different RequestProcessor for each module.
There are many methods that can be overridden within the RequestProcessor class. One of the methods that was designed with extension in mind is the processPreprocess( ) method. This method is called for each request. By default, it does nothing with the requests, but you can use this method in various ways to change the default request-processing behavior. The processPreprocess( ) method looks like this:
protected boolean processPreprocess( HttpServletRequest request,
HttpServletResponse response ){
return (true);
}
This method is called early in the request-processing stage, before the ActionForm is called and before the execute( ) method is called on the Action object. By default, the processPreprocess( ) method returns true, which tells the RequestProcessor to continue processing the request. However, you can override this method and perform some type of conditional logic, and if the request should not be processed any further, you can return false. When processPreprocess( ) returns false, the RequestProcessor stops processing the request and simply returns from the doPost( ) or doGet( ) call. Consequently, it's up to you to programmatically forward or redirect the request within the processPreprocess( ) method.
Example 9-3 illustrates this approach. This example checks to see whether the request is from a local host. If so, it returns true and the request continues. If the request is from any other host, the request is redirected to the unauthorized access page and the method returns false.
protected boolean processPreprocess( HttpServletRequest request,
HttpServletResponse response ){
boolean continueProcessing = true;
// Get the name of the remote host and log it
String remoteHost = request.getRemoteHost( );
log.info( "Request from host: " + remoteHost );
// Make sure the host is from one that you expect
if ( remoteHost == null || !remoteHost.startsWith( "127.") ){
// Not the localhost, so don't allow the host to access the site
continueProcessing = false;
ForwardConfig config = appConfig.findForwardConfig("Unauthorized");
try{
response.sendRedirect( config.getPath( ) );
}catch( Exception ex ){
log.error( "Problem sending redirect from processPreprocess( )" );
}
}
return continueProcessing;
}
The most important thing to note in this example is that even though the processPreprocess( ) method is returning false, it's still up to the method to take care of redirecting the request. Returning false just lets the RequestProcessor know that it doesn't need to continue. The controller doesn't do anything with the request; that's the responsibility of the processPreprocess( ) method.
|
There are other ways that you can perform the same logic without using the processPreprocess( ) method. One alternative is to use a servlet filter (part of the Servlet 2.3 API). Filters allow you to inspect the request before it ever reaches the Struts controller. However, there are two problems to be aware of with filters.
First, because filters are part of the 2.3 API, you will not be able to use them if you are using a servlet container that supports only 2.2. Second, because the filter inspects the request very early in the processing stage, filters don't have easy access to the Struts API. This makes it hard to look up ActionForwards or anything else that you might normally use in the processPreprocess( ) method. In fact, because the Struts controller hasn't even seen the request at the time the filter inspects it, the controller hasn't had a chance to select the proper application module.
There have been several places in previous chapters where I've mentioned a technique of creating a base Action that extends the Struts Action class and then using it as a superclass for other actions. One of the reasons for doing this is that in many applications, there is common logic that must be implemented by most of the Action classes. Letting this Action superclass contain most of this code eliminates the redundancy. Example 9-4 provides a very simple version of a base Action class that performs this type of behavior.
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Iterator;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.framework.exceptions.*;
import com.oreilly.struts.storefront.framework.UserContainer;
import com.oreilly.struts.storefront.service.IStorefrontService;
/**
* An abstract Action class that all Storefront Action classes can extend.
*/
abstract public class StorefrontBaseAction extends Action {
/**
* The default execute( ) method that all actions must implement.
*/
public ActionForward execute( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response ) throws Exception{
// It just calls a worker method that contains the real execute logic
return executeAction( mapping,form,request,response,getUserContainer(request));
}
/**
* The actual do work method that must be overridden by the subclasses.
*/
abstract public ActionForward executeAction( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response,
UserContainer userContainer )
throws Exception;
// This super Action is also a good place to put common utility methods
public boolean isLoggedIn( HttpServletRequest request ){
UserContainer container = getUserContainer(request);
return ( container != null && container.getUserView( ) != null );
}
/**
* Retrieve the UserContainer for the user tied to the request.
*/
protected UserContainer getUserContainer(HttpServletRequest request) {
HttpSession session = request.getSession( );
UserContainer userContainer =
(UserContainer)session.getAttribute(IConstants.USER_CONTAINER_KEY);
// Create a UserContainer if one doesn't exist already
if(userContainer == null) {
userContainer = new UserContainer( );
userContainer.setLocale(request.getLocale( ));
session.setAttribute(IConstants.USER_CONTAINER_KEY, userContainer);
}
return userContainer;
}
}
The StorefrontBaseAction class shown in Example 9-4 illustrates how you can use a base Action to perform repetitive behavior so that all of the subclasses need not perform it themselves.
Suppose, for example, that all of your Action classes needed to obtain the UserContainer for the current user and use some information within it (e.g., user ID or security permissions). One approach is to force all of the Action classes to obtain the UserContainer on their own, handle the situation when there isn't one, and so on. An alternate, more manageable approach is to put that behavior in a super Action and pass the UserContainer to the subclasses.
As Example 9-4 shows, the StorefrontBaseAction implements the execute( ) method, but inside that method, it gets an instance of a UserContainer and passes it as an argument to the executeAction( ) method. Each subclass implements the abstract executeAction( ) method and has the UserContainer passed in, instantiated, and guaranteed not to be null. This is only a trivial example of what you can do. Any behavior that all actions need to perform is a candidate for being implemented in the Action superclass, so that when the time comes to modify the implementation, only the behavior in the superclass needs to change.