Any application can shine under normal conditions; how an application responds to unexpected conditions reveals much more about its robustness and production-readiness. As you saw in Chapter 8, the Struts Validator provides a means for handling unexpected user input. However, applications need to handle unexpected system behavior. This behavior typically manifests as exceptions. These exceptions may be business-related, may have meaning to the end user, or they may be system- and coding-related, or may have meaning to system administrators and developers. This chapter will show you some good solutions for handling both of these cases.
Remove the exception-handling
code from your Action
,
and define global and local exception handlers in your
struts-config.xml
file, as shown in Example 9-1.
... <global-exceptions> <exception key="error.unknown.user" type="com.oreilly.strutsckbk.ch09.UnknownUserException" path="/securityError.jsp"/> </global-exceptions> ... <action-mappings> <action path="/Login" type="com.oreilly.strutsckbk.ch09.LoginAction" scope="request" name="LoginForm" validate="true" input="/login.jsp"> <exception key="error.password.match" type="com.oreilly.strutsckbk.ch09.PasswordMatchException"> <forward name="success" path="/login_success.jsp"/> </action> ...
Prior to Struts 1.1, the handling of exceptions was left to the
devloper’s devices. Exceptions returned from calls
to the business layer from an Action
had to be
handled individually in your code. Because the perform(
)
method of Struts 1.0 only allowed you to throw
IOException
and
ServletException
, you didn’t have
much choice in the matter:
public ActionForward perform( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { ...
Any checked exception thrown within the body of perform()
had to be caught and handled. In some cases, it was
appropriate to catch the exception, generate an
ActionError
, and forward to the
input
page, much like a validation failure. But
more often, the exception couldn’t be handled by the
application, so the developer returned a
ServletException
wrapped around the application
exception. With Struts 1.1, the
perform()
method was deprecated and the execute(
)
method was introduced. Unlike perform()
, execute( )
can throw any exception,
and once an exception is thrown, you can process using exception
handlers configured in your struts-config.xml
file.
If you migrated an Action class from Struts 1.0 to Struts without using declarative exception handling, the class might look something like Example 9-2.
package com.oreilly.strutsckbk.ch09; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { String username = null; String password = null; ActionErrors errors = new ActionErrors( ); try { username = (String) PropertyUtils.getSimpleProperty(form, "username"); password = (String) PropertyUtils.getSimpleProperty(form, "password"); } catch (Exception e) { throw new IOException("Unable to retrieve username and password"); } SecurityService service = new SecurityService( ); try { service.authenticate( username, password); } catch (UnknownUserException e1) { errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("error.unknown.user")); saveErrors(request, errors); return mapping.findForward("securityError"); } catch (PasswordMatchException e) { errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("error.password.match")); } // Report any errors we have discovered back to the original form if (!errors.isEmpty( )) { saveErrors(request, errors); return (mapping.getInputForward( )); } User user = new User( ); user.setUsername(username); request.getSession( ).setAttribute("user", user); return mapping.findForward("success"); } }
There’s a lot of code here whose sole purpose is
handling exceptions. The first try . . . catch
block handles technical exceptions thrown in
getting data from the form. The more interesting application
exceptions are thrown by the SecurityService
. The
UnknownUserException
is handled by generating an
error, saving the errors in the request, and forwarding to a Struts
forward. The PasswordMatchException
is handled
similarly, except that the page is forwarded to the
input
path for the action, where the error will
probably be reported and the user can try again.
All of this exception-handling code can be eliminated by using
declarative
exception handling. Example 9-3 shows the
LoginAction
with the exception handling removed.
package com.oreilly.strutsckbk.ch09; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { String username = (String) PropertyUtils.getSimpleProperty(form, "username"); String password = (String) PropertyUtils.getSimpleProperty(form, "password"); SecurityService service = new SecurityService( ); service.authenticate( username, password); User user = new User( ); user.setUsername(username); request.getSession( ).setAttribute("user", user); return mapping.findForward("success"); } }
The exceptions are now handled declaratively, as shown in the
Solution. The UnknownUserException
is declared as
a global exception:
<global-exceptions> <exception key="error.unknown.user" type="com.oreilly.strutsckbk.ch09.UnknownUserException" path="/securityError.jsp"/> ... </global-exceptions>
The key
specifies a
MessageResources
message that will be used to
generate an ActionError
. The path specifies the
name of the resource to forward to. If path is omitted, the
input
page for an action
will
be used. The generated ActionError
is stored in an
ActionErrors
object and can be displayed on the
destination page using the
<html:errors/>
tag.
Any ActionErrors
stored in the request by your
Action
will be replaced if your
Action
throws an exception handled declaratively.
If you want to preserve existing ActionErrors
,
you’ll need to create a custom
ExceptionHandler
.
Because this exception is declared as a
global-exception
, any action that throws an
UnknownUserException
will be handled by this
declaration unless overridden by a local
exception.
A local exception is defined by
nesting the exception
element within the
action
element to which it applies. It defines
action-specific handling for specified types of exceptions. The
PasswordMatchException
is handled by a local
exception:
<action path="/Login" ...> <exception key="error.password.match" type="com.oreilly.strutsckbk.ch09.PasswordMatchException" path="/login.jsp"/> <forward .../> </action>
So what happens when you throw an exception that
doesn’t have a declared handler? These exceptions
will be wrapped in a ServletException
and
ultimately thrown by the ActionServlet.service( )
method to be handled by the application server.
For more information on custom display of error messages, see Recipe 9-6.
You can provide custom exception handling by extending the Struts exception handler. This approach is discussed in Recipe 9.2.
If you want to provide a fallback exception-handling approach instead of relying on the container, take a look at Recipe 9.4.
Your application requires specialized handling for certain types of exceptions.
Extend the Struts
ExceptionHandler
with your own class such as the one
shown in Example 9-4.
package com.oreilly.strutsckbk.ch09; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ExceptionHandler; import org.apache.struts.config.ExceptionConfig; public class CustomExceptionHandler extends ExceptionHandler { public ActionForward execute(Exception ex, ExceptionConfig ae, ActionMapping mapping, ActionForm formInstance, HttpServletRequest request, HttpServletResponse response) throws ServletException { // TODO Add custom code here to completely control handling return super.execute(ex, ae, mapping, formInstance, request, response); } protected void logException(Exception e) { // TODO Add custom code here for exception logging System.out.println("Customized logException for:"+e); super.logException(e); } protected void storeException(HttpServletRequest request, String property, ActionMessage error, ActionForward forward, String scope) { // TODO Add custom code here for storing errors System.out.println("Customized error storing for:"+error); super.storeException(request, property, error, forward, scope); } }
In your struts-config.xml file, specify your
ExceptionHandler
’s class name as
the value for the handler
attribute on each
exception
element you want to use the handler:
<exception key="error.exception" type="com.oreilly.strutsckbk.ch09.CustomException" handler="com.oreilly.strutsckbk.ch09.CustomExceptionHandler" path="/some_error_page.jsp"/>
You might think that if you use declarative exceptions you are “locked in” to the Struts-way of processing exceptions. In fact, you are not bound to the Struts exception-handling process for two oft overlooked reasons; Struts was designed for extensibility, and Struts is open source.
When an Action
throws an
Exception
, the RequestProcessor
handles it in the
processException
method:
protected ActionForward processException(HttpServletRequest request, HttpServletResponse response, Exception exception, ActionForm form, ActionMapping mapping) throws IOException, ServletException { // Is there a defined handler for this exception? ExceptionConfig config = mapping.findException(exception.getClass( )); if (config == null) { log.warn(getInternal( ).getMessage("unhandledException", exception.getClass( ))); if (exception instanceof IOException) { throw (IOException) exception; } else if (exception instanceof ServletException) { throw (ServletException) exception; } else { throw new ServletException(exception); } } // Use the configured exception handling try { ExceptionHandler handler = (ExceptionHandler) RequestUtils.applicationInstance(config.getHandler( )); return (handler.execute(exception, config, mapping, form, request, response)); } catch (Exception e) { throw new ServletException(e); } }
First, the method searches for an exception
configuration—represented as an
ExceptionConfig
object—for the exception
type. It searches for a local declarative exception; if none can be
found, it looks for a global declarative exception. The search
mechanism takes object inheritance into account—that is, if
FooException
extends
BarException
and there is an
exception
element for
BarException
, then mapping.findException(
)
will use that configuration when
FooException
is thrown. If a declarative exception
is found, Struts instantiates the associated
ExceptionHandler
and calls the
handler’s execute( )
method.
An
ExceptionHandler
mimics an Action
. Its execute()
method accepts the same arguments as
Action.execute( )
plus additional arguments for
the Exception
and
ExceptionConfig
objects. You create a custom
exception handler by extending the
ExceptionHandler
.
The base
ExceptionHandler
defines two protected methods designed for extension. The first,
logException()
, is called to log the thrown exception.
Here’s the source for this method from the
ExceptionHandler
class:
protected void logException(Exception e){ log.debug(messages.getMessage("exception.log"), e); }
If you need to provide custom logging behavior, you can override this method; however, you should investigate the Struts logging mechanism before you start adding a lot of custom code (see Recipe 13.2).
The second protected method,
storeException()
, is somewhat misnamed because it stores an
ActionMessage
(ActionError
in
Struts 1.1), created for the exception in the
ExceptionHandler.execute( )
method, in the
HttpServletRequest
or
HttpSession
. Here’s the source
for this method from the ExceptionHandler
class:
protected void storeException( HttpServletRequest request, String property, ActionMessage error, ActionForward forward, String scope) { ActionMessages errors = new ActionMessages( ); errors.add(property, error); if ("request".equals(scope)) { request.setAttribute(Globals.ERROR_KEY, errors); } else { request.getSession( ).setAttribute(Globals.ERROR_KEY, errors); } }
The errors are stored in the request or session based on the value of
the scope
attribute for the declarative exception.
You override storeException( )
if you want to
change or augment the base behavior. For example,
let’s say you wanted to store the error message in a
database:
protected void storeException(HttpServletRequest request, String property, ActionMessage error, ActionForward forward, String scope) { MessageResources msgRes = MessageResources.getMessageResources("ApplicationResources"); String msg = msgRes.getMessage(error.getKey( )); saveMessage(msg); super.storeException(request, property, error, forward, scope); } private void saveMessage(String msg) { // store error message in database ... }
If you need to do more than customize the logging or storing of the
exception, you will need to override the
execute()
method. Start by cutting and pasting the code from the
base ExceptionHandler
’s
execute( )
method, shown in Example 9-5, into your handler’s
execute( )
method and modify as needed.
public ActionForward execute( Exception ex, ExceptionConfig ae, ActionMapping mapping, ActionForm formInstance, HttpServletRequest request, HttpServletResponse response) throws ServletException { ActionForward forward = null; ActionMessage error = null; String property = null; // Build the forward from the exception mapping if it exists // or from the form input if (ae.getPath( ) != null) { forward = new ActionForward(ae.getPath( )); } else { forward = mapping.getInputForward( ); } // Figure out the error if (ex instanceof ModuleException) { error = ((ModuleException) ex).getActionMessage( ); property = ((ModuleException) ex).getProperty( ); } else { error = new ActionMessage(ae.getKey( ), ex.getMessage( )); property = error.getKey( ); } logException(ex); // Store the exception request.setAttribute(Globals.EXCEPTION_KEY, ex); storeException(request, property, error, forward, ae.getScope( )); return forward; }
The
execute( )
method
takes the following steps:
The forward path is determined from the
ExceptionConfig
. If no path is specified, then the
action
’s input
path is used (accessed using mapping.getInputForward(
)
).
If the exception is a ModuleException
, the
contained ActionMessage
is retrieved. Otherwise, a
new ActionMessage
is generated using the
key
from the ExceptionConfig
.
The exception is logged using the logException( )
method.
The exception is added as an attribute to the
HttpServletRequest
.
The error is stored by calling storeException( )
.
The forward
is returned.
The ActionMessage
generation (step 2) is a common
function you might want to customize. Recipe 9.3 shows an example of this.
If you want to change the logging of exceptions, consider using the customization features of Struts’ logging mechanism instead of a custom exception handler. Recipe 13.2 has details on this approach. You can find information at http://struts.apache.org/userGuide/configuration.html#config_logging.
The Struts User’s Guide discusses custom exception handlers in the section found at http://struts.apache.org/userGuide/building_controller.html#exception_handler.
Recipe 9.1 shows you how to configure exception handling in your struts-config.xml file. Recipe 9.3 shows a more ambitious use of customized exception handling.
You want to use an exception class that accepts a logical error code or key and optional arguments that can be used to display a localized error message.
Use or extend my
ErrorCodeException
,
shown in Example 9-6.
package com.oreilly.strutsckbk.ch09; public class ErrorCodeException extends Exception { public ErrorCodeException(int code) { this.code = code; } public ErrorCodeException(int code, Object[] args) { this.code = code; this.args = args; } public ErrorCodeException(int code, Object[] args, String msg) { super(msg); this.code = code; this.args = args; } public ErrorCodeException(int code, Object[] args, String msg, Throwable cause) { super(msg, cause); this.code = code; this.args = args; } public int getCode( ) { return code; } public Object[] getArgs( ) { return args; } private Object[] args; private int code; }
Use my
ErrorCodeExceptionHandler
,
shown in Example 9-7, to handle these exception
types.
package com.oreilly.strutsckbk.ch09; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.Globals; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ExceptionHandler; import org.apache.struts.config.ExceptionConfig; public class ErrorCodeExceptionHandler extends ExceptionHandler { public ActionForward execute( Exception ex, ExceptionConfig ae, ActionMapping mapping, ActionForm formInstance, HttpServletRequest request, HttpServletResponse response) throws ServletException { if (!(ex instanceof ErrorCodeException)) { return super.execute(ex, ae, mapping, formInstance, request, response); } ErrorCodeException errCodeEx =(ErrorCodeException) ex; ActionForward forward = null; ActionMessage error = null; String property = null; // Build the forward from the exception mapping if it exists // or from the form input if (ae.getPath( ) != null) { forward = new ActionForward(ae.getPath( )); } else { forward = mapping.getInputForward( ); } ErrorCodeException ece =(ErrorCodeException) ex; String code = Integer.toString(ece.getCode( )); error = new ActionMessage(code, ece.getArgs( )); property = error.getKey( ); logException(ex); // Store the exception request.setAttribute(Globals.EXCEPTION_KEY, ex); storeException(request, property, error, forward, ae.getScope( )); return forward; } }
Finally, declare a global exception handler in your struts-config.xml for this exception:
<global-exceptions>
...
<exception key="error.exception"
type="com.oreilly.strutsckbk.ch09.ErrorCodeException"
handler="com.oreilly.strutsckbk.ch09.ErrorCodeExceptionHandler"
path="/error_page.jsp
"/>
</global-exceptions>
A lot of large software applications report problems using error
codes as well as text. The Oracle database reports errors using an
error code formatted as ORA-9999
. You can
use an
Oracle utility program
to retrieve the meaning of the error code. This approach allows the
application to localize text messages based on the
user’s language and country. The
ErrorCodeException
shown in Example 9-6 can be used in a similar manner in your Struts
application. You create this exception class using a numeric error
code and an optional set of Object arguments. The error code serves
as a key for retrieving a localized message from your
MessageResources
properties file. The arguments
are used at runtime as substitution parameters in that message.
When an Action
or business service creates an
ErrorCodeException
, it can populate the exception
with an appropriate error code and an array of arguments for message
substitution. The ErrorCodeExceptionHandler
shown
in Example 9-7 is designed to process
ErrorCodeException
s. The code in the
execute( )
method was originally copied from the
Struts base ExceptionHandler
(org.apache.struts.action.ExceptionHandler
) and
modified as needed. This custom handler first checks if the error is
of type ErrorCodeException
; if not, it allows the
Struts exception handler to process the Exception
by calling super.execute( )
. Otherwise, the
exception is handled as follows:
The path to forward to is determined from the
ExceptionConfig
. If no path is specified, the
action
’s input
path is used (accessed using mapping.getInputForward(
)
).
The error code is retrieved from the
ErrorCodeException
and converted to a
String
.
An ActionMessage
is created using the error code
String
and the Object
array
from the ErrorCodeException
.
The exception is logged using the logException( )
method (implemented by the super class).
The exception is added as an attribute to the
HttpServletRequest
.
The error is stored by calling storeException( )
(implemented by the super class).
The forward
is returned.
Only steps 2 and 3 are specific to this handler. The other steps are
identical to the handling in the base
ExceptionHandler
.
Though not required, you can extend the
ErrorCodeException
class to create exception types
for specific functional areas of your application. Using this
approach allows you to control exception processing based on type,
yet give the benefits provided by using an error code and message
arguments.
If you have an existing application that relies heavily on predefined
error codes, this Solution can help bridge the gap with exception
handling. The error codes and corresponding error messages can be
defined in your application’s default
MessageResources
file, or they can be stored in
their own properties file. Since you are coding the exception
handler, you can do it how you want.
The ErrorCodeException
and corresponding handler
are useful if you need to localize the generated message in support
of internationalization. Since the exception handler uses
MessageResources
, the message can be localized by
creating a properties file for specific locales.
Best practices for localizing exceptions have been discussed a number of times on the struts-user mailing list. You’ll find the thread at http://marc.theaimsgroup.com/?l=struts-user&m=109474569119868&w=2 to be particularly interesting.
Creating a custom exception handler is easier than you think. See Recipe 9.2 for details.
Your application should display the same error page for any server error or uncaught exception thrown from a Struts Action, a servlet, or a JSP page.
Declare a global error page, such as the one shown in Example 9-8, to handle all exceptions and errors in your web.xml file as well as your struts-config.xml file.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <%@ page language="java" isErrorPage="true" %> <%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jstl/fmt" prefix="fmt"%> <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %> <%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %> <html:html> <head> <title>Struts Cookbook Chapter 9 : Global error page</title> <style type="text/css"> h2{background:darkblue;color:white} h3{background:darkblue;color:white} </style> </head> <body> <div align="center"> <c:choose> <c:when test="${not empty pageContext.exception}"> <c:set var="problemType">JSP Exception</c:set> <c:set var="appException" value="${pageContext.exception}"/> <c:set var="causeException" value="${appException.cause}"/> </c:when> <c:when test="${not empty requestScope['javax.servlet. error.exception']}"> <c:set var="problemType">Servlet Exception</c:set> <c:set var="appException" value="${requestScope['javax. servlet.error.exception']}"/> <c:set var="causeException" value="${appException.rootCause}"/> </c:when> <c:when test="${not empty requestScope['org.apache.struts.action. EXCEPTION']}"> <c:set var="problemType">Struts Exception</c:set> <c:set var="appException" value="${requestScope['org.apache. struts.action.EXCEPTION']}"/> <c:set var="causeException" value="${appException.cause}"/> </c:when> <c:otherwise> <c:set var="problemType">Unidentified Server Error</c:set> </c:otherwise> </c:choose> <!-- end determine error --> <!-- start framework --> <table cellpadding="0" cellspacing="0" border="0" width="750"> <tr> <td valign="top" colspan="2"> <table cellpadding="4" cellspacing="0" border="0" width="100%"> <tr valign="top"> <td> <!-- start user review --> <table cellpadding="4" cellspacing="0" border="0" width="100%"> <tr> <td> <h2>System problem</h2> </td> </tr> </table> <table cellpadding="2" cellspacing="1" border="0" width="80%"> <tr> <td colspan="2"> A system error has occured. If the problem persists, please contact the User Helpdesk. </td> </tr> <tr><td colspan="2"> <html:errors/> </td></tr> <tr valign="top"> <td> <b>Problem type</b> <br/><c:out value="${problemType}"/> </td> <td> <b>Problem details</b> <c:if test="${not empty requestScope['javax.servlet.error.message']}"> <br/> <c:out value= "${requestScope['javax.servlet.error.message']}" /> </c:if> <c:if test="${not empty appException}"> <br/><c:out value="${appException.message}"/> </c:if> </td> </tr> <c:if test="${not empty causeException}"> <tr> <td> <b>Caused by</b> <br/><c:out value="${causeException}"/> </td> <td> <b>Cause details</b> <br/><c:out value="${causeException.message}"/> </td> </tr> </c:if> </table> <table id="showDetailsLinkDiv" style="{display:inline}" cellpadding="2" cellspacing="1" border="0" width="80%"> <tr> <td align="left"> [ <a href="javascript:showDetails( )"> Show details</a> ] </td> </tr> </table> <table id="hideDetailsLinkDiv" style="{display:none}" cellpadding="2" cellspacing="1" border="0" width="80%"> <tr> <td align="left"> [ <a href="javascript:hideDetails( )"> Hide details</a> ] </td> </tr> </table> <!-- begin details --> <div id="stackTraceDiv" style="{display:none}"> <c:if test="${not empty appException}"> <p></p> <table cellpadding="4" cellspacing="0" border="0" width="100%"> <tr> <td> <h3>Exception stack trace</h3> </td> </tr> </table> <b><c:out value="${appException}"/></b> <br/> <table align="center" cellpadding="0" cellspacing="0" border="0" width="90%" class="pod"> <c:forEach var="stackItem" items="${appException.stackTrace}"> <tr><td><c:out value="${stackItem}"/></td></tr> </c:forEach> </table> </c:if> <c:if test="${not empty causeException}"> <p></p> <table cellpadding="4" cellspacing="0" border="0" width="100%"> <tr> <td> <h3>Cause stack trace</h3> </td> </tr> </table> <b><c:out value="${causeException}"/></b> <br/> <table align="center" cellpadding="0" cellspacing="0" border="0" width="90%" class="pod"> <c:forEach var="stackItem" items="${causeException.stackTrace}"> <tr><td><c:out value="${stackItem}"/></td></tr> </c:forEach> </table> </c:if> </div> <!-- end details --> </td> </tr> </table> </td> </tr> </table> <script language="javascript"> function showDetails( ) { document.getElementById("showDetailsLinkDiv").style.display = "none"; document.getElementById("hideDetailsLinkDiv").style.display = "inline"; document.getElementById("stackTraceDiv").style.display = "inline"; } function hideDetails( ) { document.getElementById("showDetailsLinkDiv").style.display = "inline"; document.getElementById("hideDetailsLinkDiv").style.display = "none"; document.getElementById("stackTraceDiv").style.display = "none"; } </script> </div> </body> </html:html>
Create an error-page
element for handling HTTP
Status 500 errors in your web.xml file,
specifying the location
as the error page:
<error-page> <error-code>500</error-code> <location>/error.jsp</location> </error-page>
For exceptions thrown from Struts Action
s, create
a global exception handler for exceptions of type
java.lang.Exception
in the
struts-config.xml file with the
path
set to the error page:
<global-exceptions> <exception key="error.general" type="java.lang.Exception" path="/error.jsp"/> ... </global-exceptions>
Exception handling tends to be an afterthought. You’re sitting there testing the path for your application when a 500 error pops up and your boss says “Ugh. That’s ugly. I want the same page to come up no matter what the problem and don’t make the details obvious, but the users need to be able to display the details so they can tell tech support what happened.”
The JSP shown in Example 9-8 can be used for this
because it is designed to handle any type of exception that can be
thrown by the container. If the exception has a causing exception,
the details can be shown for that exception as well. Figure 9-1 shows the display when a
NullPointerException
is thrown from an
Action
.
Clicking “Show Details” reveals the stack trace, as shown in Figure 9-2.
When a ServletException
containing a nested root
cause is thrown by a servlet, the error page shows the cause details
as well. Figure 9-3 shows the error page with the
stack trace details shown.
If you scroll down the page in Figure 9-3, you will see the stack trace for the causing exception, as shown in Figure 9-4.
To handle any situation, the error page determines what kind of
exception has been thrown. The JSTL c:choose
block
contains the following logic:
<c:choose> <c:when test="${not empty pageContext.exception}"> <c:set var="problemType">JSP Exception</c:set> <c:set var="appException" value="${pageContext.exception}"/> <c:set var="causeException" value="${appException.cause}"/> </c:when> <c:when test="${not empty requestScope['javax.servlet.error.exception']}"> <c:set var="problemType">Servlet Exception</c:set> <c:set var="appException" value="${requestScope['javax.servlet.error.exception']}"/> <c:set var="causeException" value="${appException.rootCause}"/> </c:when> <c:when test="${not empty requestScope['org.apache.struts.action.EXCEPTION']}"> <c:set var="problemType">Struts Exception</c:set> <c:set var="appException" value="${requestScope['org.apache.struts.action.EXCEPTION']}"/> <c:set var="causeException" value="${appException.cause}"/> </c:when> <c:otherwise> <c:set var="problemType">Unidentified Server Error</c:set> </c:otherwise> </c:choose>
This block determines if the error is a
JSPException
, ServletException
,
or an exception thrown by Struts. For each exception, the causing
exception is retrieved.
You get the causing exception for a
ServletException
using the
rootCause
property instead of the normal
cause
property.
The rest of the page displays the exception message and exception
stack trace for the exception and its cause, if any. For exceptions
thrown by Struts, the html:errors
tag renders any
error messages. JavaScript provides the magic for showing and hiding
the exception details. The trick is to put the stack trace details in
a div
tag and then use a JavaScript function to
toggle the CSS display
style from
none
to inline
.
The one common error this page doesn’t handle is an
HTTP Status 404 (Page Not Found)
response. If you wanted to use this page for handling these errors,
add an additional error-page
element to the
web.xml. However, you will probably want to
handle this error differently, perhaps forwarding to the main menu
page of your application or some other location.
This solution works well for intranet applications; for external applications, though, you will probably want to display less information for security purposes. One thing you can do is use this error page in development and testing and then replace it with a terser version when you deploy to production.
Recipe 9.1 shows you the details on
configuring a global exception using Struts declarative exception
handling. Recipe 9.6 shows alternatives to
html:errors
for displaying error messages. Recipe 13-5 shows you a JSP page
that you can use specifically for debugging.
For other approaches to localizing exception messages, take a look at some guidelines from Sun’s Java Blueprints at http://java.sun.com/blueprints/guidelines/designing_enterprise_applications_2e/i18n/i18n8.html.
Brian Goetz’s excellent series on exception handling from JavaWorld is a must-read for any Java developer. He discusses the use of message catalogs for exception localization in Part 3 of the series. You can find the entire series at http://www.javaworld.com/javaworld/jw-08-2001/jw-0803-exceptions.html.
You want to display error messages to the user when things go wrong and success messages when things go right.
For Struts 1.1, use ActionErrors
for reporting
errors and ActionMessages
for informational
messages. For Struts 1.2, use ActionMessages
for
informational messages and errors.
Struts gives you a least four classes and three custom tags for
creating, storing, and displaying errors and messages. In Struts 1.1,
the ActionErrors
class is used to hold a
collection of errors, represented by ActionError
instances. Each ActionError
consists of a key to
be looked up from the default MessageResources
bundle and an optional set of Object
s (up to four)
to be used as parameters for message substitution. The
Action
class in Example 9-9 shows
typical usage of ActionErrors
.
package com.oreilly.strutsckbk.ch09; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.Globals; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; public class RegistrationAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { ActionErrors errors = new ActionErrors( ); String username = (String) PropertyUtils.getSimpleProperty( form, "username"); String password = (String) PropertyUtils.getSimpleProperty( form, "password"); if (username.equals(password)) { ActionError error = new ActionError( "error.register.sameAsPassword", username); // Use Action Message with Struts 1.2 //ActionMessage error = new ActionMessage( // "error.register.sameAsPassword", username); errors.add("username", error); } if (!errors.isEmpty( )) { saveErrors(request, errors); return mapping.getInputForward( ); } User user = new User( ); user.setUsername(username); user.setPassword(password); SecurityService service = new SecurityService( ); service.add(user); return mapping.findForward("success"); } }
If you need to create informational messages, use the
ActionMessages
and
ActionMessage
classes in a similar fashion, as
shown in Example 9-10.
package com.oreilly.strutsckbk.ch09; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ActionMessages; public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { ActionMessages messages = new ActionMessages( ); String username = (String) PropertyUtils.getSimpleProperty( form, "username"); String password = (String) PropertyUtils.getSimpleProperty( form, "password"); SecurityService service = new SecurityService( ); service.authenticate( username, password); User user = new User( ); user.setUsername(username); request.getSession( ).setAttribute("user", user); ActionMessage message = new ActionMessage( "message.login.success", username); messages.add(ActionMessages.GLOBAL_MESSAGE, message); if (!messages.isEmpty( )) { saveMessages( request, messages ); } return mapping.findForward("success"); } }
Though you’ll find little difference in the APIs you use to create errors or messages, the approaches for rendering errors or messages can differ. You can display all errors on a JSP page this way:
<html:errors/>
You would think you could do the same thing with the
html:messages
tag, but the
html:messages
tag behaves differently than the
html:errors
tag. By default, the
html:messages
tag retrieves the values stored as
errors—not messages—from the
HttpServletRequest
. You must explicitly set the
message
attribute to
true
to retrieve values stored as
messages.
The html:messages
tag was originally created to
display error messages without requiring that the message resources
contain HTML. After ActionMessage
s were added to
Struts, the html:messages
tag was amended to
support display of information messages in addition to error
messages.
Unlike the html:errors
tag, the
html:messages
tag is a looping tag, like
logic:iterate
. The tag iterates through
ActionErrors
, ActionMessages
,
or an array of strings. The required id
attribute
defines a page-scoped variable that contains the formatted message.
You can use the bean:write
or the JSTL
c:out
tag to display each message:
<html:messages message="true" id="msg"> <c:out value="${msg}"/><br /> </html:messages>
Unlike the html:errors
tag, the
html:messages
tag doesn’t use a
predefined header and footer for display. The presentation details
are left to you. Recipe 9.6 shows you some
ways to create custom presentation for your errors and messages.
What if you are using Struts 1.2? Well, the
ActionError
class has been deprecated in favor of
ActionMessage
in Struts 1.2. On the other hand,
ActionErrors
has not been
deprecated; however, in most places you can use an
ActionMessages
object where you used to use
ActionErrors
.
So, how do you identify errors versus messages? It depends on the attribute name that they are stored under in the request.
The saveErrors( )
method now accepts
ActionMessages
instead of
ActionErrors
; yet it stores the object in the
request as errors using the key identified by the constant
org.apache.struts.Globals.ERROR_KEY
. The
saveMessages( )
method accepts
ActionMessages
. It stores the object in the
request using the key identified by the
org.apache.struts.Globals.MESSAGE_KEY
constant.
The html:errors
and
html:messages
tags are used in Struts 1.2 just
like in Struts 1.1.
If you have ever needed to store messages HTTP session, you can use a
new method, added in Struts 1.2, of the base
Action
class:
saveMessages(HttpSession session, ActionMessages messages);
This method was added to provide a workaround for message display
when the rendering page is the result of a redirect instead of a
forward. When the messages are saved in the servlet request, the JSP
page that displays those messages must be the result of a forward; if
the JSP were redirected to the messages would be lost. With the new
saveMessages( )
method, messages are saved in the
session and are therefore available whether the page is the result of
a forward or a redirect.
You can redirect instead of forward to the “success page” to prevent double-submission problems. See Recipe 7.9 for more details.
Now that the messages are saved in the session,
what’s to
prevent them from showing up on a page where they
shouldn’t? The Struts developers anticipated this
problem. The ActionMessages
class provides the
isAccessed( )
method to indicate if the messages
have been displayed (accessed) or not. The
RequestProcessor
, in the new
processCachedMessages( )
method, checks this value
and removes any messages stored in the session that have been
accessed. So, though you store the messages in the session, they will
be cleaned on the next request after they have been displayed.
Here’s how you would use the new
saveMessages
method in the
Action
shown in Example 9-10:
if (!messages.isEmpty( )) { saveMessages( request.getSession(true), messages ); }
You can change the forward for the success
page to
a redirect:
<forward name="success" path="/login_success.jsp" redirect="true"/>
Your messages will still be available on the success page, and you don’t have to worry about double postings.
Struts 1.2 introduced the ability to have session-scoped action messages. The Struts 1.2 release notes at http://struts.apache.org/userGuide/release-notes.html describe this feature.
You can use the html:messages
tag to render errors
or messages; see Recipe 9.6 for additional
details on this and other related tags.
Your application requires a custom look and feel for error messages
beyond the abilities of the html:errors
tag.
Use the logic:messagesPresent
and the
html:messages
tags to display the error messages
in a custom format. The JSP fragment
(errors.inc.jsp) shown in Example 9-11 can be included on any page that may need to
display errors.
<logic:messagesPresent> <table border="1" bgcolor="orange" width="100%" align="center"> <tr><td> <p> <img src="/images/icon-warning.gif" border="0" vspace="2" hspace="10" align="center"> WARNING: <bean:message key="errors.heading"/> </p> <ul> <html:messages id="error"> <li><bean:write name="error"/></li> </html:messages> </ul> </td></tr> </table> <p> </logic:messagesPresent>
Errors displayed using the JSP code in Example 9-11 result in a display similar to Figure 9-5.
Though the html:errors
tag is a convenient way of
displaying error messages, it’s fairly restrictive
in its formatting. By default, it displays all errors for a page,
starting with a header markup, then each error message, and ending
with footer markup.
Using the property
attribute, you can tell the
html:errors
tag to display error messages for a
specific field. This ability is commonly used to display validation
errors beside the invalid input field.
This tag relies on predefined messages in your
MessageResources
to control the look and feel of
the display. These messages are shown in Table 9-1.
Message key |
Description |
Sample value |
|
Markup rendered for the header |
|
|
Markup rendered before each error message |
|
|
Markup rendered after each error message |
|
|
Markup rendered for the footer |
|
Though you can use these special message resources to format errors many ways, having HTML markup in a properties file spells trouble. After all, one of the goals of using an MVC framework such as Struts is to separate—not commingle—data and its presentation.
The Solution presents an approach where the entire markup for
presentation is contained on the JSP page. First, the
logic:messagesPresent
tag is used to determine if
any errors will display. This tag processes its tag body if it can
find a scoped variable stored under the
org.apache.struts.Global.ERROR_KEY
key. The tag
body contains an HTML table used for formatting. A warning icon and
message are displayed. Then, the html:messages
tag
lists each error message:
<ul> <html:messages id="error"> <li><bean:write name="error"/></li> </html:messages> </ul>
The html:messages
tag doesn’t
render the message; rather, it acts like the
logic:iterate
tag. It iterates over a set of
errors, creating a page-scoped variable containing the error message
with a name equal to the value of the id
attribute. The error message can be rendered using the
bean:write
or c:out
tags.
So what would you do if you needed to display different types of
messages? Say you wanted to display validation errors as warnings,
and exceptions as more severe problems. All you need to do is store
the different messages in the request using attribute names of your
own choosing. Start by creating a base Action
,
like the one shown in Example 9-12, which provides
methods that save the different errors in the servlet request. If you
are using a base Action
, you can add the methods
shown in Example 9-12 to your own class.
package com.oreilly.strutsckbk.ch09; import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.Action; import org.apache.struts.action.ActionMessages; public class BaseAction extends Action { public static final String APP_WARNING_KEY = "APP_WARNING_KEY"; public static final String APP_ERROR_KEY = "APP_ERROR_KEY"; protected void saveAppWarnings(HttpServletRequest request, ActionMessages messages) { saveAppMessages(request, messages, APP_WARNING_KEY); } protected void saveAppErrors(HttpServletRequest request, ActionMessages messages) { saveAppMessages(request, messages, APP_ERROR_KEY); } private void saveAppMessages(HttpServletRequest request, ActionMessages messages, String key) { // Remove any messages attribute if none are required if ((messages == null) || messages.isEmpty( )) { request.removeAttribute(key); return; } // Save the messages we need request.setAttribute(key, messages); } }
Action
s that subclass this base
Action
call these specialized methods to store the
messages based on severity. The custom Action
shown in Example 9-13 subclasses this base
Action
, calling the
saveAppWarnings
and
saveAppErrors
methods as needed.
package com.oreilly.strutsckbk.ch09; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ActionMessages; public class ValidatingLoginAction extends BaseAction { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { ActionMessages appWarnings = form.validate(mapping, request); saveAppWarnings(request, appWarnings); String username = (String) PropertyUtils.getSimpleProperty(form, "username"); String password = (String) PropertyUtils.getSimpleProperty(form, "password"); ActionMessages appErrors = new ActionMessages( ); try { SecurityService service = new SecurityService( ); service.authenticate( username, password); } catch (PasswordMatchException e) { appErrors.add("password", new ActionMessage("error. password.match")); } saveAppErrors(request, appErrors); if (!appWarnings.isEmpty( ) || !appErrors.isEmpty( )) { return mapping.getInputForward( ); } User user = new User( ); user.setUsername(username); request.getSession( ).setAttribute("user", user); ActionMessages messages = new ActionMessages( ); ActionMessage message = new ActionMessage("message.login.success", username); messages.add(ActionMessages.GLOBAL_MESSAGE, message); if (!messages.isEmpty( )) { saveMessages( request.getSession(true), messages ); } return mapping.findForward("success"); } }
If you look closely at the Action
in Example 9-13, you’ll see that it does
something kind of strange: The
ActionForm
’s
validate
method is called within the
execute( )
method. Why would you do something like
this? Well, remember from the requirements, you want to store
validation errors as warnings; in other words, the errors should be
stored under a custom request attribute name. The only way to
accomplish this is for the Action
to call the
ActionForm
’s
validate
method. Since your
Action
controls the validation workflow, configure
the corresponding action
mapping in the
struts-config.xml file
so Struts doesn’t call the
ActionForm
’s
validate
method:
<action path="/ValidatingLogin"
type="com.oreilly.strutsckbk.ch09.ValidatingLoginAction"
scope="request"
name="LoginForm"
validate="false"
input="/validating_login.jsp">
<forward name="success" path="/login_success.jsp" redirect="true"/>
</action>
Now that you’ve got the warnings and errors being
stored under separate attribute names, you can create a common JSP
fragment to display them. The JSP fragment
(errors2.inc.jsp) shown in Example 9-14 uses the name
attribute of
the logic:messagesPresent
tag and
html:messages
tag to indicate the type of error to
process.
<logic:messagesPresent name="APP_ERROR_KEY"> <table border="1" bgcolor="orange" width="100%" align="center"><tr><td> <p> <img src="/images/icon-alert.gif" border="0" vspace="2" hspace="10" align="center"> <bean:message key="errors.heading"/> </p> <ul> <html:messages id="error" name="APP_ERROR_KEY"> <li><bean:write name="error"/></li> </html:messages> </ul> </td></tr></table> <p> </logic:messagesPresent> <logic:messagesPresent name="APP_WARNING_KEY"> <table border="1" bgcolor="yellow" width="100%" align="center"><tr><td> <p> <img src="/images/icon-warning.gif" border="0" vspace="2" hspace="10" align="center"> <bean:message key="warnings.heading"/> </p> <ul> <html:messages id="error" name="APP_WARNING_KEY"> <li><bean:write name="error"/></li> </html:messages> </ul> </td></tr></table> <p> </logic:messagesPresent>
Now, when the errors are displayed, you’ll see a page like the one shown in Figure 9-6.
If you’re new to web development, this last example may seem a bit extreme, but it demonstrates the flexibility afforded by Struts. If you don’t like the way Struts handles errors, then you can do it yourself. As long as you utilize the Struts APIs, you’ll find that the Struts tags can cope with most customizations in an intelligent and logical way.
Ted Husted discusses custom error formatting in his invaluable set of Struts Tips. The specific tip (#17) can be found at http://husted.com/struts/tips/017.html.
Rick Reumann, a frequent poster to the
struts-user mailing list and all-around good
guy, prefers to call the
ActionForm
’s
validate
method as he explains in the archived
posting at http://marc.theaimsgroup.com/?l=struts-user&m=109242668231755&w=2.
If you are using declarative exception handling, you can use a similar approach for storing the exception error message by creating a custom exception handler. Custom exception handlers are discussed in Recipe 9.2.
18.188.178.181