by Sunil Patil
11/10/2004
Introduction
I have seen lot of projects where the developers implemented a proprietary MVC framework, not because they wanted to do something fundamentally different from Struts, but because they were not aware of how to extend Struts. You can get total control by developing your own MVC framework, but it also means you have to commit a lot of resources to it, something that may not be possible in projects with tight schedules.
Struts is not only a very powerful framework, but also very extensible. You can extend Struts in three ways.
PlugIn: Create your own PlugIn class if you want to execute some business logic at application startup or shutdown.
RequestProcessor: Create your own RequestProcessor if you want to execute some business logic at a particular point during the request-processing phase. For example, you might extend RequestProcessor to check that the user is logged in and he has one of the roles to execute a particular action before executing every request.
ActionServlet: You can extend the ActionServlet class if you want to execute your business logic at either application startup or shutdown, or during request processing. But you should use it only in cases where neither PlugIn nor RequestProcessor is able to fulfill your requirement.
In this article, we will use a sample Struts application to demonstrate how to extend Struts using each of these three approaches. Downloadable sample code for each is available below in the Resources section at the end of this article. Two of the most successful examples of Struts extensions are the Struts Validation framework and the Tiles framework.
I assume that you are already familiar with the Struts framework and know how to create simple applications using it. Please see the Resources section if you want to know more about Struts.
PlugIn
According to the Struts documentation "A plugin is a configuration wrapper for a module-specific resource or service that needs to be notified about application startup and shutdown events." What this means is that you can create a class implementing the PlugIn interface to do something at application startup or shutdown.
Say I am creating a web application where I am using Hibernate as the persistence mechanism, and I want to initialize Hibernate as soon as the application starts up, so that by the time my web application receives the first request, Hibernate is already configured and ready to use. We also want to close down Hibernate when the application is shutting down. We can implement this requirement with a Hibernate PlugIn by following two simple steps.
Create a class implementing the PlugIn interface, like this:
2.
3. public class HibernatePlugIn implements PlugIn{
4. private String configFile;
5. // This method will be called at application shutdown time
6. public void destroy() {
7. System.out.println("Entering HibernatePlugIn.destroy()");
8. //Put hibernate cleanup code here
9. System.out.println("Exiting HibernatePlugIn.destroy()");
10. }
11. //This method will be called at application startup time
12. public void init(ActionServlet actionServlet, ModuleConfig config)
13. throws ServletException {
14. System.out.println("Entering HibernatePlugIn.init()");
15. System.out.println("Value of init parameter " +
16. getConfigFile());
17. System.out.println("Exiting HibernatePlugIn.init()");
18. }
19. public String getConfigFile() {
20. return name;
21. }
22. public void setConfigFile(String string) {
23. configFile = string;
24. }
25. }
The class implementing PlugIn interface must implement two methods: init() and destroy(). init() is called when the application starts up, and destroy() is called at shutdown. Struts allows you to pass init parameters to your PlugIn class. For passing parameters, you have to create JavaBean-type setter methods in your PlugIn class for every parameter. In our HibernatePlugIn class, I wanted to pass the name of the configFile instead of hard-coding it in the application.
Inform Struts about the new PlugIn by adding these lines to struts-config.xml:
27.
28. <struts-config>
29. ...
30. <!-- Message Resources -->
31. <message-resources parameter=
32. "sample1.resources.ApplicationResources"/>
33.
34. <!-- Declare your plugins -->
35. <plug-in className="com.sample.util.HibernatePlugIn">
36. <set-property property="configFile"
37. value="/hibernate.cfg.xml"/>
38. </plug-in>
39. </struts-config>
The className attribute is the fully qualified name of the class implementing the PlugIn interface. Add a <set-property> element for every initialization parameter which you want to pass to your PlugIn class. In our example, I wanted to pass the name of the config file, so I added the <set-property> element with the value of config file path.
Both the Tiles and Validator frameworks use PlugIns for initialization by reading configuration files. Two more things which you can do in your PlugIn class are:
If your application depends on some configuration files, then you can check their availability in the PlugIn class and throw a ServletException if the configuration file is not available. This will result in ActionServlet becoming unavailable.
The PlugIn interface's init() method is your last chance if you want to change something in ModuleConfig, which is a collection of static configuration information that describes a Struts-based module. Struts will freeze ModuleConfig once all PlugIns are processed.
How a Request is Processed
ActionServlet is the only servlet in Struts framework, and is responsible for handling all of the requests. Whenever it receives a request, it first tries to find a sub-application for the current request. Once a sub-application is found, it creates a RequestProcessor object for that sub-application and calls its process() method by passing it HttpServletRequest and HttpServletResponse objects.
The RequestProcessor.process() is where most of the request processing takes place. The process() method is implemented using the Template Method design pattern, in which there is a separate method for performing each step of request processing, and all of those methods are called in sequence from the process() method. For example, there are separate methods for finding the ActionForm class associated with the current request, and checking if the current user has one of the required roles to execute action mapping. This gives us tremendous flexibility. The RequestProcessor class in the Struts distribution provides a default implementation for each of the request-processing steps. That means you can override only the methods that interest you, and use default implementations for rest of the methods. For example, by default Struts calls request.isUserInRole() to find out if the user has one of the roles required to execute the current ActionMapping, but if you want to query a database for this, then then all you have to do is override the processRoles() method and return true or false, based whether the user has the required role or not.
First we will see how the process() method is implemented by default, and then I will explain what each method in the default RequestProcessor class does, so that you can decide what parts of request processing you want to change.
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);
}
processMultipart(): In this method, Struts will read the request to find out if its contentType is multipart/form-data. If so, it will parse it and wrap it in a wrapper implementing HttpServletRequest. When you are creating an HTML FORM for posting data, the contentType of the request is application/x-www-form-urlencoded by default. But if your form is using FILE-type input to allow the user to upload files, then you have to change the contentType of the form to multipart/form-data. But by doing that, you can no longer read form values submitted by user via the getParameter() method of HttpServletRequest; you have to read the request as an InputStream and parse it to get the values.
processPath(): In this method, Struts will read request URI to determine the path element that should be used for getting the ActionMapping element.
processLocale(): In this method, Struts will get the Locale for the current request and, if configured, it will save it in HttpSession as the value of the org.apache.struts.action.LOCALE attribute. HttpSession would be created as a side effect of this method. If you don't want that to happen, then you can set the locale property to false in ControllerConfig by adding these lines to your struts-config.xml file:
4. <controller>
5. <set-property property="locale" value="false"/>
6. </controller>
processContent(): Sets the contentType for the response by calling response.setContentType(). This method first tries to get the contentType as configured in struts-config.xml. It will use text/html by default. To override that, use the following:
8. <controller>
9. <set-property property="contentType" value="text/plain"/>
10. </controller>
processNoCache(): Struts will set the following three headers for every response, if configured for no-cache:
12.
13. requested in struts config.xml
14. response.setHeader("Pragma", "No-cache");
15. response.setHeader("Cache-Control", "no-cache");
16. response.setDateHeader("Expires", 1);
If you want to set the no-cache header, add these lines to struts-config.xml:
<controller>
<set-property property="noCache" value="true"/>
</controller>
processPreprocess(): This is a general purpose, pre-processing hook that can be overridden by subclasses. Its implementation in RequestProcessor does nothing and always returns true. Returning false from this method will abort request processing.
processMapping(): This will use path information to get an ActionMapping object. The ActionMapping object represents the <action> element in your struts-config.xml file.
19.
20. <action path="/newcontact" type="com.sample.NewContactAction"
21. name="newContactForm" scope="request">
22. <forward name="sucess" path="/sucessPage.do"/>
23. <forward name="failure" path="/failurePage.do"/>
24. </action>
The ActionMapping element contains information like the name of the Action class and ActionForm used in processing this request. It also has information about ActionForwards configured for the current ActionMapping.
processRoles(): Struts web application security just provides an authorization scheme. What that means is once user is logged into the container, Struts' processRoles() method can check if he has one of the required roles for executing a given ActionMapping by calling request.isUserInRole().
26.
27. <action path="/addUser" roles="administrator"/>
Say you have AddUserAction and you want only the administrator to be able to add a new user. What you can do is to add a role attribute with the value administrator in your AddUserAction action element. So before executing AddUserAction, it will always make sure that the user has the administrator role.
processActionForm(): Every ActionMapping has a ActionForm class associated with it. When Struts is processing an ActionMapping, it will find the name of the associated ActionForm class from the value of the name attribute in the <action> element.
29. <form-bean name="newContactForm"
30. type="org.apache.struts.action.DynaActionForm">
31. <form-property name="firstName"
32. type="java.lang.String"/>
33. <form-property name="lastName"
34. type="java.lang.String"/>
35. </form-bean>
In our example, it will first check to see if an object of the org.apache.struts.action.DynaActionForm class is present in request scope. If so, it will use it; otherwise, it will create a new object and set it in the request scope.
processPopulate(): In this method, Struts will populate the ActionForm class instance variables with values of matching request parameters.
processValidate(): Struts will call the validate() method of your ActionForm class. If you return ActionErrors from the validate() method, it will redirect the user to the page indicated by the input attribute of the <action>element.
processForward() and processInclude(): In these functions, Struts will check the value of the forward or include attributes of the <action> element and, if found, put the forward or include request in the configured page.
39.
40. <action forward="/Login.jsp" path="/loginInput"/>
41. <action include="/Login.jsp" path="/loginInput"/>
You can guess difference in these functions from their names. processForward() ends up calling RequestDispatcher.forward(), and processInclude() calls RequestDispatcher.include(). If you configure both forward and include attributes, it will always call forward, as it is processed first.
processActionCreate(): This function gets the name of the Action class from the type attribute of the <action> element and create and return instances of it. In our case it will create an instance of the com.sample.NewContactAction class.
processActionPerform(): This function calls the execute() method of your Action class, which is where you should write your business logic.
processForwardConfig(): The execute()method of your Action class will return an object of type ActionForward, indicating which page should be displayed to the user. So Struts will create RequestDispatcher for that page and call the RequestDispatcher.forward() method.
The above list explains what the default implementation of RequestProcessor does at every stage of request processing and the sequence in which various steps are executed. As you can see, RequestProcessor is very flexible and it allows you to configure it by setting properties in the <controller> element. For example, if your application is going to generate XML content instead of HTML, then you can inform Struts about this by setting a property of the controller element.
Creating Your own RequestProcessor
Above, we saw how the default implementation of RequestProcessor works. Now we will present a example of how to customize it by creating our own custom RequestProcessor. To demonstrate creating a custom RequestProcessor, we will change our sample application to implement these two business requirements:
We want to create a ContactImageAction class that will generate images instead of a regular HTML page.
Before processing every request, we want to check that user is logged in by checking for userName attribute of the session. If that attribute is not found, we will redirect the user to the login page.
We will change our sample application in two steps to implement these business requirements.
Create your own CustomRequestProcessor class, which will extend the RequestProcessor class, like this:
2. public class CustomRequestProcessor
3. extends RequestProcessor {
4. protected boolean processPreprocess (
5. HttpServletRequest request,
6. HttpServletResponse response) {
7. HttpSession session = request.getSession(false);
8. //If user is trying to access login page
9. // then don't check
10. if( request.getServletPath().equals("/loginInput.do")
11. || request.getServletPath().equals("/login.do") )
12. return true;
13. //Check if userName attribute is there is session.
14. //If so, it means user has allready logged in
15. if( session != null &&
16. session.getAttribute("userName") != null)
17. return true;
18. else{
19. try{
20. //If no redirect user to login Page
21. request.getRequestDispatcher
22. ("/Login.jsp").forward(request,response);
23. }catch(Exception ex){
24. }
25. }
26. return false;
27. }
28.
29. protected void processContent(HttpServletRequest request,
30. HttpServletResponse response) {
31. //Check if user is requesting ContactImageAction
32. // if yes then set image/gif as content type
33. if( request.getServletPath().equals("/contactimage.do")){
34. response.setContentType("image/gif");
35. return;
36. }
37. super.processContent(request, response);
38. }
39. }
In the processPreprocess method of our CustomRequestProcessor class, we are checking for the userName attribute of the session and if it's not found, redirect the user to the login page.
For our requirement of generating images as output from the ContactImageAction class, we have to override the processContent method and first check if the request is for the /contactimage path. If so, we set the contentType to image/gif; otherwise, it's text/html.
Add these lines to your struts-config.xml file after the <action-mapping> element to inform Struts that CustomRequestProcessor should be used as the RequestProcessor class:
41. <controller>
42. <set-property property="processorClass"
43. value="com.sample.util.CustomRequestProcessor"/>
44. </controller>
Please note that overriding processContent() is OK if you have very few Action classes where you want to generate output whose contentType is something other than text/html. If that is not the case, you should create a Struts sub-application for handling requests for image-generating Actions and set image/gif as the contentType for it.
The Tiles framework uses its own RequestProcessor for decorating output generated by Struts.
ActionServlet
If you look into the web.xml file of your Struts web application, it looks like this:
<web-app >
<servlet>
<servlet-name>action=</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<!-- All your init-params go here-->
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app >
That means ActionServlet is responsible for handling all of your requests to Struts. You can create a sub-class of the ActionServlet class if you want to do something at application startup or shutdown or on every request, but you should try creating a PlugIn or RequestProcessor before extending the ActionServlet class. Before Servlet 1.1, the Tiles framework was based on extending the ActionServlet class to decorate a generated response. But from 1.1 on, it's used the TilesRequestProcessor class.
Conclusion
Deciding to develop your own MVC framework is a very big decision--you should think about the time and resources it will take to develop and maintain that code. Struts is a very powerful and stable framework and you can change it to accommodate most of your business requirements.
On the other hand, the decision to extend Struts should not be taken lightly. If you put some low-performance code in your RequestProcessor class, it will execute on every request and can reduce the performance of your whole application. And there will be situations where it will better for you to create your own MVC framework than extend Struts.
Resources
Download sample code for this article.
"Introduction to the Jakarta Struts Framework"
Sunil Patil has worked on J2EE technologies for four years. He is currently working with IBM Software Labs.