Tuesday, October 19, 2010

Global handling of all unchecked / unexpected exceptions in JSF 2

JSF 2 specification introduced a new ExceptionHandler API. All exceptions can be handled globally by an ExceptionHandler instance. Advantage of this centralization is that it allows developers to devise more error handling strategies. I was inspired by the great article of Ed Burns "Dealing Gracefully with ViewExpiredException in JSF2" and developed my own central Exception Handler.
Firstly, we need a factory class that creates and returns a new Exception Handler instance.
public class DefaultExceptionHandlerFactory extends ExceptionHandlerFactory {
    private ExceptionHandlerFactory parent;

    public DefaultExceptionHandlerFactory(ExceptionHandlerFactory parent) {
        this.parent = parent;
    }

    @Override
    public ExceptionHandler getExceptionHandler() {
        ExceptionHandler eh = parent.getExceptionHandler();
        eh = new DefaultExceptionHandler(eh);

        return eh;
    }
}
This factory class has to be registered in faces-config.xml
<factory>
    <exception-handler-factory>
        ip.client.jsftoolkit.commons.DefaultExceptionHandlerFactory
    </exception-handler-factory>
</factory>
Secondly, we need a class DefaultExceptionHandler. This is the default implementation of the exception handler to catch unchecked / unexpected exceptions in order to proper display.
public class DefaultExceptionHandler extends ExceptionHandlerWrapper {
    private static final Log LOG = LogFactory.getLog(DefaultExceptionHandler.class);

    /** key for session scoped message detail */
    public static final String MESSAGE_DETAIL_KEY = "ip.client.jsftoolkit.messageDetail";

    private ExceptionHandler wrapped;

    public DefaultExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ExceptionHandler getWrapped() {  
        return this.wrapped;
    }

    @Override
    public void handle() throws FacesException {
        if (fc.isProjectStage(ProjectStage.Development)) {
            // Code for development mode. E.g. let the parent handle exceptions
            getWrapped().handle();
        } else {
            for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {
                ExceptionQueuedEvent event = i.next();
                ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();

                String redirectPage = null;
                FacesContext fc = FacesContext.getCurrentInstance();
                Throwable t = context.getException();

                try {
                    if (t instanceof AbortProcessingException) {
                        // about AbortProcessingException see JSF 2 spec.
                        LOG.error("An unexpected exception has occurred by event listener(s)", t);
                        redirectPage = "/views/error.jsf?statusCode=jsftoolkit.exception.UncheckedException";
                        fc.getExternalContext().getSessionMap()
                            .put(DefaultExceptionHandler.MESSAGE_DETAIL_KEY, t.getLocalizedMessage());
                    } else if (t instanceof ViewExpiredException) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("View '" + ((ViewExpiredException) t).getViewId() + "' is expired", t);
                        }

                        ApplicationConfiguration appConfiguration =
                            (ApplicationConfiguration) FacesAccessor.accessManagedBean(
                                ApplicationConfiguration.BEAN_NAME_APPLICATION_CONFIGURATION);
                        HttpSession session = (HttpSession) fc.getExternalContext().getSession(false);
                        if (session != null) {
                            // should not happen
                            session.invalidate();
                        }

                        if (appConfiguration.getBoolean(ConfigKeys.KEY_LOGOFF_2_LOGOUT_PAGE, false)) {
                            // redirect to the specified logout page
                            redirectPage = "/views/logout.jsf";
                        } else {
                            // redirect to the login page
                            redirectPage = "";
                        }
                    } else if (t instanceof ServiceNotAvailableException) {
                        LOG.error("'" + ((ServiceNotAvailableException) t).getServiceName() + "' is not available", t);
                            redirectPage = "/views/error.jsf?statusCode=jsftoolkit.exception.ServiceNotAvailableException";
                    } else {
                        // custom handling of unexpected exceptions can be done in the method handleUnexpected
                        String messageKey = handleUnexpected(fc, t);
                        redirectPage = "/views/error.jsf?statusCode=" + messageKey;
                        fc.getExternalContext().getSessionMap()
                            .put(DefaultExceptionHandler.MESSAGE_DETAIL_KEY, t.getLocalizedMessage());
                    }
                } finally {
                    i.remove();
                }

                SecurityPhaseListener spl = new SecurityPhaseListener();
                spl.doRedirect(fc, redirectPage);

                break;
        }
    }

    protected String handleUnexpected(FacesContext facesContext, final Throwable t) {
        LOG.error("An unexpected internal error has occurred", t);

        return "jsftoolkit.exception.UncheckedException";
    }
}
The method handleUnexpected(FacesContext, Throwable) can be overwritten in derived classes in order to customization. Handling of various unexpected exceptions can be done there. An example of using:
protected String handleUnexpected(FacesContext facesContext, final Throwable t) {
    if (t instanceof IllegalStateException) {
        // some special handling here
        ...
        return "key.exception.IllegalStateException";
    } else {
        return super.handleUnexpected(facesContext, t);
    }
}
SecurityPhaseListener is the phase listener from my last post allowing JSF redirects.

18 comments:

  1. Nice post, very helpful.
    Do you have an example application where you had implemented this exception handling???
    I have some problems with libraries.
    this is my email: alfredo.michalfred@gmail.com
    thanks.

    ReplyDelete
  2. Hello Michelito,

    Yes, I have, but it's a big application. What a problem do you have with libraries? Are you mssing any libraries in classpath?

    ReplyDelete
  3. import javax.faces.context.ExceptionHandler;
    import javax.faces.context.ExceptionHandlerFactory;

    These are the libraries that I'm using in the first class. But I have an error in the line 11 in the "DefaultExceptionHandler" it says : DefaultExceptionHandler cannot be resolved to a type
    Do you know what could be the problem?
    Thanks for your help.

    ReplyDelete
  4. You should import DefaultExceptionHandler.java in DefaultExceptionHandlerFactory.java or simple place these two classes in the same package.

    ReplyDelete
  5. Hello Oleg, how are you doing?

    I have some compiling errors in my code.
    I have imported some libraries and added jars to solve the issues but I have 3 classes or objects that I don't know how to solve it. I'll appreciate if you can help me to know what libraries or jars I need to include.
    First one is at line 44 ApplicationConfiguration
    Second one at line 45 FacesAccessor
    3. at line 54 ConfigKeys
    4. at line 60 ServiceNotAvailableException
    the last is SecurityPhaseListener

    Maybe you can explain the libraries and imports necessaries to run this code. Thanks and regards.

    ReplyDelete
  6. Hello Anonymous,

    Those are my own classes. They don't matter for this example. I just wanted to show an idea of global exception handling.

    ReplyDelete
  7. First off Oleg, great artticle! This has given me hope for a work-a-round an issue I'm having with upgrading to JSF 2.0. I was wrapping the Lifecycle by adding my custom lifecycle to catch exceptions and email the interested people the error, then forward the user to a "pretty" error handling page. This approach worked great in JSF 1.1, but I'm upgrading to 2.0 and it still works fine in the execute() method of my lifecycle, but the render() dosn't work due to the response being commited when I catch excptions.

    That said, this is why your demo is so apealing to me. It seems like this should be a more gracefull way to handle the excptions and perform the same logic as described. I do need a little guidance if you don't mind. As described above, I'm not able to perform a HttpServletResponse.redirect("path) in the render() method due to being in PhaseId.RENDER_RESPONSE phase. In that senerio also I tried the method you have in your code of using the NaviationalHandler.handleNavigation(), howerver; the program seemed to ignore this code, and simply forward me to the broken page with a seemingly ulsess and ugly faclets error. After finding/trying your method of wrapping JSF's ExceptionHandler, unfortunatly, I'm getting the same results using the NavigationHandler.handleNavigation(). The same is true regarding trying to use HttpServletResponse.redirect() due to the response being already commiteed.

    Is there something I'm missing? It seems that regarless of redirect or forward (via NavigationHandler) I am not able to get my client to the error page...Also, a side note, I am trying to catch ALL unhandled exceptions not just specific ones. If there is an error I need to send the admins an email, and then rerout client to error page.

    Hopfully my rambling is clear, I apoligize for the length of my post/question, I'm just trying to provide clear details to my senerio and what I'm trying to accomplish. Thank you a ton in advance for any help with this!

    ReplyDelete
  8. Hi Mick,

    Yes, I think the response can be committed in / after the last lyfecycle phase (rare case). Nothing to do. I check that simple in the doRedirect(...) method. See here http://ovaraksin.blogspot.com/2010/10/jsf-ajax-redirect-after-session-timeout.html

    ReplyDelete
  9. Nice article. Thanks for sharing.

    ReplyDelete
  10. Great artice...will this handle 404s?

    ReplyDelete
  11. No, for 404 and similar codes you have to define error pages in web.xml.

    <!-- Error pages -->
    <error-page>
    <error-code>403</error-code>
    <location>/views/error.jsf?statusCode=403</location>
    </error-page>
    <error-page>
    <error-code>404</error-code>
    <location>/views/error.jsf?statusCode=404</location>
    </error-page>
    <error-page>
    <error-code>500</error-code>
    <location>/views/error.jsf?statusCode=500</location>
    </error-page>

    But I let go such errors through the same error page as in ExceptionHandler (I had preRenderView event there and I can prepare message text in listener).

    ReplyDelete
  12. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Thanks for your post, but how to include dynamic header footer in error page. It did not understand that in my app?

      Delete
  13. what is pplicationConfiguration ? I am getting compile time error.

    ReplyDelete
  14. This is my own class (used apache configuration), it doesn't matter, just an example.

    ReplyDelete
  15. blog grows http://casinogamesonlinee@blogspot.com

    ReplyDelete
  16. HI!
    I emulate and SQL Error but not works

    ReplyDelete
  17. Is there also a way to inject CDI beans?

    ReplyDelete

Note: Only a member of this blog may post a comment.