Gracefully Dealing With Errors in Galleria - Part 6
Join the DZone community and get the full member experience.
Join For FreeGeneral Exception Mechanism
The application uses checked exceptions to communicate errors between the layers. The ApplicationException is the root of all possible business exceptions.
I am not going to jump in on checked vs. unchecked exceptions here (Google a bit about it if you are curious). I tend to use checked exceptions when the application has a chance to recover from the error. Unchecked are thrown when something happens which isn't recoverable. That is the reason, I am not happy with the exception handling mechanism build in at the moment. I am going to get into this a little later.
What is missing? ViewExpired and more.
Seems as if everything is handled right now. But only on the first impression. Open the login screen and wait a bit and let your http session timeout. You are now greeted with a not so nice ViewExpired exception screen.
If you are trying this as a loged-in user you are simply redirected to
the login page. Anyway, the same error page could come up for some
other unexpected conditions in the presentation layer. So, let's fix
this. Most obvious thing to do is to simply introduce a dedicated
error-page.
<error-page> <exception-type>javax.faces.application.ViewExpiredException</exception-type> <location>/viewExpired.xhtml</location> </error-page>
Now you redirect your users to a dedicated page which could tell him/her
something nice about workplace security and not leaving the app
unattended for such a long time. This works for most of the applications
out there. If you are willing to have some additional information on
the page or simply want to catch more than one exception and handle them
individually without having to configure them statically, you need
something called an ExceptionHandler. This is new in JSF 2 and all you
need to do is to implement an ExceptionHandler and it's factory. The
factory itself is configured in the facex-config.xml because there isn't
any annotation for it.
Open the faces-config.xml and add the following lines at the bottom:
<factory> <exception-handler-factory>info.galleria.handlers.GalleriaExceptionHandlerFactory</exception-handler-factory> </factory>
Now we are going to implement the GalleriaExceptionHandlerFactory in the dedicated package. The interesting method here is the:
@Override public ExceptionHandler getExceptionHandler() { ExceptionHandler result = parent.getExceptionHandler(); result = new GalleriaExceptionHandler(result); return result; }
This is called once per request must return a new ExceptionHandler instance each time it's called. Here the the real ExceptionHandlerFactory is called and asked to create the instance, which is then wrapped in the custom GalleriaExceptionHandler class. This is where the real interesting stuff happens.
@Override public void handle() throws FacesException { for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) { ExceptionQueuedEvent event = i.next(); ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource(); Throwable t = context.getException(); if (t instanceof ViewExpiredException) { ViewExpiredException vee = (ViewExpiredException) t; FacesContext fc = FacesContext.getCurrentInstance(); Map<String, Object> requestMap = fc.getExternalContext().getRequestMap(); NavigationHandler nav = fc.getApplication().getNavigationHandler(); try { // Push some stuff to the request scope for later use in the page requestMap.put("currentViewId", vee.getViewId()); nav.handleNavigation(fc, null, "viewExpired"); fc.renderResponse(); } finally { i.remove(); } } } // Let the parent handle all the remaining queued exception events. getWrapped().handle(); }
Iterate over the unhandler exceptions using the iterator returned from getUnhandledExceptionQueuedEvents().iterator(). The ExeceptionQueuedEvent is a SystemEvent from which you can get the actual ViewExpiredException. Finally you extract some extra information from the exception and place it in request scope to access it via EL in the page later on. Last thing to do here for a ViewExpiredException is to use the JSF implicit navigation system ("viewExpired" is resolved to "viewExpired.xhtml") and navigate to the "viewExpired" page via the NavigationHandler. Don't forget to remove the handled exception in the finally block. You don't want this to be handled again by the parent exception handler. Now we have to create the viewExpired.xhtml page. Do this inside the galleria-jsf\src\main\webapp folder.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <ui:composition xmlns:ui="http://java.sun.com/jsf/facelets" template="./templates/defaultLayout.xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" > <ui:define name="title"> <h:outputText value="#{msg['Exception.page.title']}" /> </ui:define> <ui:define name="content"> <h:form> <h:outputText value="#{msg['Exception.page.message']}" /> <p>You were on page #{currentViewId}. Maybe that's useful.</p> <p>Please re-login via the <h:outputLink styleClass="homepagelink" value="#{request.contextPath}/Index.xhtml" ><h:outputText value="Homepage" /></h:outputLink>.</p> </h:form> </ui:define> </ui:composition>
Please note that I added new message properties here, so you need to
make sure to place them in
galleria-jsf\src\main\resources\resources\messages.properties and
translations.
Until now this obviously only handles one special instance of exception.
You could extend it to handle others as well. Now that we have the
basic mechanism in place you are free to do this.
Refactoring the RuntimeException handling
As I said, I am not happy with the way the application is handling
RuntimeExceptions. Now that we have a nice central exception handling in
place we can move those stuff around a bit and refactor the *Manager
classes. Delete all those catch (EJBException ejbEx) { blocks from
all of them. We are going to take care of them in the
GalleriaExceptionHandler in a minute. Simply add an another check to the
GalleriaExceptionHandler and redirect the user to another page if any
other exception than a ViewExpiredException is thrown.
// check for known Exceptions if (t instanceof ViewExpiredException) { ViewExpiredException vee = (ViewExpiredException) t; // Push some stuff to the request scope for later use in the page requestMap.put("currentViewId", vee.getViewId()); } else { forwardView = "generalError"; Locale locale = fc.getViewRoot().getLocale(); String key = "Excepetion.GeneralError"; logger.error(Messages.getLoggerString(key), t); String message = Messages.getString(key, locale); FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null); fc.addMessage(null, facesMessage); }
This approach has some advantages. It reduces the needed code in the *Manager classes and we finally have a central place to take care of those unrecoverable exceptions. This still is not very enterprise like. Imagine your first level support team needs to look after customers and they start complaining that the only message they get is a "GeneralError". That is not very helpful. You support team would need to escalate it and second or third level would need to check the logs and and and .. All this because of an error, that we could have know. First thing to do is to find out about the causing error. Parsing stack traces isn't big fun. Especially not of RuntimeExceptions that are wrapped in EJBExceptions and further on in FacesExceptions. Thank god for the Apache Commons ExceptionUtils. Open your galleria-jsf pom.xml and add them as dependency:
<dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency>
Now you can start to examine the root cause:
} else { forwardView = "generalError"; // no known instance try to specify Throwable causingEx = ExceptionUtils.getRootCause(t); if (causingEx == null) { causingEx = t; }
//... logger.error(Messages.getLoggerString(key), t); requestMap.put("errorCode", errorCode);
Don't forget to also log the complete stack-trace (t and not only causingEx) here. In general it's a bad thing to let users know about exceptions. Nobody really wants to see errors happen (because we do hate making mistakes) and after all exception stack-traces could disclose sensitive information which you wouldn't like to see on a screen somewhere. So you need to find a way to display something meaningful to the user without disclosing too much. That is where the famous error-codes come into play. Use the root-cause exception as message key or make your own decisions on what effort you are going to put in here. It might be a system of categories of errors (db, interface systems, etc.) which give the first-level support a good hint about what was causing the error. I would stick to a simpler solution from the beginning. Simply generate a UUID for every caught exception and trace it to both the log and the UI. A very simple one could be the following.
String errorCode = String.valueOf(Math.abs(new Date().hashCode()));
This should also be added to the message properties and don't forget that you need another one for the generalError template. If slf4j would use the same message format than jdk logging does you would only need one property .. anyway:
Exception.generalError.log=General Error logged: {}. Exception.generalError.message=A general error with id {0} occured. Please call our hotline.
Add this to the generalError.xhtml and see how the error code is passed to the message template.
<h:outputFormat value="#{msg['Exception.generalError.message']}" > <f:param value="#{errorCode}"/> </h:outputFormat>
There is still a lot to improve on here. You could use
the javax.faces.application.ProjectStage to lookup the current mode the
application is running in. If you are running in
ProjectStage.Development you could also put the complete stack-trace to
the UI and make debugging live a bit easier. The following snippet is
trying to get the ProjectStage from JNDI.
public static boolean isProduction() { ProjectStage stage = ProjectStage.Development; String stageValue = null; try { InitialContext ctx = new InitialContext(); stageValue = (String) ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME); stage = ProjectStage.valueOf(stageValue); } catch (NamingException | IllegalArgumentException | NullPointerException e) { logger.error("Could not lookup JNDI object with name 'javax.faces.PROJECT_STAGE'. Using default 'production'"); } return ProjectStage.Production == stage; }
What about the 3-digit Http Error Pages?
That is another thing to take care of. All those remaining 3-digit http
error codes which return one of those not nice looking error pages. The
only thing to do this is to map them in the web.xml like shown in the
following:
<error-page> <error-code>404</error-code> <location>/404.xhtml</location> </error-page>
You should make sure to have those mappings in place and present a meaningful error to your users. It should become best practice to always offer a way to navigate further from there.
Published at DZone with permission of Markus Eisele, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments