46
votes

I find this article to be useful for non-ajax request How to handle session expiration and ViewExpiredException in JSF 2? but I can't make use of this when I am submitting using an AJAX call.

Suppose in a primefaces dialog, I am making a post request using AJAX and session has already timed out. I see my page getting stuck.

How to fix this kind of scenario such that when I post using AJAX, I could redirect him to my view expired page and then forward him to the login page similar to the solution in the link above?

JSF2/Primefaces/Glassfish

6

6 Answers

51
votes

Exceptions which are thrown during ajax requests have by default totally no feedback in the client side. Only when you run Mojarra with project stage set to Development and use <f:ajax>, then you will get a bare JavaScript alert with the exception type and message. But other than that, and in PrimeFaces, there's by default no feedback at all. You can however see the exception in the server log and in the ajax response (in the webbrowser's developer toolset's "Network" section).

You need to implement a custom ExceptionHandler which does basically the following job when there's a ViewExpiredException in the queue:

String errorPageLocation = "/WEB-INF/errorpages/expired.xhtml";
context.setViewRoot(context.getApplication().getViewHandler().createView(context, errorPageLocation));
context.getPartialViewContext().setRenderAll(true);
context.renderResponse();

Alternatively, you could use the JSF utility library OmniFaces. It has a FullAjaxExceptionHandler for exactly this purpose (source code here, showcase demo here).

See also:

18
votes

A merge between the answer of @BalusC and this post, I solved my problem!

My ExceptionHandlerWrapper:

public class CustomExceptionHandler extends ExceptionHandlerWrapper {

    private ExceptionHandler wrapped;

    CustomExceptionHandler(ExceptionHandler exception) {
        this.wrapped = exception;
    }

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

    @Override
    public void handle() throws FacesException {
        final Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator();
        while (i.hasNext()) {
            ExceptionQueuedEvent event = i.next();
            ExceptionQueuedEventContext context
                    = (ExceptionQueuedEventContext) event.getSource();

            // get the exception from context
            Throwable t = context.getException();

            final FacesContext fc = FacesContext.getCurrentInstance();
            final Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
            final NavigationHandler nav = fc.getApplication().getNavigationHandler();

            //here you do what ever you want with exception
            try {

                //log error ?
                //log.log(Level.SEVERE, "Critical Exception!", t);
                if (t instanceof ViewExpiredException) {
                    requestMap.put("javax.servlet.error.message", "Session expired, try again!");
                    String errorPageLocation = "/erro.xhtml";
                    fc.setViewRoot(fc.getApplication().getViewHandler().createView(fc, errorPageLocation));
                    fc.getPartialViewContext().setRenderAll(true);
                    fc.renderResponse();
                } else {
                    //redirect error page
                    requestMap.put("javax.servlet.error.message", t.getMessage());
                    nav.handleNavigation(fc, null, "/erro.xhtml");
                }

                fc.renderResponse();
                // remove the comment below if you want to report the error in a jsf error message
                //JsfUtil.addErrorMessage(t.getMessage());
            } finally {
                //remove it from queue
                i.remove();
            }
        }
        //parent hanle
        getWrapped().handle();
    }
}

My ExceptionHandlerFactory:

public class CustomExceptionHandlerFactory extends ExceptionHandlerFactory {

    private ExceptionHandlerFactory parent;

    // this injection handles jsf
    public CustomExceptionHandlerFactory(ExceptionHandlerFactory parent) {
        this.parent = parent;
    }

    @Override
    public ExceptionHandler getExceptionHandler() {
        ExceptionHandler handler = new CustomExceptionHandler(parent.getExceptionHandler());
        return handler;
    }

}

My faces-config.xml

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
              xmlns="http://xmlns.jcp.org/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    <factory>
        <exception-handler-factory>
            your.package.here.CustomExceptionHandlerFactory
        </exception-handler-factory>
    </factory>
</faces-config>
3
votes

I am using Mojarra 2.1.7 in Production mode with JBoss 7. After the session expires, AJAX calls return an error XML document. You can easily catch this error using the usual onerror handler of f:ajax.

<script type="text/javascript">
    function showError(data) {
        alert("An error happened");
        console.log(data);
    }
</script>

<h:commandLink action="...">
    <f:ajax execute="..." render="..." onerror="showError"/>
</h:commandLink>
1
votes

I have included this in my ViewExpiredExceptionHandler class and it worked fine for me in WAS

    public void handle() throws FacesException {
    FacesContext facesContext = FacesContext.getCurrentInstance();
                 for (Iterator<ExceptionQueuedEvent> iter = getUnhandledExceptionQueuedEvents()
            .iterator(); iter.hasNext();) {
        Throwable exception = iter.next().getContext().getException();

        if (exception instanceof ViewExpiredException) {


            final ExternalContext externalContext = facesContext
                    .getExternalContext();

            try {


                facesContext.setViewRoot(facesContext.getApplication()
                        .getViewHandler()
                        .createView(facesContext, "/Login.xhtml"));     //Login.xhtml is the page to to be viewed. Better not to give /WEB-INF/Login.xhtml
                externalContext.redirect("ibm_security_logout?logoutExitPage=/Login.xhtml");    //  when browser back button is pressed after session timeout, I used this.         
                facesContext.getPartialViewContext().setRenderAll(true);
                facesContext.renderResponse();

            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                iter.remove();
            }
        }

    }

    getWrapped().handle();
}

Hope this helps

1
votes

I faced this problem, Requirement need to display a confirmation popup when user do any action after session gets timed out, my proposed solution was:

<security:http use-expressions="true" auto-config="true" entry-point-ref="authenticationEntryPoint">
            <security:intercept-url pattern="/common/auth/**" access="permitAll" />
            <security:intercept-url pattern="/javax.faces.resource/**" access="permitAll" />
            <security:intercept-url pattern="/**/   *.*" access="hasRole('ROLE_ADMIN')" />
            <security:form-login login-page="/common/auth/login.jsf" />
            <!-- <security:remember-me key="secret" services-ref="rememberMeServices" /> -->
            <security:logout invalidate-session="true" logout-success-url="/common/auth/login.jsf" />
        </security:http>
        <bean id="authenticationEntryPoint" class="com.x.y.MyRedirectEntryPoint" >
           <property name="loginFormUrl" value="/common/auth/login.jsf"/>
        </bean>

The MyRedirectEntryPoint should extends AuthenticationProcessingFilterEntryPoint and override commence method

public void commence(HttpServletRequest request, HttpServletResponse response,   AuthenticationException authException)
        throws IOException, ServletException {
    boolean ajaxRedirect = request.getHeader("faces-request") != null
            && request.getHeader("faces-request").toLowerCase().indexOf("ajax") > -1;
    if (ajaxRedirect) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            response.sendError(403);

        }
    } else {

        super.commence(request, response, authException);
    }
}

Now you can simply bind a callback javascript function to catch the thrown 403 error and do what ever you want:

$(document).bind('ajaxError',
                    function(event, request, settings, exception){
                          if (request.status==403){
                             //do whatever you wanted may be show a popup or just redirect
                             window.location = '#{request.contextPath}/';
                             }
                             });
1
votes

For me a simple client side javascript handler worked:

function handleAjaxExpired(xhr,status,args) {
    // handler for "oncomplete" ajax callback
    if ( xhr.responseXML.getElementsByTagName('error-name').length ) {
        // "<error-name>" tag is present -> check for "view expired" exception
        html = xhr.responseXML.getElementsByTagName('error-name')[0].innerHTML;

        if ( html.indexOf('ViewExpiredException') > -1 ) {
            // view expired exception thrown
            // do something / reload page
            if ( confirm('session expired -> reload page?') ) {
                document.location.href=document.location.href;
            }
        }
    }
}

This handler is called from "oncomplete" attribute in triggering UI element, e.g. here from a rowSelect event in a Primefaces datatable:

<p:ajax event="rowSelect" oncomplete="handleAjaxExpired(xhr,status,args)" />

Update: To avoid adding "oncomplete" attributes to every ajax-enabled element, this javascript code searches globally in all ajax responses for errors:

(function() {
    // intercept all ajax requests
    var origXHROpen = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function() {

        this.addEventListener('load', function() {
            handleAjaxExpired(this);
        });
        origXHROpen.apply(this, arguments);
    };
})();

This code makes "oncomplete" attributes in PrimeFaces UI-elements obsolete.