Extending StrutsI 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. |
Related Reading
Programming Jakarta Struts |
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.
PlugInAccording 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:
public class HibernatePlugIn implements PlugIn{
private String configFile;
// This method will be called at application shutdown time
public void destroy() {
System.out.println("Entering HibernatePlugIn.destroy()");
//Put hibernate cleanup code here
System.out.println("Exiting HibernatePlugIn.destroy()");
}
//This method will be called at application startup time
public void init(ActionServlet actionServlet, ModuleConfig config)
throws ServletException {
System.out.println("Entering HibernatePlugIn.init()");
System.out.println("Value of init parameter " +
getConfigFile());
System.out.println("Exiting HibernatePlugIn.init()");
}
public String getConfigFile() {
return name;
}
public void setConfigFile(String string) {
configFile = string;
}
}
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:
<struts-config>
...
<!-- Message Resources -->
<message-resources parameter=
"sample1.resources.ApplicationResources"/>
<!-- Declare your plugins -->
<plug-in className="com.sample.util.HibernatePlugIn">
<set-property property="configFile"
value="/hibernate.cfg.xml"/>
</plug-in>
</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:
PlugIn class and throw a ServletException if
the configuration file is not available. This will result in ActionServlet becoming
unavailable.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. 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:
<controller>
<set-property property="locale" value="false"/>
</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:
<controller>
<set-property property="contentType" value="text/plain"/>
</controller>
processNoCache(): Struts will set the following three
headers for every response, if configured for no-cache:
requested in struts config.xml
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
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.
<action path="/newcontact" type="com.sample.NewContactAction"
name="newContactForm" scope="request">
<forward name="sucess" path="/sucessPage.do"/>
<forward name="failure" path="/failurePage.do"/>
</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().
<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.
<form-bean name="newContactForm"
type="org.apache.struts.action.DynaActionForm">
<form-property name="firstName"
type="java.lang.String"/>
<form-property name="lastName"
type="java.lang.String"/>
</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.
<action forward="/Login.jsp" path="/loginInput"/>
<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.
RequestProcessorAbove, 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:
ContactImageAction class that will generate
images instead of a regular HTML page.userName attribute of the session. If that attribute is not
found, we will redirect the user to the login page. CustomRequestProcessor class, which will extend the RequestProcessor class, like this:
public class CustomRequestProcessor
extends RequestProcessor {
protected boolean processPreprocess (
HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession(false);
//If user is trying to access login page
// then don't check
if( request.getServletPath().equals("/loginInput.do")
|| request.getServletPath().equals("/login.do") )
return true;
//Check if userName attribute is there is session.
//If so, it means user has allready logged in
if( session != null &&
session.getAttribute("userName") != null)
return true;
else{
try{
//If no redirect user to login Page
request.getRequestDispatcher
("/Login.jsp").forward(request,response);
}catch(Exception ex){
}
}
return false;
}
protected void processContent(HttpServletRequest request,
HttpServletResponse response) {
//Check if user is requesting ContactImageAction
// if yes then set image/gif as content type
if( request.getServletPath().equals("/contactimage.do")){
response.setContentType("image/gif");
return;
}
super.processContent(request, response);
}
}
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.
<action-mapping> element
to inform Struts that CustomRequestProcessor should be used
as the RequestProcessor class:
<controller>
<set-property property="processorClass"
value="com.sample.util.CustomRequestProcessor"/>
</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. RequestProcessor for decorating
output generated by Struts.
ActionServletIf 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.
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.
Sunil Patil has worked on J2EE technologies for more than five years. His areas of interest include object relational mapping tools, UI frameworks, and portals.
Return to ONJava.com
Copyright © 2009 O'Reilly Media, Inc.