I recently had a requirement to record when users log-in and out of my ADF application, as well as time-out of the application after an inactive period. As part of the requirement the user's name, login datetime, and logout/timeout datetime needed to be written to the database.
After much head scratching, staring at ADF BC help documentation to see if there was a solution, a case of Java brain freeze, I finally posted on the JDev OTN forum for
assistance. Luckily John and Pako came to my rescue (thanks gents!) and gave me the inspiration for a solution. I thought I'd share my solution here for all to use.
My problem in looking for a solution was I was focusing on ADF BC and ADF Faces/JSF too closely to find a solution and ignoring the rest of the J2EE stack. Let's explain:
While ADF BC is ideal for working with the database, it is not ideal for capturing the start and end of user sessions. (This isn't such a surprise if you consider ADF BC works as an ORM, not a session management tool) The ADF BC application modules are only instantiated when you have a web page that reference's the AM and the VOs of the ADF BC project. If the user first navigates to a web page in your application such as a splash screen that shows no database derived information, the AM user session isn't started, missing the opportunity to record when the user session's started.
What about JavaServer Faces? Do JSF session level beans provide a solution? Unfortunately the same issue for ADF BC above applies to JSF session level beans. The JSF beans are only instantiated when an EL expression references them. If the first page your user navigates to doesn't include the appropriate EL, the JSF session bean is not instantiated missing the beginning of the user's session.
As such we need to step back and see what the J2EE stack can provide to us to solve this problem. The solution is 2 classes, a custom HttpSessionListener class and a custom Filter class.
J2EE applications can support creating a custom implementation of javax.servlet.http.HttpSessionListener interface. When configured within the project's web.xml file, on the start and end of any session, the custom listener's methods sessionCreated() and sessionDestroyed() are guaranteed to be called:
package view;
import // relevant imports
public class HttpSessionListener
implements javax.servlet.http.HttpSessionListener {
public HttpSessionListener() { }
public void sessionCreated(
HttpSessionEvent httpSessionEvent) { }
public void sessionDestroyed(
HttpSessionEvent httpSessionEvent) { }
}And the web.xml entry looks like:
<listener>
<listener-class>view.HttpSessionListener</listener-class>
</listener>What constitutes the start of a session? Whenever a new user accesses a web page within our application.
What constitutes the end of a session? Either when the user logs out of the session through a facility supplied by the programmer that calls the session.invalidate() method. In JSF this would be:
FacesContext fc = FacesContext.getCurrentInstance();
HttpServletRequest request =
(HttpServletRequest)fc.getExternalContext().getRequest();
request.getSession().invalidate();....or alternatively the session times-out, configured by the web.xml <session-timeout> entry (expressed in number of minutes).
With the sessionCreated() and sessionDestroyed() methods within the HttpSessionListener, we now have an ideal place to write-and-read values to-and-from the implicit J2EE session scope to capture the user logging in and out. For example:
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
String sessionScopeUser =
(String)session.getAttribute("User");
if (sessionScopeUser == null ||
sessionScopeUser.equals("")) {
session.setAttribute("User", "?????");
}
Long sessionScopeLoginDateTime =
(Long)session.getAttribute("LoginDateTime");
if (sessionScopeLoginDateTime == null) {
session.setAttribute("LoginDateTime",
System.currentTimeMillis());
}
}
public void sessionDestroyed(
HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
String user = (String)session.getAttribute("User");
Long loginDateTime =
(Long)session.getAttribute("LoginDateTime");
Date loginDateTime = new Date(loginDateTime);
Date logoutDateTime =
new Date(System.currentTimeMillis());
SimpleDateFormat format =
new SimpleDateFormat("dd/MM/yyyy hh:mm:ss");
String strLoginDateTime =
format.format(loginDateTime);
String strLogoutDateTime =
format.format(logoutDateTime);
// JDBC to write the values to the database
}For example you see in the sessionCreated() method that we can set a session variable LoginDateTime = System.currentTimeMillis, then in the sessionDestroyed() method we can retrieve the LoginDateTime and later write that to the database via a JDBC call.
However note an issue with the above solution. Within HttpSessionListener, we have no way to get at the authenticated user's details (essentially their account name). Unfortunately the HttpSessionListener doesn't have access to the Http request or response which does have access to the user's credentials, thus the limitation.
To get around this we introduce another J2EE construct called a javax.servlet.Filter. A class implementing this interface has a method called doFilter which is called on each Http request/response in our application. Within the Filter we are allowed to write to session scope, and in addition it has access to the user's credentials.
As such we can do something like this:
package view;
import // relevant imports
public class SessionFilter
implements javax.servlet.Filter {
public SessionFilter() { }
public void init(FilterConfig filterConfig)
throws ServletException { }
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain)
throws IOException,
ServletException {
if (servletRequest != null &&
servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (
HttpServletRequest)servletRequest;
HttpSession session = request.getSession();
String sessionScopeUser =
(String)session.getAttribute("User");
if (sessionScopeUser == null ||
sessionScopeUser.equals("")) {
String user = request. (cont..)
getUserPrincipal().toString();
if (user != null) {
session.setAttribute("User", user);
}
}
}
filterChain.doFilter(
servletRequest, servletResponse);
}
public void destroy() { }
}Note if the current session scope User variable is null, we grab the principal user from the request and write this to the session scope. Thus when the sessionDestroyed() method is called it has an actual User to work with.
To configure the Filter in the web.xml file you include the following entries:
<filter>
<filter-name>SessionFilter</filter-name>
<filter-class>view.SessionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SessionFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>Now the listener can grab the user name from the session scope supplied via the filter. This completes the solution.
There are a couple of caveats with this solution:
The user's credentials (account name) can only be retrieved by the Filter if the user has or is visiting a J2EE container based protected page of the application. If the user has yet to visit a protected page and thus hasn't been forced to login, the user's credentials will not yet be initialised. As such in creating an application where the user's details are recorded on entry and exit to the application, the user must be forced to log in regardless of which application web page they access. This is simply at the programmer's discretion depending on how they developer sets up the container based security.
(Note however at some point if the user does login, the filter will correctly pickup the user's name at that point, and that will be used by the end listener.sessionDestroyed() method thereafter defeating the caveat above. Also note there appears to be a difference between what the call to request.getUserPrincipal() returns in a number of cases when the user isn't authenticated. I believe if the app isn't configured against an LDAP security provider, it'll return null, but otherwise "anonymous" in the case of an LDAP security provider. Please ensure to test this functionality in your environment).
In testing this facility, you might notice if you force the user to logout via the invalidate() method described above that the HttpSessionListener sessionDestroyed() method is called twice, once when they logout, and a second time after some delayed time period. This occurs if after the logout you redirect the user back to a web page within your application. What you're essentially doing is starting another session (which may not be immediately obvious if you haven't added security/authentication requirements to all your web pages), and the delayed second call of the sessionDestroyed() method is a timeout occurring. The simple solution, on logout redirect the user to a web page outside of your application.
The solution above assumes you'll write the session information to the database via a JDBC call creating its own database connection. Ideally in an ADF BC project, you'd make use of the connection pool provided by ADF BC rather than creating wasteful dedicated database sessions.
A final note is the above solution is just that, and not necessarily the best way to do this. This code has not been tested, has not been run in a production environment, and therefore your mileage may vary. You have been warned! However hopefully this will be helpful to someone, or alternatively somebody will see the error in my ways and tell me a much better way to do this!