How to let your user know his session has expired in an ADF10g application

In a proper and user-friendly application, it is a good idea to let your user know when his session has expired. Or to inform him that his session is about to be expired. How can you accomplish that in an ADF10g? In this article I will first show you how you can notify a user his session has expired using a component I recently discovered in ADF Faces, an af:poll. This is a really cool component that can trigger a so called pollEvent after a specified interval, and is delivered at the server upon polling. Then I will show you another example where this component can be usefull: to maintain a user session while the application is still open in the browser.

 

 

 

Normally, in a Java web-application a session timeout is configured (in minutes) in the deployment descriptor (DD) web.xml. This means that if the client doesn’t make any requests on this session for 35 minutes the application server will invalidate the session.

 

<web-app …>
<servelet>

</servlet>
<session-config>
<session-timeout>35</session-timeout>
</session-config>
</web-app>

 

We use a servlet filter to check the session and attributes on the session. When the session is no longer valid, the servlet container redirects the request to our configured Login page.

 

Javascript solution in the browser

A very simple but for many ADF applications not yet complete solution could be that you add a single <afh:script> component to a region-JSPX that is included in every JSPX, for example a menu region. This <afh:script> component calls a JavaScript function (which calls another function):

 

<afh:script id="popupSessionTimeout" text="timerpopupSessionTimeout('Your session has expired. Do you want to go to the Login page?', '2100000');" />

 

With your message from your resourcebundle and the maximum inactive interval time red from a backing bean:

 

<afh:script id="popupSessionTimeout"  text="timerpopupSessionTimeout('#{nls['SESSION_TIMEOUT_MESSAGE']}', '#{SessionTimeout.maxInactiveIntervalInMilliSeconds}');" />

 

In JavaScript we can call a standard JavaScript function, setTimeout(), which calls the function specified in the first parameter, after an interval (in milliseconds) specified in the second parameter. In this case, setTimeout() calls our function popupSessionTimeout() after the number of milliseconds specified in the parameter maxInactiveIntervalInMilliSeconds (2.100.000 milliseconds is 35 minutes) . This is red by an EL expression from a backing bean who calls getMaxInactiveInterval() on the HttpSession object, which reflects the session-timeout time specified in the DD.

 

var timeoutMessagePopup=null;
function timerpopupSessionTimeout(timeoutMessage,maxInactiveIntervalInMilliSeconds)
{
timeoutMessagePopup=timeoutMessage;
setTimeout("popupSessionTimeout()", maxInactiveIntervalInMilliSeconds);
}
function popupSessionTimeout()
{
var answer = confirm(timeoutMessagePopup);
if (answer){
window.location = "/myApplication/start";
}
}

 

Now, every time a JSPX is rendered, the JavaScript function timerpopupSessionTimeout() is called, and a JavaScript confirm-popup is showed to the user after the session-timeout.

Problem with JavaScript-only solution

This approach has one disadvantage: because of dialogs, Lists Of Values (LOV’s),  and Partial Page Rendering (PPR) the session timeout time in the browser is not always reflecting the current inactive interval of the user in the application server.
In our current application we have a lot of dialogs, LOV’s and PPR in the application. The ADF Faces Dialog and Process framework provides an easy way to jump to a page, or a series of pages, to get information from the user and then return to the original page to use that information. In ADF Faces this is called “processes”. One special type of process supported by ADF Faces is a “dialog:” it’s shown in a separate popup window instead of in the same window.

 

How to let your user know his session has expired in an ADF10g application NIEUW POPUP Customize mode(1)

 

 

For example, when our ‘customize mode’ link is clicked, the application shows a dialog with customizable prompts. When the user chooses and saves these prompts and closes the popup window, the server receives a request to save data and to close the window. But it will also reset the session timeout time, starting again from the beginning with the time in minutes specified in the DD, for example 35 minutes. At the same time in your browser, your afh:script component in the JSPX will not refresh and will not start over from the beginning but continues to count down and will be maybe already around 34 minutes. Now our popup message might come too early – we don’t want smart users to click the message away and still continue with the application. We need to make sure the session is already invalidated when our message is displayed to the user. For the same reason Partial Page Rendering is a problem for this solution. PPR is a technology provided by ADF Faces that allows a portion of a page to be redrawn rather than reloading the entire page. If there are many components redrawn by PPR in a JSPX and our afh:script is not redrawn, the browser session timeout time won’t be reset. We need a better and saver solution to make sure the session is invalidated. We can do that making use of the af:poll component.

 

Solution using an af:poll component

 

The main idea is this: An <af:poll> component can trigger an event (a so called PollEvent) after the time that is specified by the interval attribute. We will trigger a request just before (one minute) the session is about to be expired on the server. On the ‘pollListener’ attribute we can specify with EL a listener in a backing bean to this pollEvent. In this listener we can set the rendered property an afh:script component to true. This afh:script that now will be rendered will call a slightly different version of the JavaScript function ‘timerpopupSessionTimeout()’ from above and will display our message to the user. At the same time, the user session is invalidated in this backing bean.

 

<!--start: destroy a session after session timeout and show a popup window informing the user. -->
<af:panelGroup id="pollPanelGroup" partialTriggers="timer">
<af:poll interval="#{PollListener.sessionTimeoutLaunchPopup}" id="timer" pollListener="#{PollListener.pollaction}"/>
<afh:script id="pollTriggeredPopupSessionTimeout" rendered="#{PollListener.sessionTimeoutPopupRendered}"
text="timerpopupSessionTimeout('#{nls['SESSION_TIMEOUT_MESSAGE']}');"/>
</af:panelGroup>
<!--end: destroy a session after session timeout and show a popup window informing the user. -->

 

This code could be added to each JSPX page. But most applications have a general region-JSPX that is part of every JSPX where they offer some general functionality (a region with menu- or help links). Then it can be added only to this page.
After the interval, in our case 34 minutes, a pollEvent is triggered and the listener method is called in our backing bean called PollListener.java. We make this backing bean available by registering it in faces-config.xml:

 

<managed-bean>
<managed-bean-name>pollListener</managed-bean-name>
<managed-bean-class>HR.session.PollListener</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>

 

If we want the af:poll component to trigger an event before the session is invalidated on the server, we must be sure to take enough time. There is a start-time difference between the time the container receives the request (=HttpSession. getLastAccessedTime(), from this point in time the server starts counting), and the time the poll-interval from the JSPX starts in the client’s browser. When, for example, a JSPX request with an af:poll component takes 30 seconds to respond and to display in the browser after the webcontainer (server) receives the user-request, the poll interval start-time in the browser begins 30 seconds LATER than the container starts the session inactive-interval. To be safe, we use 60 seconds because we have several JSPX pages that can take up to a 30 seconds before the browser receives a response and the browser interval starts. When a response takes even longer than 60 seconds, the pollEvent will be triggered after the session is invalidated by the container, and that is of course a situation we don’t want.

First, in the constructor, we will set the HttpSession object and the attribute ‘sessionTimeoutLaunchPopup’. This attribute has a value binding with the ‘interval’ attribute of the af:poll component. Its value is the maximum inactive interval in seconds that is specified in the DD (returned by session.getMaxInactiveInterval() ) multiplied by 1000 to get milliseconds and then subtract the result with 60.000 milliseconds (1 minute):

 

public PollListener() {
FacesContext ctx = FacesContext.getCurrentInstance();
HttpServletRequest request = (HttpServletRequest)ctx.getExternalContext().getRequest();
session = request.getSession(false);
int maxInactiveIntervalInSeconds = session.getMaxInactiveInterval();
sessionTimeoutLaunchPopup = ((maxInactiveIntervalInSeconds * 1000) - 60000);
      }

 

When the interval time of our af:poll component is passed, a pollEvent is triggered and our listener method is called:

 

public void pollaction(PollEvent pe) {
sLog.debug("pollaction()...");
sessionTimeoutPopupRendered=true;
        Thread thread = new Thread(new SessionInvalidator());
thread.start();

 

As you can see, the ‘sessionTimeoutPopupRendered’ attribute in the listener is set to true, now the ‘rendered’ property of the afh:script component will evaluate to true and will be rendered in response to the pollEvent:

 

<afh:script id="pollTriggeredPopupSessionTimeout" rendered="#{PollListener.sessionTimeoutPopupRendered}" text="timerpopupSessionTimeout('#{nls['SESSION_TIMEOUT_MESSAGE']}');" /> 

 

Note that we have set the ‘partialTriggers’ attribute on the parent component af:panelGroup:

 

<af:panelGroup id="pollPanelGroup" partialTriggers="timer" >

 

Now the timerpopupSessionTimeout(‘#{nls[‘SESSION_TIMEOUT_MESSAGE’]}’) JavaScript function is called which renders our message (Complete JavaScript code is coming below).

 

We have still one problem: we have to make sure our session object is invalidated. If we do it straight away in the listener, our response won’t be rendered because it needs a valid session object. Without a valid session object the essential FacesContext object won’t be available anymore to render the response. The application will immediately go back to the Login page without showing the popup message. The response must be completed before the session can be made invalid. This pollEvent is a small PPR request, so the response won’t take that long. We can make the session invalid in a separate thread and let it sleep for 5 seconds before the session is invalidated. This to first allow the PPR-response to be rendered and to show our JavaScript popup message:

 

Thread thread = new Thread(new SessionInvalidator());
thread.start();

 

And with an inner class that implements Runnable the session can be made invalid:

 

class SessionInvalidator implements Runnable {
public void run() {
try {
sLog.debug("in run()... sleep 5 seconds before session is destroyed, to first allow a PPR-response to render a session-has-expired JavaScript popup message…");
Thread.sleep(5000);
            } catch (InterruptedException e) {
sLog.debug("exc: "+e);
}
if(session!=null){
session.invalidate();
                sLog.debug("in run()...session has been invalidated");
}
}
}

 

 

Our complete PollListener.java:

 

package HR.session;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import oracle.adf.view.faces.event.PollEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PollListener {
private boolean sessionTimeoutPopupRendered = false;
private int sessionTimeoutLaunchPopup;
private HttpSession session;
private static final Log sLog = LogFactory.getLog(PollListener.class);

public PollListener() {
FacesContext ctx = FacesContext.getCurrentInstance();
HttpServletRequest request = (HttpServletRequest)ctx.getExternalContext().getRequest();
session = request.getSession(false);
int maxInactiveIntervalInSeconds = session.getMaxInactiveInterval();
sessionTimeoutLaunchPopup = ((maxInactiveIntervalInSeconds * 1000) – 60000);
}

public void pollaction(PollEvent pe) {
sLog.debug(“pollaction()…”);
sessionTimeoutPopupRendered=true;
Thread thread = new Thread(new SessionInvalidator());
thread.start();
}

    public boolean isSessionTimeoutPopupRendered() {
return sessionTimeoutPopupRendered;
}

    public int getSessionTimeoutLaunchPopup() {
return sessionTimeoutLaunchPopup;
}

    class SessionInvalidator implements Runnable {
public void run() {
try {
sLog.debug("in run()...sleep 5 seconds before session is destroyed, to first allow a PPR-response to render a session-has-expired JavaScript popup message...");
Thread.sleep(5000);
} catch (InterruptedException e) {
sLog.debug("exc: "+e);
}
if(session!=null){
session.invalidate();
sLog.debug("in run()...session has been invalidated");
}
}
}
}

 

Because we make the session invalid at the server, in a separate thread 5 seconds after the listener method is called, we must show our message in the browser 5 seconds later to make certain the session is already invalidated before the message is showed. We can do that by calling the standard JavaScript function setTimeout() and by specifying 5000 milliseconds in the second parameter (the same time the separate thread in Java sleeps):

 

setTimeout("popupSessionTimeout()", 5000);

 

Our final JavaScript file:

 

var timeoutMessagePopup=null;
function timerpopupSessionTimeout(timeoutMessage){
timeoutMessagePopup=timeoutMessage;
setTimeout("popupSessionTimeout()", 5000);
}
function popupSessionTimeout(){
var answer = confirm(timeoutMessagePopup);
if (answer){
window.location = "/myapplication/start";
}
}

 

Now we are sure the session is already invalidated when the user sees our message. All the user has to do to return to the Login page is pressing the enter key. When ‘Cancel’ is choosen, the application won’t return to the login page but the session will – of course – still be invalid and the user won’t be able to do anything before logging in again.

 

How to let your user know his session has expired in an ADF10g application ART message and loginpage(1)

 

Maintaining a user session while the application is open in the browser

 

Maybe you don’t want users to be thrown out of the application server, and maintain all user sessions while their application is open in the browser. One option is to set the session-timeout in the DD to the value of -1, and sessions won’t timeout on the application server. But this option has a big disadvantage: if you have thousands and thousands of users and when they do close the application using the browser X close-button on the right upper side, the user session will unnecessary stay alive in the application server, and still use valuable memory space. Many users do not click on a logout link which should invalidate their session in the application server using HttpSession.invalidate().

 

A second and in my opinion better option is to maintain a user session only when the application is open in the browser. We can also use an af:poll component for this:

 

<af:poll interval="2040000"/>

 

As long as the application is open in the browser, you can trigger a request just before the session is about to be expired on the server. You simply specify an interval time a little bit smaller (for example 1 minute) than your maximum inactive interval time. In our case 2.040.000 milliseconds could be specified on the af:poll component (34 minutes, our session timeout time minus 1). Now this af:poll will take care of triggering an event on the client and deliver it at the server, and the session timeout time will start from the beginning again and the session won’t be invalidated. When a user closes his browser using the browser X close button, no more poll events will be triggered AND the application server will invalidate the session after 35 minutes and save valuable memory space.